Core concepts
Products & catalogue
Your catalogue is the spine of everything personalised. Get SKUs, categories and product URLs right and the rest of Stitchwork works without further help.
Product shapes
Pick the type that matches how the thing is sold:
| Type | Use when | Variants |
|---|---|---|
simple |
One thing, one price. A notebook, a candle, a single guitar. | One, created automatically, inheriting the parent SKU, price, currency and status. |
configurable |
The customer chooses: size, colour, wood, finish. | Generated from the option axes you declare. |
variant |
Your export lists every variant as its own top-level row with no parent. | Managed by hand or by the importer. |
Status is draft, active or discontinued. Only
active products are ranked or recommended — the single most common cause
of an empty recommendations list is a catalogue that imported as drafts.
Fields that pull their weight
- sku
- The join key for everything: tracked events, imported order lines, external quote lines, category re-ranking. Up to 64 characters of letters, digits, dot, hyphen, underscore or slash. Unique per workspace when set.
- web_url
-
The product's page on your site. It is how a bare
page_viewis matched to a product, and it is what product QR codes point at. Without it you can still rank — you just have to send SKUs on every intent event, and you cannot make a product QR. - category
-
Free text, one per product. Drives the
categoryfilter on recommendations, the category-interest endpoint, and category balancing. Worth populating even if it is coarse. - base_price / base_compare_price
-
The price and the "was" price. When
base_compare_pricesits above the effective price, the product counts as on sale and the on_sale ranking signal can boost it. - created_at
- Settable on create and update. The newness ranking signal decays against it, so importing the real creation date from your source system means a five-year-old product doesn't rank as brand new the day you migrate.
- external_source / external_id
-
Your system's identifier for this product. Store it and you can ask for
recommendations by your own ids (
?external_ids=) instead of ours. - tags, attributes
- A string array and an arbitrary JSON blob. Carried through, not interpreted.
Options and variants
A configurable product declares its axes as options, then you generate the combinations.
# declare the axes
curl -X POST https://app.stitchwork.io/api/v1/products/12/options \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"name":"wood","display_name":"Wood","position":0,
"values":[{"value":"mahogany"},{"value":"walnut"}]}'
curl -X POST https://app.stitchwork.io/api/v1/products/12/options \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"name":"size","display_name":"Body size","position":1,
"values":[{"value":"O"},{"value":"OO"},{"value":"OOO"}]}'
# generate every combination
curl -X POST https://app.stitchwork.io/api/v1/products/12/generate-variants -b cookies.txt
The generator returns what it did:
{"version":"v1","data":{
"created": 6,
"discontinued": 0,
"total": 6,
"variants": [ … ]
}}
Option names must be 1–40 lowercase letters, digits or underscores
(display_name is the pretty version shown to staff). Re-running the
generator after changing an option is safe: new combinations are created, and
combinations that no longer exist are marked discontinued rather than
deleted — a variant that has appeared on a quote or an order must not vanish.
Editing a variant
Prices and statuses are per variant. Set them individually rather than through the parent:
curl -X PATCH https://app.stitchwork.io/api/v1/variants/88 \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"price":"2750.00","status":"active"}'
# and look one up by SKU when that's all you have
curl https://app.stitchwork.io/api/v1/variants/by-sku/CONCERTO-WAL-OO -b cookies.txt
Importing a catalogue
Products → Import, or POST /api/v1/products/import. Owner
only. Up to 20 MB and 50,000 rows per file; the endpoint is allowed to run for up
to ten minutes because being cut off mid-file is the worst possible outcome.
Three ways to send the file
# 1. multipart upload
curl -X POST https://app.stitchwork.io/api/v1/products/import \
-b cookies.txt -F "file=@catalogue.csv"
# 2. JSON body with a URL we fetch, or inline CSV
curl -X POST https://app.stitchwork.io/api/v1/products/import \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"url":"https://example.com/exports/catalogue.csv"}'
# 3. raw CSV body
curl -X POST https://app.stitchwork.io/api/v1/products/import \
-H "Content-Type: text/csv" -b cookies.txt --data-binary @catalogue.csv
Columns
Headers are matched case-insensitively and aliases are accepted, so most Shopify and
Magento exports work unmodified. Only product_sku is required.
| Column | Aliases | Notes |
|---|---|---|
product_sku | sku, parent_sku | Required. Matched against product SKUs first, then variant SKUs. |
product_name | name, title | 1–200 characters. |
product_type | type | simple / variant / configurable. virtual, bundle, downloadable, grouped, service and physical are coerced to simple. Blank becomes configurable if the row declares options, else simple. |
description | — | Free text. |
brand | — | Free text. |
category | categories | Magento paths are simplified: Default/Beds/All Beds → Beds, and a comma-separated list takes the first path. |
tags | — | Comma-separated. |
base_price | — | Number. |
base_compare_price | compare_price, compare_at_price, rrp, was_price, list_price, msrp | The "was" price. Feeds the on-sale signal. |
currency | — | 3-letter ISO code. |
status | — | draft / active / discontinued. |
web_url | url, product_url, url_key | The product page on your site. |
created_at | created, created_date, date_created | The source system's creation date. Read day-first for ambiguous formats. |
variant_sku | child_sku | Makes the row a variant of product_sku. |
variant_price | price | Per-variant price. |
variant_name, variant_status | — | Per-variant label and status. |
option:<name> | — | Repeatable. option:wood declares the wood axis and gives this row's value. |
configurable_variations | — | Magento's inline child list: sku=A,size=L,color=Red|sku=B,size=M,color=Blue. Child rows named here are absorbed as variants rather than imported as standalone products. |
What the importer guarantees
-
Idempotent on SKU. Re-running the same file updates rather than
duplicates. Products match on
products.skuthenproduct_variants.sku; variants match on(organization_id, sku), falling back to the option-value combination. - An update is a merge, not a replace. Only columns the CSV actually supplies are written. A three-column price refresh will not blank your descriptions or knock every product back to draft.
- Blank cells mean "leave alone". To clear a field, edit the product rather than importing an empty cell.
-
Rows fail individually. One bad row does not abort the file; it lands
in
errorswith its line number.
Reading the summary
{"version":"v1","data":{
"rows_total": 1204,
"rows_ok": 1198,
"rows_failed": 6,
"products_created": 310,
"products_updated": 88,
"variants_created": 806,
"variants_updated": 92,
"parents_grouped": 310,
"variants_grouped": 806,
"type_remapped": {"bundle": 4},
"variants_unmatched": [ … ],
"child_conflicts": [ … ],
"skus_sanitised": 2,
"errors": [{"line": 47, "message": "product_sku is required."}],
"warnings": [ … ]
}}
- variants_unmatched
- Rows that named a child SKU whose parent could not be resolved. Usually a typo in
product_sku, or a child listed before its parent in a file with noconfigurable_variations. - child_conflicts
- A child SKU claimed by two different parents. The first parent wins; fix the export.
- type_remapped
- Counts per source type that was coerced onto our enum — useful for spotting that half your catalogue arrived as
bundle. - skus_sanitised
- SKUs that contained characters we don't allow and were cleaned. Check them; a sanitised SKU no longer matches your source system.
Managing products over the API
# list, filtered
curl "https://app.stitchwork.io/api/v1/products?status=active&brand=Maison%20Aurelia&q=concerto" \
-b cookies.txt
# one product, with its options and variants
curl https://app.stitchwork.io/api/v1/products/12 -b cookies.txt
# create
curl -X POST https://app.stitchwork.io/api/v1/products \
-H "Content-Type: application/json" -b cookies.txt \
-d '{
"name": "Concerto Acoustic",
"sku": "CONCERTO",
"type": "configurable",
"status": "active",
"brand": "Maison Aurelia",
"category": "Acoustic guitars",
"base_price": "2500.00",
"base_compare_price": "2999.00",
"currency": "GBP",
"web_url": "https://example.com/guitars/concerto",
"external_source": "shopify",
"external_id": "gid://shopify/Product/123",
"tags": ["handmade","uk"]
}'
DELETE /api/v1/products/{id} sets the product's status to
discontinued. It stops being ranked and recommended, but every quote,
order and event that references it keeps working. There is no hard product delete —
that would rewrite history.
Featuring a product
A manual thumb on the scale. is_featured contributes the
featured signal, which is weighted at 2.0 by default — the heaviest of the
global weights.
curl -X PATCH https://app.stitchwork.io/api/v1/products/12/featured \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"featured": true}'
Owner or admin. See ranking signals.
Catalogue health checklist
- Products are
active, notdraft. - Every product has a
category. - Every product has a
web_urlthat matches the real page, including protocol. - Variant SKUs match exactly what your order export and your tag send — same case, same separators.
- The unmatched-SKU list on Rankings is short. Anything on it is a sale that isn't feeding the bestseller signal.
created_atreflects the real launch date, not the import date.