Core concepts
Customers & identity
A customer is a named person. A visitor is an anonymous browser. The interesting part is what happens when the two become one — and what happens to that link when things get deleted.
The customer record
Customers are unique per workspace by email. That single constraint is what makes bulk order imports, external quote creation and external order creation all idempotent: creating a customer whose email already exists updates the existing record rather than failing.
curl -X POST https://app.stitchwork.io/api/v1/customers \
-H "Content-Type: application/json" -b cookies.txt \
-d '{
"name": "Eleanor Vance",
"email": "eleanor@example.co.uk",
"phone": "+44 20 7946 0958",
"company": "Vance & Co.",
"notes": "Prefers figured grain. Visits Mayfair twice yearly.",
"attributes": {"vip_tier": "gold"}
}'
name and a valid email are required; everything else is
optional. attributes is an arbitrary JSON blob carried through untouched —
use it for loyalty ids, segment membership, anything your own systems need to round-trip.
Searching matches name, email and company:
curl "https://app.stitchwork.io/api/v1/customers?q=vance" -b cookies.txt
The activity timeline
Everything a customer has done, newest first — web browsing, product views, cart adds, searches, store visits and campaign arrivals, in one stream.
# everything, most recent 50
curl https://app.stitchwork.io/api/v1/customers/14/events -b cookies.txt
# just the commercially interesting types, more of them
curl "https://app.stitchwork.io/api/v1/customers/14/events?types=view_product,add_to_cart,qr_scan&limit=200" \
-b cookies.txt
limit is clamped to 1–200 and defaults to 50. types is a
comma-separated list of event types. Matched
products are joined inline, which is what lets the quote builder render add-to-quote
cards straight off a browsing row.
It covers every visitor linked to this customer, not just the one in front of you. Someone who browsed on their phone, scanned a QR in store and then bought on a laptop appears as one continuous history.
Visitors
A visitor is one browser on your domain, identified by the value of the
first-party __sw_v cookie. Stitchwork never reads that cookie
cross-origin — the tag sends its value in the request body, and the server only ever
sees a token.
Each visitor row carries:
- visitor_token
- The cookie value. Unique per workspace.
- customer_id
- Null until stitched. Setting it is what turns anonymous browsing into a named history.
- first_seen_at, last_seen_at
- Bumped on every event.
- user_agent, ip_hash
- The IP is never stored raw — only a salted SHA-256 hash, with a per-deployment salt.
The cookie has a sliding two-year expiry: every event re-writes it, so an actively returning customer keeps the same identity indefinitely.
Identity stitching
Four routes from anonymous to known. All of them do the same thing in the end — set
visitors.customer_id — and all of them retroactively attribute every event
that visitor ever fired.
1. The QR handoff (in store)
The agent has a quote open. The customer scans a QR from the agent's screen, and their own phone does the rest.
AGENT SCREEN CUSTOMER'S PHONE YOUR SITE STITCHWORK
────────────── ──────────────── ───────── ──────────
quote detail
"start session" ──POST──▶ /quotes/public/{token}/join-code ─────────────────────▶ mint code
◀──────── { code, bridge_url } ◀──────────────────── 5-min TTL
renders QR of
bridge_url
scan ───▶ https://you.example/__stitchwork/join?code=…&return=…
reads __sw_v cookie
POST /track/join ──▶ link visitor
(Bearer sk_live_) → customer
302 back to /q/{token}?joined=1
Join codes are single-use and expire after five minutes. The response tells you exactly why a link didn't happen:
| Reason | Status | Meaning |
|---|---|---|
missing_fields | 422 | No visitor_token or no join_code in the body. |
expired | 410 | The code was already used, or more than five minutes old. |
wrong_org | 403 | The code belongs to a different workspace than the bearer key. |
no_visitor | 200 | That token has never fired an event — usually the customer has never been on your site, or consent blocked the tag. |
no_customer | 410 | The quote behind the code has no customer. |
visitor_other_customer | 409 | That browser is already linked to somebody else. A visitor belongs to one customer, and re-pointing it silently would be worse. |
A successful link writes a "Browsing shared by customer" row onto the quote's timeline, so the team can see the handoff happened.
2. External quote creation
Your website builds a basket, the shopper clicks "reserve in store", and your backend
posts the quote — passing the __sw_v cookie value it can read on its own
domain. The visitor is linked before the agent ever opens the quote.
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"},
"visitor_token": "the __sw_v cookie value",
"line_items": [{"sku":"CONCERTO-MAH-OO","quantity":1,"unit_price":"2600.00"}]
}'
3. External order creation
Same idea at the point of sale. Your checkout books the order and passes the visitor token; Stitchwork upserts the customer by email, stitches the visitor, and snapshots the ad-touch trail onto the order.
4. In-app quote creation
A quote created through the normal API can carry a visitor_token too. It
stitches and stamps attribution at the same moment the quote is created.
If the token doesn't resolve, or the visitor already belongs to someone else, the quote or order is still created. Identity is valuable; it is never worth losing a sale record over.
Campaign attribution
Every time the tag sees UTM parameters or an ad-platform click id
(gclid, fbclid, msclkid) on a landing URL, it
records a touch against the visitor — source, medium, campaign, term,
content, click id, landing URL and referrer.
Two things then happen:
- A
campaign_touchevent lands on the customer's timeline, so a human can see "arrived from the spring campaign". - A row lands in the touch table, which is the multi-touch source of truth.
When a quote or an order is created with a resolvable visitor_token, a
snapshot of that trail is stamped onto the conversion —
{"first_touch":{…},"last_touch":{…},"touch_count":N} — so it keeps its
attributed source even if the visitor's identity is later reassigned.
The window and default model are workspace settings:
curl -X PATCH https://app.stitchwork.io/api/v1/settings/attribution \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"conversion_window_days": 60, "model": "position_based"}'
conversion_window_days is clamped to 1–365 (default 90).
model is one of time_decay (default), linear,
position_based, last_touch, first_touch. Touches
are captured regardless of these — the settings govern how they are credited.
Deduplication is client-side: a 30-minute __sw_attr cookie stops a refresh
or a same-visit return to the landing URL re-recording the same touch. UTM parameters are
deliberately not stripped from the URL, because your own analytics expect them.
Deleting a customer
Owner-only, and deliberately two-step. Because a quote's customer reference is restrictive — a stranded quote is worse than none — deleting a customer with history takes their quotes and orders with them. The first call refuses and tells you exactly what would go:
curl -X DELETE https://app.stitchwork.io/api/v1/customers/14 -b cookies.txt
HTTP/1.1 409 Conflict
{"error":"This customer has 3 quotes and 1 order. Deleting them removes those too, and the totals they count towards.",
"data":{"requires_cascade":true,"dependents":{"quotes":3,"orders":1}}}
Repeat with ?cascade=1 to go ahead. It runs in one transaction, so a half-deleted customer is not possible.
curl -X DELETE "https://app.stitchwork.io/api/v1/customers/14?cascade=1" -b cookies.txt
Orders survive their originating quote — orders.quote_id simply becomes
null — so tidying a quote away never destroys the sale recorded behind it.
Delete a quote and the visitor stays recognised as that person; only the quote's spent one-time join codes go with it. Delete a customer and the visitor is detached, not destroyed: the cookie, its events and its ad touches survive as anonymous browsing, ready to be identified again by a later quote.
Recommendations for a customer
The customer detail page and the quote builder both call the staff recommendations
endpoint with a customer_id:
curl "https://app.stitchwork.io/api/v1/recommendations?customer_id=14&limit=12" -b cookies.txt
Naming a customer is a request for that person's list, so the result is restricted to products they have actually engaged with. If they have no history, you get an empty list rather than the global bestsellers dressed up as personal — an agent reading a list aloud has no way to tell those two apart otherwise.
Every staff call returns a diagnostics block explaining the result. See
Diagnostics.