Stitchwork Docs
stitchwork.io

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.

The timeline spans devices

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
The join endpoint is one small route on your domain — that is the only place the first-party cookie is readable. Ready-made implementations for Node, PHP, Next.js and Cloudflare Workers are in the recipes.

Join codes are single-use and expire after five minutes. The response tells you exactly why a link didn't happen:

ReasonStatusMeaning
missing_fields422No visitor_token or no join_code in the body.
expired410The code was already used, or more than five minutes old.
wrong_org403The code belongs to a different workspace than the bearer key.
no_visitor200That token has never fired an event — usually the customer has never been on your site, or consent blocked the tag.
no_customer410The quote behind the code has no customer.
visitor_other_customer409That 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.

Stitching is soft-failing everywhere

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:

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.

Deleting never cuts the tracking connection

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.