Core concepts
Quotes
A quote is a proposal with a customer's name on it and a link only they have. This page covers the whole lifecycle: building, totals, sending, the public page, payment, and turning an accepted quote into an order.
The lifecycle
┌────── POST /quotes/{id}/send ──────┐
│ ▼
┌───────┐ │ ┌──────┐ customer accepts ┌──────────┐ pays ┌──────┐
│ draft │───┘──▶│ sent │───────────────────▶│ accepted │─────────▶│ paid │
└───┬───┘ └──┬───┘ └────┬─────┘ └──────┘
│ │ customer declines │
│ └────────────▶ ┌──────────┐ │ convert-to-order
│ │ declined │ ▼
│ POST /quotes/{id}/cancel └──────────┘ ┌───────┐
└──────────────▶ ┌───────────┐ │ order │
│ cancelled │ └───────┘
└───────────┘
valid_until passes ──▶ expired
draft. Acceptance and decline are only possible from
sent; payment from sent or accepted.
POST /quotes/{id}/cancel is the soft path: the quote stops being
actionable but stays on the customer's history and in your totals.
DELETE /quotes/{id} removes it, its lines and its timeline for good, and
moves every total it counted towards. Delete exists so trial data can be cleared, not
as a nicer word for cancel. It is owner-only.
Creating a quote
Every quote needs a channel and a customer. Everything else has a default.
curl -X POST https://app.stitchwork.io/api/v1/quotes \
-H "Content-Type: application/json" -b cookies.txt \
-d '{
"channel_id": 2,
"customer_id": 14,
"currency": "GBP",
"discount_type": "percent",
"discount_value": "10",
"tax_rate": 20,
"tax_inclusive": true,
"notes_customer": "Delivery week commencing 14 April.",
"notes_internal": "Held the walnut back from the Mayfair floor stock.",
"valid_until": "2026-09-30",
"visitor_token": "optional __sw_v value",
"line_items": [
{"sku":"CONCERTO-WAL-OO","description":"Concerto in walnut, OO","quantity":1,"unit_price":"2850.00","product_variant_id":88}
]
}'
| Field | Default | Notes |
|---|---|---|
channel_id | — | Required. |
customer_id | — | Required. |
currency | GBP | 3-letter ISO code. |
discount_type | none | amount or percent. Anything else is treated as no discount. |
discount_value | null | Money for amount, 0–100 for percent. |
tax_rate | 0 | A percentage, e.g. 20. |
tax_inclusive | false | See Totals and tax. |
valid_until | +30 days | Any parseable date. |
notes_customer | null | Shown on the public quote page. |
notes_internal | null | Never sent to the customer — stripped from every public response. |
line_items | [] | Created in order. See below. |
visitor_token | — | Stitches the browser to this customer and stamps ad attribution onto the quote. Soft-failing. |
The response carries a generated quote_number (e.g. Q-2026-000007) and a public_token.
Line items
Lines are snapshots. Description, SKU and unit price are stored on the line, so editing a product later never rewrites a quote you already sent.
| Field | Notes |
|---|---|
description | What the customer reads. Blank becomes "Line item". |
quantity | Integer, minimum 1. |
unit_price | Money as a string. Non-numeric becomes 0.00. |
sku | Optional but recommended — it is what carries through to fulfilment. |
product_variant_id | Optional. Setting it links the line to the catalogue, which is what bumps the customer's affinity for that product and feeds the bestseller signal. |
# add a line to a draft
curl -X POST https://app.stitchwork.io/api/v1/quotes/7/lines \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"product_variant_id":88,"sku":"CONCERTO-WAL-OO","description":"Concerto in walnut, OO","quantity":1,"unit_price":"2850.00"}'
# change it
curl -X PATCH https://app.stitchwork.io/api/v1/quotes/7/lines/31 \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"quantity":2,"unit_price":"2750.00","description":"Concerto in walnut, OO"}'
# remove it
curl -X DELETE https://app.stitchwork.io/api/v1/quotes/7/lines/31 -b cookies.txt
Totals recalculate on every line change, inside the same transaction. Adding, editing or
removing a line on a non-draft quote returns 422.
Totals and tax
Four numbers, computed in this order:
subtotal = Σ (quantity × unit_price)
discount_amount = discount_type == 'amount' → min(discount_value, subtotal)
discount_type == 'percent' → subtotal × clamp(discount_value, 0, 100) / 100
otherwise → 0
taxable = max(0, subtotal − discount_amount)
Then the two tax modes diverge:
| Mode | tax_amount | total | Use when |
|---|---|---|---|
tax_inclusive: falsedefault |
taxable × rate% |
subtotal − discount + tax |
Your prices are ex-VAT and tax is added on top. Typical B2B. |
tax_inclusive: true |
taxable − taxable / (1 + rate/100)(informational) |
taxable |
Your shelf prices already contain VAT. Typical UK/EU retail. |
All four are stored as fixed 2-decimal values, so what you see is what is charged. A discount larger than the subtotal is capped rather than producing a negative total.
Sending
curl -X POST https://app.stitchwork.io/api/v1/quotes/7/send -b cookies.txt
This moves the quote to sent, stamps sent_at, and emails the customer a link to their public quote page. The customer must have an email on file, or you get 422.
Mail delivery is best-effort: if the mailer is misconfigured the status change still happens and the failure is logged, so you can copy the link by hand rather than lose the state transition. Grab the link any time:
curl https://app.stitchwork.io/api/v1/quotes/7/qr -b cookies.txt
# {"data":{"url":"https://app.stitchwork.io/q/9f2c…","token":"9f2c…"}}
Timeline notes
curl -X POST https://app.stitchwork.io/api/v1/quotes/7/notes \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"body":"Customer asked about a hard case.","visible_to_customer":false}'
The quote timeline records every state change, line change and note. Notes with
visible_to_customer: true appear on the public page; everything else is
internal.
The public quote page
/q/{token}. No login, no account. The 32-character token is the
authentication, and it is unguessable.
Fields stripped from every public response:
notes_internalcreated_by_user_id,created_by_name,created_by_emailstripe_payment_intent_id,stripe_charge_id,stripe_connect_account_id
What the customer can do:
| Action | Endpoint | Allowed from |
|---|---|---|
| Accept | POST /quotes/public/{token}/accept | sent |
| Decline | POST /quotes/public/{token}/decline | sent — optional reason |
| Pay by card | POST /quotes/public/{token}/pay | sent or accepted, Stripe connected |
| Share browsing | POST /quotes/public/{token}/join-code | Any state, if a join URL is configured |
Acting on a quote in the wrong state returns 409 with a plain-English reason.
Locking a workspace never breaks a quote the merchant's own customer is halfway through paying for. Public quote routes never resolve a workspace through the access gate.
"Find my quote" on your own site
A customer who has lost the email can retrieve their quote from a form on your site, authenticated by your public key plus the email on the quote:
curl -X POST https://app.stitchwork.io/api/v1/quotes/public/lookup \
-H "Authorization: Public pk_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"number":"Q-2026-000007","email":"ada@example.com"}'
CORS-enabled, so it can be called straight from the browser. Any miss — wrong number,
wrong email, missing key — returns an identical generic 404, and the email
comparison runs even when there is no quote so the timing is the same either way. That
is deliberate: it stops anyone walking Q-2026-000001 through
Q-2026-009999.
Taking payment
Optional, and it settles to your Stripe account, not ours. Onboarding creates a Stripe Express account and returns a Stripe-hosted link to complete it:
curl -X POST https://app.stitchwork.io/api/v1/billing/connect/onboard \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"country":"GB"}'
# {"data":{"url":"https://connect.stripe.com/setup/…","account_id":"acct_…"}}
curl https://app.stitchwork.io/api/v1/billing/connect/status -b cookies.txt
# {"data":{"account_id":"acct_…","charges_enabled":true,"payouts_enabled":true,"country":"GB"}}
Owner only. Once charges_enabled is true, the pay button appears on public quote pages.
Paying creates a Stripe PaymentIntent as a direct charge on your account, with a
Stitchwork application fee on top (a basis-point rate set per deployment — 300 bps
= 3% by default). Stripe's webhook then marks the quote paid and records
the charge id. A failed payment lands as a note on the quote's timeline rather than
changing its status.
The quote is marked paid by the Stripe webhook, not by the browser coming back to a return page. A customer who pays and immediately closes their phone still ends up with a paid quote.
Handing off to your own checkout
If you would rather the customer completed on your site, set a URL template in Settings → Quote completion. The public page then shows a button pointing at the resolved URL.
curl -X PATCH https://app.stitchwork.io/api/v1/settings/quote-complete-url \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"url":"https://example.com/cart/prefill?ref={quote_number}&email={email}&total={total}"}'
Tokens use {name} or [name], are case-insensitive, and are URL-encoded on substitution:
| Token | Also accepts | Value |
|---|---|---|
{quote_number} | {number}, {order_id}, {name} | Q-2026-000007 |
{quote_id} | {id} | Numeric id |
{token} | {public_token} | The public token |
{customer_email} | {email} | Email on the quote |
{customer_name} | — | Name on the quote |
{total} | — | Quote total |
{currency} | — | 3-letter code |
An unknown token is left visible in the resolved URL rather than silently stripped, so a
typo shows up the first time you test it. Maximum 1024 characters; must start with
http:// or https://.
Browsing inside the builder
The quote builder shows what the customer has been looking at, with matched products joined inline so each row can be added to the quote in one click:
curl "https://app.stitchwork.io/api/v1/quotes/7/browsing?types=view_product,add_to_cart" -b cookies.txt
Fifty most recent events for the quote's customer. Nothing appears here until that customer has a stitched visitor.
Quotes created from your website
Let a customer build a basket online and walk into the store to an agent who already has the draft open. Server-to-server, authenticated with the secret key:
curl -X POST https://app.stitchwork.io/api/v1/quotes/external \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 7,
"customer": {"email":"ada@example.com","name":"Ada Lovelace","phone":"+44…"},
"visitor_token": "the __sw_v cookie value, if you have it",
"currency": "GBP",
"tax_rate": 20,
"tax_inclusive": true,
"line_items": [
{"sku":"CONCERTO-WAL-OO","description":"Concerto in walnut","quantity":1,"unit_price":"2850.00"}
]
}'
The customer is upserted by email, the visitor is stitched, attribution is stamped, and
the response includes public_url — redirect the shopper there or email it.
The quote appears in the app tagged External so the team can see it came
from the website rather than a till.
Converting to an order
curl -X POST https://app.stitchwork.io/api/v1/quotes/7/convert-to-order -b cookies.txt
Only from accepted or paid — anything else returns
409. The new order links back through orders.quote_id, and the
conversion is idempotent: clicking twice finds the existing order rather than creating a
second one.
See Orders for what happens next.
Quote events
Every state change and line change emits a webhook:
quote.created quote.line_added
quote.updated quote.line_updated
quote.sent quote.line_removed
quote.accepted quote.deleted
quote.declined
quote.cancelled
quote.paid
data.quote is the same shape the JSON API returns for a quote. See
Webhooks.