> ## 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 an anonymous session into a known contact

> Declare that a tracked-anonymous session (an SDK `anonymous_id`: a visitor who arrived on your web / messaging widget, then popped up elsewhere as a bare session id) IS a known person you can name (your own `external_id`, your system's stable key). Both sides are resolved against live contact rows. If both resolve to the same person the call is a no-op; if only the anonymous side resolves, its contact is re-keyed to your id (`linked`); if both resolve as distinct people, the anonymous session's contact is folded into the known one with the standard merge cascade across messaging threads, voice calls, video participants, CDP events, and consent (`merged`). Phone- or email-shaped `known_id` values are refused with a 200 `{ outcome: "refused" }` — route those through the ingest `identify` with an explicit trait so a mistyped number can never collapse one person's record into another's. Idempotent: re-POSTing the same pair after a stitch returns `noop`. Every outcome writes one consolidated `cdp.identity_stitched` audit entry naming both ids. Requires the `contacts:write` scope, owner / admin / developer role. Rate-limited to 10 calls per minute per tenant.



## OpenAPI

````yaml /openapi.yaml post /api/v1/cdp/identity-stitch
openapi: 3.1.0
info:
  title: Devotel CPaaS API
  description: Orbit by Devotel — Communications Platform as a Service API
  version: 1.0.0
  contact:
    name: Devotel
    url: https://devotel.io
    email: support@devotel.io
  license:
    name: Proprietary
servers:
  - url: https://api.orbit.devotel.io
    description: Production
security:
  - Bearer: []
  - ApiKey: []
tags:
  - name: Messages
    description: >-
      Send and manage messages across all channels (SMS, WhatsApp, RCS, Email,
      Viber, etc.)
  - name: Agents
    description: AI agent creation, configuration, and execution
  - name: Voice
    description: Voice calls, IVR, conferencing, and SIP trunking
  - name: Webhooks
    description: Webhook endpoint management and delivery logs
  - name: Numbers
    description: Phone number search, provisioning, and configuration
  - name: Contacts
    description: Contact management, segmentation, and lifecycle tracking
  - name: Campaigns
    description: Marketing campaign orchestration and analytics
  - name: Flows
    description: Automation flow builder and execution engine
  - name: Templates
    description: Message template management and approval workflows
  - name: Settings
    description: Organization, channel, and user preference settings
  - name: Verify
    description: OTP generation and verification across channels
  - name: Push
    description: Push notification delivery via FCM and APNs
  - name: Telegram
    description: >-
      Telegram bring-your-own-bot channel: connect a bot, verify a chat against
      the shared sandbox bot, and read unified channel state. Production sends
      go through the Messaging API.
  - name: USSD
    description: >-
      Menu-driven USSD for feature phones: define a menu tree, simulate session
      steps, and host the tenant-scoped aggregator callback.
  - name: Integrations
    description: Third-party service connections and OAuth management
  - name: Files
    description: >-
      Server-to-server media upload, listing, retrieval, and deletion
      (signed-URL backed)
  - name: Messaging Services
    description: >-
      Twilio MessagingService-parity containers bundling sender pool, opt-out
      list, inbound webhook, and sticky-sender/geomatch flags
  - name: Sender Pools
    description: >-
      Group sending numbers into pools with a selection strategy (sticky /
      round-robin / random) for outbound sends
  - name: Opt-Out Lists
    description: >-
      Per-list STOP / HELP / START keyword sets and auto-response copy (Twilio
      Advanced Opt-Out parity)
  - name: SMPP
    description: Tenant BYO-SMPP bind credentials and upstream termination carriers
  - name: Commerce
    description: >-
      Unified omnichannel conversational-commerce: cart/checkout state machine,
      cross-channel payment reconciliation, hosted pay-by-link, native WhatsApp
      checkout, and agentic-checkout payment mandates
  - name: Orby
    description: >-
      In-dashboard Orby operator assistant: streamed assistant turns,
      conversation threads, product knowledge-base search, and the tool-action
      approval gate. Available to signed-in operators only (dashboard session
      auth — API-key requests are rejected).
paths:
  /api/v1/cdp/identity-stitch:
    post:
      tags:
        - CDP
      summary: Stitch an anonymous session into a known contact
      description: >-
        Declare that a tracked-anonymous session (an SDK `anonymous_id`: a
        visitor who arrived on your web / messaging widget, then popped up
        elsewhere as a bare session id) IS a known person you can name (your own
        `external_id`, your system's stable key). Both sides are resolved
        against live contact rows. If both resolve to the same person the call
        is a no-op; if only the anonymous side resolves, its contact is re-keyed
        to your id (`linked`); if both resolve as distinct people, the anonymous
        session's contact is folded into the known one with the standard merge
        cascade across messaging threads, voice calls, video participants, CDP
        events, and consent (`merged`). Phone- or email-shaped `known_id` values
        are refused with a 200 `{ outcome: "refused" }` — route those through
        the ingest `identify` with an explicit trait so a mistyped number can
        never collapse one person's record into another's. Idempotent:
        re-POSTing the same pair after a stitch returns `noop`. Every outcome
        writes one consolidated `cdp.identity_stitched` audit entry naming both
        ids. Requires the `contacts:write` scope, owner / admin / developer
        role. Rate-limited to 10 calls per minute per tenant.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: The anonymous session id and the known identity to bind it to.
              properties:
                anonymous_id:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: >-
                    The SDK `anonymous_id` — the anonymous handle from your web
                    SDK (e.g. `anon_01h2x8…`).
                known_id:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: >-
                    Your known identity for the person — your own stable
                    `external_id` (e.g. `crm-44882`). Phone- or email-shaped
                    values are refused; route those through `identify`.
        description: The anonymous session id and the known identity to bind it to.
      responses:
        '200':
          description: >-
            The stitch outcome. `outcome` is one of `merged` (both sides
            resolved as distinct contacts and the anonymous one was folded in),
            `linked` (only the anonymous side resolved; its contact was
            re-keyed), `noop` (already stitched / nothing to resolve — see
            `reason`), `refused` (phone/email-shaped known_id), or `error`
            (retry-safe — re-POST the same pair).
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: >-
                  The stitch outcome. `outcome` is one of `merged` (both sides
                  resolved as distinct contacts and the anonymous one was folded
                  in), `linked` (only the anonymous side resolved; its contact
                  was re-keyed), `noop` (already stitched / nothing to resolve —
                  see `reason`), `refused` (phone/email-shaped known_id), or
                  `error` (retry-safe — re-POST the same pair).
                properties:
                  data:
                    type: object
                    additionalProperties: true
                    properties:
                      outcome:
                        type: string
                        enum:
                          - merged
                          - linked
                          - noop
                          - refused
                          - error
                      contact_id:
                        type: string
                        nullable: true
                        description: The surviving contact id.
                      anonymous_contact_id:
                        type: string
                        nullable: true
                        description: >-
                          The anonymous-side contact id (on `merged`, now
                          soft-deleted).
                      known_contact_id:
                        type: string
                        nullable: true
                        description: The known-side contact id when it resolved.
                      reason:
                        type: string
                        description: >-
                          Machine label explaining a `noop` / `refused` /
                          `error`.
                  meta:
                    type: object
                    properties:
                      request_id:
                        type: string
                        description: >-
                          Unique request identifier (also returned in
                          X-Request-Id header)
                      timestamp:
                        type: string
                        format: date-time
                        description: ISO 8601 UTC timestamp of the response
                example:
                  data:
                    outcome: merged
                    contact_id: cnt_01J8KQTFXX
                    anonymous_contact_id: cnt_01J8KQXGYY
                    known_contact_id: cnt_01J8KQTFXX
                  meta:
                    request_id: req_cdp_stitch
                    timestamp: '2026-08-28T12:00:00.000Z'
        '400':
          $ref: '#/components/responses/StandardError'
        '404':
          $ref: '#/components/responses/StandardError'
        '422':
          $ref: '#/components/responses/StandardError'
        '429':
          $ref: '#/components/responses/RateLimitedResponse'
        '500':
          $ref: '#/components/responses/StandardError'
      security: []
components:
  responses:
    StandardError:
      description: >-
        Standard error envelope. `error.code` is machine-readable; see the
        [error reference](https://docs.orbit.devotel.io/reference/error-codes)
        for the catalogue.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                required:
                  - code
                  - message
                  - status
                properties:
                  code:
                    type: string
                    description: Machine-readable error code (e.g. INVALID_PHONE_NUMBER)
                  message:
                    type: string
                    description: Human-readable error description
                  status:
                    type: integer
                    description: HTTP status code
                  details:
                    type: object
                    additionalProperties: true
                    description: Additional context about the error
              meta:
                type: object
                properties:
                  request_id:
                    type: string
                  timestamp:
                    type: string
                    format: date-time
                  docs_url:
                    type: string
                    format: uri
                    description: Link to relevant error documentation
    RateLimitedResponse:
      description: >-
        Rate limit exceeded. Wait `error.retry_after` seconds (or read the
        `Retry-After` header) before retrying. Returned when the request would
        exceed the bucket identified by `X-RateLimit-Bucket`.
      headers:
        X-RateLimit-Limit:
          description: Total request quota for the current window.
          schema:
            type: integer
            minimum: 0
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
            minimum: 0
        X-RateLimit-Reset:
          description: >-
            Absolute unix-epoch-seconds timestamp at which the current window
            resets.
          schema:
            type: integer
            minimum: 0
        X-RateLimit-Bucket:
          description: >-
            Name of the rate-limit bucket the request was bound by (e.g.
            `auth-write`, `money`, `agent-invoke`, or `custom:<max>/<window>`).
            Stripe-style — lets clients see which named cap they hit.
          schema:
            type: string
        RateLimit-Limit:
          description: >-
            draft-ietf-httpapi-ratelimit-headers no-prefix mirror of
            `X-RateLimit-Limit`. Some SDKs read only this form.
          schema:
            type: integer
            minimum: 0
        RateLimit-Remaining:
          description: >-
            draft-ietf-httpapi-ratelimit-headers no-prefix mirror of
            `X-RateLimit-Remaining`.
          schema:
            type: integer
            minimum: 0
        RateLimit-Reset:
          description: >-
            draft-ietf-httpapi-ratelimit-headers no-prefix mirror — delta
            seconds from now until the window resets (NOT epoch).
          schema:
            type: integer
            minimum: 0
        Retry-After:
          description: >-
            RFC 7231 §7.1.3 — number of seconds the client should wait before
            retrying.
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                required:
                  - code
                  - message
                  - status
                  - retry_after
                properties:
                  code:
                    type: string
                    enum:
                      - RATE_LIMITED
                    description: >-
                      Always `RATE_LIMITED` — the global request-limiter 429.
                      Distinct from `RATE_LIMIT_EXCEEDED`, the live
                      per-recipient/per-tenant/per-resource frequency-cap 429
                      (not retired).
                  message:
                    type: string
                  status:
                    type: integer
                    enum:
                      - 429
                  retry_after:
                    type: integer
                    minimum: 1
                    description: >-
                      Seconds to wait before retrying. Mirrors `Retry-After`
                      header.
              meta:
                type: object
                properties:
                  request_id:
                    type: string
                  timestamp:
                    type: string
                    format: date-time
                  docs_url:
                    type: string
                    format: uri
  securitySchemes:
    Bearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Dashboard JWT token from Clerk
    ApiKey:
      type: apiKey
      name: X-API-Key
      in: header
      description: Server-to-server API key (dv_live_sk_*)

````