> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Stitch anonymous sessions into known contacts

> Bridge an anonymous session to a known person — the automatic /sdk/identify bind for web tracking, and the operator POST /cdp/identity-stitch for late-declared links the merge console would otherwise miss.

# Stitch anonymous sessions into known contacts

Anonymous visits are a fact every tenant builds around: a visitor reads the site before they ever log in, a caller reaches you before their number resolves to a profile, a video-lobby guest joins by link. Until an identifier binds them to a person, each of those sessions sits on its own contact row and the story splits. Orbit has two stitch seams — pick the one that matches WHEN you learn who the visitor is:

| Seam                          | Endpoint                                        | When it applies                                                                                        |
| ----------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Real-time bind                | `POST /sdk/identify` (browser public key)       | The site learns who the visitor is in-session — a login, a form submission, a captured phone or email. |
| Late-declared operator stitch | `POST /api/v1/cdp/identity-stitch` (server key) | A backend job or an operator discovers the link after the fact and declares it explicitly.             |

## 1. Real-time bind — `POST /sdk/identify`

The moment your site learns who the visitor is, bind the strong identifier on the anonymous session:

```bash theme={null}
curl -X POST 'https://api.orbit.devotel.io/sdk/identify' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dv_live_pk_your_public_key' \
  -d '{
    "anonymous_id": "anon_8f3c2b1d",
    "phone": "+14155551234",
    "display_name": "Ava Chen",
    "traits": { "plan": "trial" }
  }'
```

One call does four things:

* **Web tracking backfill** — every pre-identify browser event captured under the same `anonymous_id` is backfilled onto the resolved contact, so the contact-360 timeline reads the full funnel (not "first seen after login").
* **Messaging** — inbound messaging replies (SMS, WhatsApp, and email) resolve by the phone or email you just bound; the channel-carried identifier does the binding from there.
* **Voice** — inbound callers resolve by the same phone number the identify call bound, so a returning call lands on the same contact the browser session folded into.
* **Video** — video joins stamped on the SDK-identified contact attribute to them; pre-identify joins stay anonymous-scoped until the visitor identifies.

Pass `phone` or `email` (prefer your own stable identifier where possible; at least one of the two is required). Re-calling with the same identifier and the same `anonymous_id` is a no-op once the contact exists, so SDKs can retry safely. Every identify call also lands a chain entry in your audit feed naming the bound identifier and each surface the stitch unlocked — a compliance "which channels did this identify enable?" trace is one query away. See the [Embedded SDK API reference](/api-reference/sdk) for the full envelope, and the [web SDK](/sdks/web) if the site calls this from its own library rather than raw `fetch`.

## 2. Late-declared operator stitch — `POST /api/v1/cdp/identity-stitch`

The automatic bind above only fires when the web SDK can name the known identifier in-session. It never guesses at a merge the deterministic rules would reject, and it won't fold two live contact rows you only later discover are the same person — a widget visitor bound after an inbound voice call created their primary, or a video-lobby guest leg that only joins by link. That late-discovered link is exactly what the server-key stitch endpoint declares:

```bash theme={null}
curl -X POST 'https://api.orbit.devotel.io/api/v1/cdp/identity-stitch' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dv_live_sk_your_private_key' \
  -d '{
    "anonymous_id": "anon_8f3c2b1d",
    "known_id": "crm-44882"
  }'
```

The endpoint resolves both ids against your live contact graph and collapses them into one record in a single durable transaction — messages, conversations, agent conversations, CDP events, video participants, and consent all fold onto the survivor, the same cascade the [merge console](/guides/contact-merge) uses. The response reports exactly what happened:

| `outcome` | Meaning                                                                                                                                                                                       |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `merged`  | Both ids resolved to distinct live contacts; the anonymous row folded into the known row. `contact_id` names the survivor.                                                                    |
| `linked`  | Only the anonymous id resolved; its contact's `external_id` was bound to the known id (first-time link, like a one-way alias).                                                                |
| `noop`    | The pair needs no action — both resolve to the same contact already, or one side resolves to nothing (`reason` names why: `already_stitched`, `no_contact_resolved`, `anonymous_missing`).    |
| `refused` | The `known_id` is phone/email-shaped — route those through `/sdk/identify` or `/cdp/identify` with explicit trait keys so a shape-unsafe value can never silently collapse the wrong contact. |
| `error`   | A merge or link failure (usually a concurrent update race); the stitch is idempotent — replay it once the pair is stable and it resolves to a no-op.                                          |

Two guardrails make this safe to run from an operator console:

* **Shape gate.** A `known_id` that looks like a phone number or email address is refused, because phone/email must flow through identify with an explicit trait key — never fold a contact here.
* **Audited.** Every call writes one consolidated `cdp.identity_stitched` audit entry with the pair and the outcome, so the fold-in is replayable from your operator console. Replies stay `200`-class even on the refused and error outcomes, so a console can tone the row off one field.

Requires a server key with the `contacts:write` scope and the `owner`, `admin`, or `developer` role — collapsing identities is a tenant-wide attribution decision.

## 3. How the stitch plays with merges and consent

A stitch is one of the three canonical anonymous → known transitions in Orbit:

* **In-session bind** (`/sdk/identify`, `/cdp/identify`) — you ship the anonymous and the known identifier together.
* **Pairwise collapse** (`/cdp/alias`) — your ingest key declares that one id folds into another; stages 2 and 3 of the canonical CDP call.
* **Late-declared operator stitch** (`/cdp/identity-stitch`) — an authenticated surface resolves both ids against live contacts when the first two missed.

After the fold-in, the survivor follows the same rules as any merged record: the contact's [survivorship policy](/guides/identity-resolution) decides which scalar fields live on the last write, and the merged record's consent verdicts (checked via `GET /api/v1/cdp/consent/:contactId`) govern whether each folded-in channel may still be contacted. Run one consent check if the two records disagreed on opt-in/opt-out before you stitched.

## See also

* [Identity resolution](/guides/identity-resolution) — always-on deterministic rules and the probabilistic review queue the stitch never bypasses.
* [Merge duplicate contacts end-to-end](/guides/contact-merge) — the dashboard-runbook for the same cascade `/cdp/identity-stitch` applies.
* [Embedded SDK API reference](/api-reference/sdk) — the full `/sdk/identify` envelope.
* [Opt-out lists](/guides/opt-out-lists) — where a merged record's suppressed channels surface.
