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.
Authentication
Section titled “Authentication”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, instantlyThe 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.
Where the anon_id comes from
Section titled “Where the anon_id comes from”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-0c5d6a7b8e91and 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.
The call
Section titled “The call”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.
Without this call
Section titled “Without this call”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.
2. The submission id
Section titled “2. The submission id”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.
3. Conflict rule
Section titled “3. Conflict rule”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.
4. Smoke test
Section titled “4. Smoke test”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.