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
| Table | Purpose |
|---|---|
products | Title, description, vendor, type, tags, status, SEO, settings |
product_options | Up to 3 per product (Size, Color, Material) with values list |
variants | option1/2/3 (Shopify-exact), SKU, barcode, weight, fulfillment_mode, gift_card_metadata |
variant_pricing | Per (variant × market) — both price_with_tax_cents and price_without_tax_cents, plus price_source and tax_rate_bps |
collections | Manual or smart (rules JSONB) |
collection_products | M:N for manual collections |
product_media | Universal 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
| Verb | Path |
|---|---|
| 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:
rulesJSONB 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.