Skip to content

Binding a browser to a person

Event: a person becomes known — a lead is created, someone signs in, an agent learns who it is talking to. Endpoint: POST /v1/identify Status: live in dev, verified end to end

Who sends this. Stitch does not know and does not need to. It sees a key, which names the system the owner issued it to, and a payload. Today the likeliest emitters are the form handler or CRM at the moment a contact is created, the borrower portal when someone signs in, and an AI agent that learns an identity mid-conversation. A middleware in front of any of them, or a replacement for any of them, sends exactly the same thing.


Authorization: Bearer <your key>

The owner issues one key per emitting system, named after whatever actually sends, and holds it in the collector’s WEBHOOK_KEYS secret. A request without a key, or with one the collector does not know, is refused with 401 {"error":"unauthorized"} and writes nothing. The system a key belongs to is recorded on every row it creates as source_system.


1. Bind at birth: the highest-leverage call in the integration

Section titled “1. Bind at birth: the highest-leverage call in the integration”

When a web form, chat or call creates a contact, send the visitor’s anon_id with the contact’s id. Stitch then binds their entire prior browsing history to the person in one deterministic call.

Web form / chat / call
→ the handler reads the _bid cookie and passes { anon_id, traits } along WITH the contact
→ a contact is created in the system of record (the anon_id stored on it, or beside it)
→ whatever sends this event (that system, or a middleware fronting it)
→ POST /v1/identify { anon_id, traits, external_ids }
→ Stitch binds all prior browsing to the person, instantly

The one thing that must not be lost on the way: the anon_id has to travel with the contact to whatever ends up sending this call. If the form handler reads the cookie but does not pass it on, a middleware downstream has nothing to send, and the browsing history never binds.

The _bid cookie is HttpOnly, so page JavaScript cannot read it. That is deliberate: an anon_id readable by page JS is a shareable identifier; paste such a link into Slack and five people merge into one person, which is silent corruption that is very hard to detect later.

A server-side handler reads the cookie from the inbound request:

Cookie: _bid=3f1a8c2e-9b4d-4a71-8e2f-0c5d6a7b8e91

and passes the value through as anon_id. makeabeeline.com and app.makeabeeline.com share a registrable domain, so the cookie already spans both; a sign-in on the app host carries the marketing browsing with it. No decoration, no token, no matching heuristics.

Terminal window
curl -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/identify \
-H "content-type: application/json" \
-H "authorization: Bearer <your key>" \
-d '{
"anon_id": "3f1a8c2e-9b4d-4a71-8e2f-0c5d6a7b8e91",
"traits": { "email": "jane.doe@example.com", "phone": "(415) 555-0132" },
"external_ids": { "relcu_contact_id": "88213" }
}'
{ "person_id": "01fa113c-ae40-4671-851f-29707ed6c2da", "created": true, "merges": 0 }

At least one of traits.email, traits.phone or an external_ids entry is required: an anon_id on its own identifies nobody and is a 400. Recognised external ids: relcu_contact_id (also accepted as octopus_id or los_contact_id), relcu_lead_id, vesta_borrower_id, vesta_loan_number, magicblocks_contact_id. These are the keys the collector recognises; which system holds them is the owner’s business, not Stitch’s.

On PII. Email and phone are normalised and SHA-256’d inside the Worker, before anything touches storage. The plaintext exists only for the duration of that request and is never enqueued, logged or written. You may send them; they help match a returning person across devices. You may equally omit them — the contact id alone is enough. Normalisation means casing, whitespace and +tag suffixes still match, so values need no cleaning first.

Send it on every sign-in, not only the first

Section titled “Send it on every sign-in, not only the first”

For an authenticated host, send it on every authenticated session start. It is one call, it is idempotent, and it repairs any drift. An authenticated session is deterministic identity, so what it binds is recorded at certainty tier A.

The anonymous history and the contact stay two islands, and the most valuable join in the system falls back to probabilistic stitching. With the lead → loan link already deterministic (see lead-events.md and loan-stages.md), this is the last remaining probabilistic step in the whole conversion path.


When the web form carried a hidden stitch_submission_id (the SDK mints it, see web-sdk.md §3a), it belongs on the lead’s created event as lead.attrs.submission_id — see lead-events.md. Stitch uses it as the lead conversion’s event id, so the pixel’s event and the server’s are one event to the platform. Anything not shaped like an id ([A-Za-z0-9_-], 8–64 characters) is ignored.


Where Stitch’s identity graph says two identities are one person and the system of record says they are two contacts, the system of record wins. The pair is written to contested_identities for ops review, because a genuine duplicate contact is itself worth surfacing.


Terminal window
STAMP=$(date +%s)
curl -sS -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/identify \
-H "content-type: application/json" \
-H "authorization: Bearer <your key>" \
-d "{\"anon_id\":\"smoke-anon-$STAMP\",\"external_ids\":{\"relcu_contact_id\":\"smoke-oct-$STAMP\"}}"

Expect 200 and a person_id. Send it again and expect the same person_id with "created": false.