Stitchwork Docs
stitchwork.io

Core concepts

Orders

An order is a sale that happened. Orders are first-class records, not a kind of quote — most merchants have far more orders than quotes, and most of those orders never went near one.

Why orders matter here

Orders do three jobs in Stitchwork:

Import history first

Importing a year or two of past orders is the single highest-leverage thing you can do in a trial. It turns recommendations from empty into credible on day two rather than day sixty.

The order record

FieldNotes
channel_idRequired. Which surface the sale happened on.
customer_idOptional — guest checkout is supported and common.
quote_idSet when the order came from a quote conversion. Becomes null if that quote is later deleted; the order survives.
order_numberYour human-readable reference. Unique per workspace when set.
external_source / external_idThe idempotency key. Together they make re-imports and retries safe.
statuspaid, refunded, cancelled, fulfilled. Only paid and fulfilled count towards revenue and ranking.
currency3-letter ISO code, default GBP.
subtotal, discount_amount, tax_amount, totalSnapshot totals, taken as given. Orders record what happened; they don't recompute it.
placed_atWhen the sale happened, not when the row was written. This is what the ranking window measures against — get it right on imports.
created_viaapp, import, quote_conversion or external.
attributionAd-touch snapshot, when a visitor token was resolvable at creation.

Line items carry a description, SKU, quantity, unit price, line total and a soft link to a product variant. Soft means a line whose SKU matches nothing still records — it just doesn't feed ranking, and it shows up in the unmatched-SKU list so you can fix the catalogue.

Importing from CSV

Orders → Import, or POST /api/v1/orders/import. Owner only. Same three ingestion paths as the product importer: multipart upload, a JSON body with url or csv, or a raw text/csv body. 20 MB and 50,000 rows.

The format is Shopify-shaped and denormalised: one row per line item, with rows grouped by order_number.

order_number,email,customer_name,sku,quantity,unit_price,line_total,placed_at,channel,external_source,external_id
1001,eleanor@example.co.uk,Eleanor Vance,CONCERTO-MAH-OO,1,2600.00,2600.00,2025-11-04,Website,shopify,4455661
1001,eleanor@example.co.uk,Eleanor Vance,SKETCHPAD,2,45.00,90.00,2025-11-04,Website,shopify,4455661
1002,marcus@example.com,Marcus Pemberton,SKETCHPAD,3,45.00,135.00,2025-11-06,Mayfair,shopify,4455662

Columns

ColumnAliasesNotes
order_numberorder, order_id, nameRequired. Rows sharing one become one order.
customer_emailemailCustomers are upserted by email as the import runs.
customer_namenameUsed when creating a new customer.
customer_phonephoneOptional.
skuvariant_sku, product_sku, item_skuSoft-linked to a variant. This is what feeds ranking.
qtyquantityDefaults to 1.
unit_pricepriceMoney.
line_totaltotalFalls back to qty × unit_price.
placed_atordered_at, order_date, created_atAmbiguous dates are read day-first.
channelchannel_name, store, locationMatched to a channel by name. Unmatched rows land in an auto-created Imported orders channel.
external_sourcesourceHalf the idempotency key.
external_idexternal_order_idThe other half. Missing, it is derived from (external_source, order_number).
descriptionproduct_name, titleWhat appears on the line.
Re-importing is safe

Orders are upserted on (organization_id, external_source, external_id), so re-running last month's export updates totals and items rather than duplicating them. You can export-and-import on a schedule without deduplicating first.

The summary

{"version":"v1","data":{
  "rows_total": 8412,
  "rows_ok": 8409,
  "rows_failed": 3,
  "orders_created": 2210,
  "orders_updated": 0,
  "customers_created": 1804,
  "line_items": 8409,
  "channel_auto_created": false,
  "ranking_recomputed": true,
  "errors": [{"line": 91, "message": "order_number is required."}]
}}

ranking_recomputed is the useful one: the importer triggers a full ranking recompute when it finishes, so imported historical sales feed the bestseller signal immediately rather than waiting for you to notice a button. If that recompute fails the import still succeeded — the failure is reported as an error entry and you can retrigger it by hand.

Converting a quote

curl -X POST https://app.stitchwork.io/api/v1/quotes/7/convert-to-order -b cookies.txt

The resulting order:

Only accepted or paid quotes convert; anything else is 409. Conversion is idempotent — it is keyed on external_id = "quote:{id}", so clicking twice returns the same order.

Creating orders from your backend

For merchants whose checkout lives elsewhere. Server-to-server, authenticated with the secret tracking key — there is no session-authenticated "create order" endpoint.

curl -X POST https://app.stitchwork.io/api/v1/orders/external \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": 2,
    "customer": {"email":"ada@example.com","name":"Ada Lovelace"},
    "visitor_token": "the __sw_v cookie value",
    "order_number": "1001",
    "external_source": "shopify",
    "external_id": "gid://shopify/Order/4455661",
    "status": "paid",
    "currency": "GBP",
    "placed_at": "2026-04-02T14:05:00Z",
    "discount_amount": "0.00",
    "tax_amount": "433.33",
    "line_items": [
      {"sku":"CONCERTO-MAH-OO","description":"Concerto in mahogany, OO","quantity":1,"unit_price":"2600.00","line_total":"2600.00"}
    ]
  }'

What it does, in order:

  1. Validates that channel_id belongs to your workspace.
  2. Upserts the customer by email.
  3. Stitches visitor_token to that customer and snapshots the ad-touch trail onto the order.
  4. Resolves each line's SKU to a variant where it can.
  5. Upserts the order on (external_source, external_id).
FieldDefault / behaviour
channel_idRequired, must belong to the workspace.
customer.emailRequired.
line_itemsRequired, at least one.
statusDefaults to paid. Anything outside the four valid values falls back to paid.
currencyDefaults to GBP.
placed_atDefaults to now. Any parseable date.
external_sourceDefaults to "external".
external_idDerived from source + order number when omitted. Set it explicitly if you want retries to be idempotent.
totalComputed as subtotal − discount + tax unless you supply it.
line line_totalComputed as quantity × unit_price unless you supply it.
line product_variant_idResolved from sku when omitted.
Always send external_id

Without one, a retry after a network timeout generates a random id and creates a second order. With one, the retry updates the first.

Reading orders

# list, filtered
curl "https://app.stitchwork.io/api/v1/orders?status=paid&customer_id=14&q=1001" -b cookies.txt

# one order with its line items
curl https://app.stitchwork.io/api/v1/orders/91 -b cookies.txt

Filters: q (order number / customer), status, customer_id, channel_id.

Deleting an order

curl -X DELETE https://app.stitchwork.io/api/v1/orders/91 -b cookies.txt

Owner-only, and real: the line items go, and the sale stops counting towards revenue and the bestseller signal. That is deliberate — this exists so imported test data can be cleared rather than lived with. A quote the order was converted from is left untouched.

Order events

order.created
order.updated
order.deleted

Note that a re-import which updates an existing order emits order.updated, not order.created. Receivers should key on the order's external_id, not on assuming creation order. See Webhooks.