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

# Identify

> Link an anonymous user to a known user, or update a known user's state.



## OpenAPI

````yaml POST /identify
openapi: 3.1.0
info:
  title: Curious API
  version: 1.0.0
  summary: The Curious data plane — decide, declare, events, identify.
  description: |
    The Curious data plane: `decide` to serve content for a user, `declare` to
    configure an element without serving anyone, `events` to report outcomes,
    and `identify` to link anonymous and known users.

    **Base URL.** `https://api.trycurious.ai/v1`. Every path below is relative
    to it.

    **Authentication.** Every call carries `Authorization: Bearer <key>`. Keys
    are `curious_sk_…` (secret) or `curious_pk_…` (publishable), each in a
    `live` or `test` environment (`curious_sk_test_…`). Secret keys may do
    everything. Publishable keys are serve-only: they may call `decide` without
    declaration fields and `events`, are refused by `declare` and `identify`,
    and must come from an origin on the key's allowlist.

    **Errors.** Every non-2xx response, and every error item inside a batch
    200, is JSON with `error` (a stable code: `invalid_request`,
    `invalid_json`, `unauthorized`, `forbidden`, `not_found`,
    `method_not_allowed`, `conflict`, `idempotency_error`, `lock_timeout`,
    `rate_limit_exceeded`, `internal_error`) and `message` (the human-readable
    reason, the text this document quotes). Every response carries an
    `X-Request-Id` header to quote to support; internal faults repeat it in
    the body as `request_id`. Unknown request fields are rejected
    with a 400 on `decide`, `declare` and `identify`; `events` ignores them.

    **Idempotency.** `events` dedupes on a caller-supplied `id`. `decide`
    replays a committed decision for a repeated `idempotency_key` with the same
    request, and answers 409 `idempotency_error` for the same key with a
    different request.
  contact:
    name: Curious
    url: https://trycurious.ai
servers:
  - url: https://api.trycurious.ai/v1
    description: >-
      Production. The hostname is the durable abstraction; `/v1` is the
      published contract.
security:
  - apiKey: []
tags:
  - name: decide
    description: Serve content for a user and record the decision.
  - name: declare
    description: Configure an element without serving a user.
  - name: events
    description: Ingest outcome events.
  - name: identify
    description: Link anonymous and known users.
paths:
  /identify:
    post:
      tags:
        - identify
      summary: Link an anonymous user to a known user
      description: |
        Declares that `anonymous_id` and `user_id` are the same person and
        merges their state; prior decisions and events on either id attribute
        to the merged user. `identifiers` and `attributes` are the same two
        buckets as decide: the caller says which keys are identity and which
        are state, and nothing is routed by key name. Without `anonymous_id`
        the call only updates the user's state. Secret keys only. There is
        no batch form.
      operationId: identify
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentifyRequest'
            examples:
              link:
                value:
                  anonymous_id: anon_456
                  user_id: user_123
                  identifiers:
                    email: alice@example.com
                    name: Alice
                  attributes:
                    plan: pro
              profile_only:
                summary: Update a known user's state without a link
                value:
                  user_id: user_123
                  attributes:
                    plan: enterprise
            x-rejected-examples:
              unknown_field:
                message: 'Received unknown parameter: traits'
                value:
                  user_id: user_123
                  traits:
                    plan: pro
              missing_user:
                message: user_id is required
                value:
                  anonymous_id: anon_456
      responses:
        '200':
          description: The link (or state update) was applied.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentifyResponse'
              examples:
                success:
                  value:
                    status: success
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: The declared identity conflicts with an existing user.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                conflict:
                  value:
                    error: conflict
                    message: Identity conflicts with an existing user
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        default:
          $ref: '#/components/responses/GatewayError'
components:
  schemas:
    IdentifyRequest:
      type: object
      required:
        - user_id
      properties:
        user_id:
          type: string
          minLength: 1
          pattern: \S
          description: The known user.
        anonymous_id:
          type: string
          minLength: 1
          pattern: \S
          description: The anonymous user to link into `user_id`.
        identifiers:
          $ref: '#/components/schemas/JsonObject'
          description: >-
            Durable identity (email, name, phone); merged into the user. Never a
            model feature.
        attributes:
          $ref: '#/components/schemas/JsonObject'
          description: >-
            Durable user state (plan, company_size); merged into the user and
            used for personalization.
      additionalProperties: false
    IdentifyResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          const: success
      additionalProperties: false
    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
          description: Human-readable reason; the text this document quotes.
        request_id:
          type: string
          format: uuid
          description: Present on internal faults only.
      additionalProperties: false
    JsonObject:
      type: object
      description: An open JSON object. Values may be any JSON.
    ErrorCode:
      type: string
      description: Stable, machine-readable; an SDK maps these to exception classes.
      enum:
        - invalid_request
        - invalid_json
        - unauthorized
        - forbidden
        - not_found
        - method_not_allowed
        - conflict
        - idempotency_error
        - lock_timeout
        - rate_limit_exceeded
        - internal_error
    RateLimited:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          const: rate_limit_exceeded
        message:
          type: string
        request_id:
          type: string
          format: uuid
          description: >-
            Present when the 429 came from the API gateway rather than the
            service (the id is in the header either way).
      additionalProperties: false
    ErrorWithRequestId:
      type: object
      required:
        - error
        - message
        - request_id
      properties:
        error:
          type: string
          const: internal_error
        message:
          type: string
        request_id:
          type: string
          format: uuid
          description: Correlation handle into the server-side logs; not stored.
      additionalProperties: false
  headers:
    RequestId:
      description: |
        The server-minted id of this request, on every response. Quote it to
        support; on an internal fault it equals the body's `request_id`.
        Browsers can read it (`Access-Control-Expose-Headers`).
      schema:
        type: string
        format: uuid
  responses:
    BadRequest:
      description: |
        The request was rejected before anything was written. `error` is
        `invalid_request` (or `invalid_json` for an unparseable body) and
        `message` the human-readable reason, the same string the examples
        quote.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unknown_field:
              value:
                error: invalid_request
                message: >-
                  Unknown field(s): segment. Allowed: element, user_id, control,
                  arms, rewards, constraints, requirements, instructions,
                  identifiers, attributes, context, idempotency_key,
                  element_type, format, variables, parameters, options
            invalid_json:
              value:
                error: invalid_json
                message: Invalid JSON body
    Unauthorized:
      description: Missing, malformed, unknown or revoked API key.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                error: unauthorized
                message: Missing or invalid API key
            revoked:
              value:
                error: unauthorized
                message: API key has been revoked
    Forbidden:
      description: |
        A publishable key from an origin not on its allowlist, or a
        publishable key on an endpoint that requires a secret key.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            origin:
              value:
                error: forbidden
                message: Origin not allowed for this key
            secret_required:
              value:
                error: forbidden
                message: Declare requires a secret key
    TooManyRequests:
      description: |
        The organization exceeded its per-endpoint request rate for the
        current minute. `Retry-After` says how many seconds remain in it.
      headers:
        Retry-After:
          description: Seconds until the current minute ends.
          schema:
            type: integer
            minimum: 1
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimited'
          examples:
            limited:
              value:
                error: rate_limit_exceeded
                message: >-
                  Request rate limit exceeded for this organization; retry after
                  the current minute ends.
    InternalError:
      description: |
        A sanitized internal fault. Quote `request_id` to support; it
        correlates to the server-side log and error capture. A 500 after a
        keyed decide may mean the decision committed and the response was
        lost — retry with the same `idempotency_key` to find out.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorWithRequestId'
          examples:
            fault:
              value:
                error: internal_error
                message: Internal error
                request_id: 2f7e9c1a-5b3d-4e8f-9a0b-1c2d3e4f5a6b
    BadGateway:
      description: |
        The API edge could not complete the call to the origin (a network
        fault between the two, or a connection dropped before the response
        arrived). The outcome is ambiguous: the request may or may not have
        been applied. Retry only with the same `idempotency_key` (decide) or
        `id` (events), which make a retry safe either way; an unkeyed retry
        can duplicate the write. Quote `request_id` to support.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorWithRequestId'
          examples:
            unreachable:
              value:
                error: internal_error
                message: >-
                  The API could not be reached, and the request may or may not
                  have been applied; retry only with the same idempotency_key
                  (decide) or id (events)
                request_id: 7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f
    GatewayError:
      description: |
        Any other status is a response no service function produced — the
        API edge or its gateway answered — rewritten into this same envelope
        at the gateway's status (`error` keyed by status: `not_found` for
        404, `rate_limit_exceeded` for 429, `internal_error` for 5xx). A 4xx
        means the gateway refused the request before the service (a 404 for
        a missing deployment, its own 429, which keeps `Retry-After`); nothing
        was written. A 5xx is ambiguous — a gateway 502 or 504 can follow a
        service call that committed and whose response was lost — so, as
        with the 502 above, retry only with the same `idempotency_key`
        (decide) or `id` (events). A redirect from the gateway is never
        forwarded; it arrives as the 502 above with the message `The API
        gateway answered with a redirect, which the data plane never issues;
        nothing was written`.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          description: On a 429 only; seconds to wait.
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            outage:
              value:
                error: internal_error
                message: >-
                  The API could not be reached, and the request may or may not
                  have been applied; retry only with the same idempotency_key
                  (decide) or id (events)
                request_id: 3b2a1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        A Curious API key: `curious_sk_…` (secret) or `curious_pk_…`
        (publishable), `live` or `test` environment (`curious_sk_test_…`).
        Publishable keys are serve-only and origin-gated; `declare` and
        `identify` require a secret key. A revoked or unknown key is a 401.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.