Products & variants

Shopify-style product structure: product → 1..N variants. Variants are what you actually sell (specific size + colour + SKU). Pricing hangs off the variant per market.

Schema in one breath

TablePurpose
productsTitle, description, vendor, type, tags, status, SEO, settings
product_optionsUp to 3 per product (Size, Color, Material) with values list
variantsoption1/2/3 (Shopify-exact), SKU, barcode, weight, fulfillment_mode, gift_card_metadata
variant_pricingPer (variant × market) — both price_with_tax_cents and price_without_tax_cents, plus price_source and tax_rate_bps
collectionsManual or smart (rules JSONB)
collection_productsM:N for manual collections
product_mediaUniversal image / video / 3d_model with R2 URLs

Create a product + variant + price

# Product
curl -X POST "$API/products.json" \
  -H "Authorization: Bearer $NEVIOS_KEY" -H "Content-Type: application/json" \
  -d '{
    "handle": "trika-bavlna",
    "title": "Bavlněné triko",
    "description": "100% bio bavlna.",
    "vendor": "Acme",
    "product_type": "Apparel",
    "tags": ["new", "summer"],
    "status": "active"
  }'
# Add an option
curl -X POST "$API/products/{product_id}/options.json" \
  -H "Authorization: Bearer $NEVIOS_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Velikost","values":["S","M","L"],"position":1}'
# Variant (option1 must match an option value)
curl -X POST "$API/products/{product_id}/variants.json" \
  -H "Authorization: Bearer $NEVIOS_KEY" -H "Content-Type: application/json" \
  -d '{
    "sku": "TRI-001-M",
    "option1": "M",
    "weight_grams": 250,
    "barcode": "8590000000000"
  }'
# Pricing — gross 499 CZK with 21% VAT, both fields auto-derived
curl -X PUT "$API/variants/{variant_id}/pricing.json" \
  -H "Authorization: Bearer $NEVIOS_KEY" -H "Content-Type: application/json" \
  -d '{
    "market_id": 308...,
    "price_with_tax_cents": 49900,
    "tax_rate_bps": 2100,
    "price_source": "gross"
  }'

The pricing engine computes price_without_tax_cents from your gross + tax rate (net = gross × 10000 / (10000 + bps)). Or pass price_source: "net" and the gross is derived. price_source preserves ground truth so you can re-derive cleanly when a tax rate changes.

Filter products

GET /admin/2026-01/{handle}/products.json
  ?status=active
  &vendor=Acme
  &product_type=Apparel
  &tag=summer
  &title=triko          # fuzzy
  &limit=50
  &cursor_id=...        # cursor pagination

Variants

VerbPath
POST/products/{id}/variants.json
PUT/variants/{id}.json
PUT/variants/{id}/pricing.json
DELETE/variants/{id}.json (archive)

gift_card_metadata opts a variant into being a gift-card seller — its purchase produces a Voucher at order time.

Collections

Collections group products for the storefront's category pages.

  • Manual: explicit list via collection_products (M:N).
  • Smart: rules JSONB DSL ({"all": [{"field":"tag","op":"contains","value":"sale"}]}). Materialized members refreshed on demand.

Media

product_media uploads (image/video/3d_model) live in Cloudflare R2. Returns CDN URLs. Each row has mime, width/height, alt, position. Used by storefront variant galleries.

Next: Inventory.