> ## 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.

# Declare

> Configure an element without serving a user.



## OpenAPI

````yaml POST /declare
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:
  /declare:
    post:
      tags:
        - declare
      summary: Configure an element without serving a user
      description: |
        The same declaration fields, validators, storage and lifecycle as a
        secret-key `decide` call — creation, supersession, in-place updates,
        declared arms — with no user upsert, no arm selection and no decision.
        Serving fields (`user_id`, `identifiers`, `attributes`, `context`,
        `idempotency_key`) are a 400 here. Secret keys only; a publishable key
        is a 403. There is no batch form.
      operationId: declare
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeclareRequest'
            examples:
              declare:
                summary: Declare an element's experiment ahead of traffic
                value:
                  element: upgrade_prompt
                  element_type: paywall
                  control:
                    content:
                      headline: Upgrade today
                      body: Unlock unlimited templates with Pro.
                  rewards:
                    - name: purchased
                      properties:
                        plan: pro
                  format:
                    headline:
                      type: text
                      max_characters: 90
                    body:
                      type: text
                  options:
                    approval: manual
                    holdout: 0.05
            x-rejected-examples:
              serving_field:
                message: >-
                  Declare configures an element without a user: user_id belong
                  on decide, not here
                value:
                  element: upgrade_prompt
                  user_id: user_123
                  control:
                    content: Upgrade today
              missing_name:
                message: element is required
                value:
                  control:
                    content: Upgrade today
      responses:
        '200':
          description: >-
            The element and its live experiment after the declaration was
            applied.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeclareResponse'
              examples:
                declared:
                  value:
                    element_id: 5c4d3e2f-0000-4000-8000-00000000f001
                    experiment_id: 1e2d3c4b-0000-4000-8000-00000000e001
                    experiment_status: initializing
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        default:
          $ref: '#/components/responses/GatewayError'
components:
  schemas:
    DeclareRequest:
      type: object
      required:
        - element
      properties:
        element:
          type: string
          minLength: 1
          pattern: \S
        control:
          $ref: '#/components/schemas/Control'
        arms:
          $ref: '#/components/schemas/Arms'
        rewards:
          $ref: '#/components/schemas/Rewards'
        constraints:
          $ref: '#/components/schemas/Constraints'
        requirements:
          $ref: '#/components/schemas/Requirements'
        instructions:
          type: string
        element_type:
          $ref: '#/components/schemas/ElementType'
        format:
          $ref: '#/components/schemas/Format'
        variables:
          $ref: '#/components/schemas/Variables'
        parameters:
          $ref: '#/components/schemas/Parameters'
        options:
          $ref: '#/components/schemas/Options'
      additionalProperties: false
    DeclareResponse:
      type: object
      required:
        - element_id
        - experiment_id
        - experiment_status
      properties:
        element_id:
          $ref: '#/components/schemas/Uuid'
        experiment_id:
          $ref: '#/components/schemas/Uuid'
        experiment_status:
          $ref: '#/components/schemas/ExperimentStatus'
        warnings:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/Warning'
      additionalProperties: false
    Control:
      $ref: '#/components/schemas/ContentOrWrapper'
      description: |
        The counterfactual baseline: the content you would serve without
        Curious. Singular, never an array; required whenever the call creates
        an experiment. A changed control supersedes the experiment.
    Arms:
      description: |
        Additional starting points beside the control: one entry or a list,
        each bare content or `{content, hypothesis}`. Additive and idempotent:
        an entry the experiment already has is skipped. Entries that duplicate
        an earlier entry in the same request are skipped with an `arm_invalid`
        warning, and so are entries that violate the format or the
        requirements, or that would exceed the 50-active-arm cap. Two rules are
        request errors (400): an entry whose content equals the control's, and
        more than 10 distinct entries after duplicates are dropped — the limit
        counts distinct contents, so the list itself is not capped here.
      oneOf:
        - $ref: '#/components/schemas/ContentOrWrapper'
        - type: array
          items:
            $ref: '#/components/schemas/ContentOrWrapper'
    Rewards:
      description: |
        The rewards the experiment optimizes. Shorthand: a name, an object, or
        a list of either; a bare name means a binary reward with weight 1 and
        a one-day window.
      oneOf:
        - type: string
          minLength: 1
        - $ref: '#/components/schemas/Reward'
        - type: array
          items:
            oneOf:
              - type: string
                minLength: 1
              - $ref: '#/components/schemas/Reward'
    Constraints:
      type: array
      description: |
        Guardrails an arm may not breach. Each `(name, type, bound,
        properties)` identity may appear once; `threshold`, `threshold_type`
        and `window_seconds` are its adjustable fields. Changing constraints
        never supersedes the experiment.
      items:
        $ref: '#/components/schemas/Constraint'
    Requirements:
      type: object
      description: >-
        Phrase rules every arm must satisfy. Changing them supersedes the
        experiment.
      properties:
        forbidden_phrases:
          type: array
          items:
            type: string
        required_phrases:
          type: array
          items:
            type: string
      additionalProperties: false
    ElementType:
      type:
        - string
        - 'null'
      enum:
        - push_notification
        - email_subject
        - email_preview
        - email_body
        - email
        - button_cta
        - hero
        - landing_page
        - blog_post
        - paywall
        - null
      description: The element's kind. Case-insensitive on input. Null means unset.
    Format:
      $ref: '#/components/schemas/FormatFieldSpec'
      description: |
        The structural content contract: a single field spec, or an object of
        named field specs. Omitted on a first call, it is inferred from the
        control; declared, the control must match it. A changed format
        supersedes the experiment.
    Variables:
      type: array
      description: |
        Names that arm content may reference as `{{name|fallback}}`,
        substituted from the user's identifiers and attributes at decision
        time. Defaults, on creation, to the identifier and attribute keys sent.
        Updates in place.
      items:
        type: string
        pattern: ^\w+$
        not:
          const: __proto__
    Parameters:
      type: object
      description: |
        Element-content facts fixed for the experiment and reproduced verbatim
        through `{{placeholders}}`. Keys match `^\w+$`; the namespace is
        disjoint from `variables`. Any change supersedes the experiment.
      propertyNames:
        pattern: ^\w+$
        not:
          const: __proto__
      additionalProperties:
        description: >-
          Any JSON value. Only non-null scalars are substituted into
          placeholders; structured values are visible to generation.
    Options:
      type: object
      description: |
        Serving-policy knobs. `assignment`, `approval` and
        `minimum_effect_size` update in place; `holdout` is fixed for the
        experiment's life and a differing value supersedes it. Every default
        is resolved when the experiment is created and stored with it, so a
        later change to a default never alters a live experiment; an omitted
        key on a later call carries the stored value forward.
      properties:
        assignment:
          type: string
          enum:
            - static
            - dynamic
          default: static
          description: >-
            `static` (the default): the same user keeps the same arm while it
            stays active. `dynamic`: every call is chosen afresh, for per-send
            channels such as push or email.
        approval:
          type: string
          enum:
            - auto
            - manual
          description: >-
            Whether generated arms serve immediately or wait for operator
            review. Defaults to the predecessor's value, then the
            organization's, then `manual`.
        holdout:
          type: number
          minimum: 0.02
          maximum: 0.2
          default: 0.05
          description: Share of users held out as the control cohort.
        minimum_effect_size:
          type: number
          minimum: 0.05
          maximum: 1
          default: 0.2
          description: |
            The smallest relative difference from the control that matters to
            you. Two arms whose contrast is confidently inside this share of the
            control's composite count as practically equivalent, and one of them
            is retired. Mutable in place; never widened by the platform.
      additionalProperties: false
    Uuid:
      type: string
      format: uuid
    ExperimentStatus:
      type: string
      enum:
        - initializing
        - exploring
        - concluded
      description: >-
        `initializing` until the first model is minted, then `exploring`;
        `concluded` is terminal.
    Warning:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - arm_invalid
            - serving_control_only
        message:
          type: string
      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
    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
    ContentOrWrapper:
      description: >-
        Bare content, or `{content, hypothesis}`. An object with a `content` key
        is the wrapper.
      oneOf:
        - $ref: '#/components/schemas/ContentWrapper'
        - $ref: '#/components/schemas/BareContent'
    Reward:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          description: Past-tense event name (`purchased`, `clicked`).
        type:
          type: string
          enum:
            - binary
            - count
            - semicontinuous
            - continuous
          default: binary
          description: The statistical shape of the reward; it selects the model.
        weight:
          type: number
          default: 1
          not:
            const: 0
          description: Finite and non-zero. Negative for cost-like attribution.
        window_seconds:
          type: integer
          minimum: 1
          default: 86400
          description: Attribution window after the decision.
        properties:
          $ref: '#/components/schemas/JsonObject'
          description: Only events whose properties match attribute to this reward.
        value:
          type: string
          pattern: ^(event\.value|event\.properties\..+|arm\.[^.\[]+)$
          description: |
            Value source, `semicontinuous` rewards only: `event.value`
            (default), `event.properties.<key>`, or `arm.<field>` — the served
            arm's top-level content field (no nested or array paths) supplies
            the amount, and the format must bound that field as a number with
            `min` and `max`.
        baseline_rate:
          type:
            - number
            - 'null'
          description: |
            Optional prior information, never identity: the rate the control
            currently achieves, in the reward's own units. Binary and
            semicontinuous rewards declare an occurrence probability strictly
            between 0 and 1; count rewards a strictly positive expected count;
            continuous rewards any finite value.
      additionalProperties: false
      allOf:
        - if:
            required:
              - value
            properties:
              value: {}
          then:
            required:
              - type
            properties:
              type:
                const: semicontinuous
        - dependentSchemas:
            baseline_rate:
              anyOf:
                - required:
                    - type
                  properties:
                    type:
                      enum:
                        - count
                        - continuous
                - properties:
                    baseline_rate:
                      exclusiveMinimum: 0
                      exclusiveMaximum: 1
        - dependentSchemas:
            baseline_rate:
              anyOf:
                - not:
                    required:
                      - type
                    properties:
                      type: {}
                - properties:
                    type:
                      not:
                        const: count
                - properties:
                    baseline_rate:
                      exclusiveMinimum: 0
    Constraint:
      type: object
      required:
        - name
        - threshold_type
        - threshold
        - bound
      properties:
        name:
          type: string
          minLength: 1
          description: Past-tense event name.
        type:
          type: string
          enum:
            - binary
            - continuous
          default: binary
        threshold_type:
          type: string
          enum:
            - relative
            - absolute
        threshold:
          type: number
          description: >-
            Finite. For a binary constraint with an absolute threshold, a rate
            in [0, 1].
        bound:
          type: string
          enum:
            - upper
            - lower
        window_seconds:
          type: integer
          minimum: 1
          default: 86400
        properties:
          $ref: '#/components/schemas/JsonObject'
      additionalProperties: false
      if:
        required:
          - threshold_type
        properties:
          threshold_type:
            const: absolute
          type:
            const: binary
      then:
        properties:
          threshold:
            minimum: 0
            maximum: 1
    FormatFieldSpec:
      description: |
        A field spec is a typed spec (text, html, number, enum or array) or an
        object mapping field names to specs. At most 4 container levels.
      oneOf:
        - $ref: '#/components/schemas/TextFieldSpec'
        - $ref: '#/components/schemas/NumberFieldSpec'
        - $ref: '#/components/schemas/EnumFieldSpec'
        - $ref: '#/components/schemas/ArrayFieldSpec'
        - $ref: '#/components/schemas/ObjectOfFieldSpecs'
    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
    ContentWrapper:
      type: object
      description: Content with an optional hypothesis about why it will perform.
      required:
        - content
      properties:
        content:
          $ref: '#/components/schemas/Content'
        hypothesis:
          type:
            - string
            - 'null'
          description: What this content is testing. Blank is stored as null.
      additionalProperties: false
    BareContent:
      description: |
        Content passed without the wrapper. An object with a `content` key is
        always read as the wrapper, so bare object content never has one.
      allOf:
        - $ref: '#/components/schemas/Content'
        - not:
            type: object
            required:
              - content
            properties:
              content: {}
    JsonObject:
      type: object
      description: An open JSON object. Values may be any JSON.
    TextFieldSpec:
      type: object
      description: >-
        A text or HTML field, optionally bounded in characters (`min_characters`
        ≤ `max_characters`).
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - text
            - html
        min_characters:
          type: integer
          minimum: 1
        max_characters:
          type: integer
          minimum: 1
      additionalProperties: false
    NumberFieldSpec:
      type: object
      description: >-
        A numeric field, optionally bounded (`min` ≤ `max`). Bounds are enforced
        on content.
      required:
        - type
      properties:
        type:
          type: string
          const: number
        min:
          type: number
        max:
          type: number
      additionalProperties: false
    EnumFieldSpec:
      type: object
      description: A field whose value is one of `values`. Enum values are strings.
      required:
        - type
        - values
      properties:
        type:
          type: string
          const: enum
        values:
          type: array
          minItems: 1
          items:
            type: string
      additionalProperties: false
    ArrayFieldSpec:
      type: object
      description: >-
        A homogeneous array of `items`. Defaults to 20 items; `max_items` may be
        declared up to 100.
      required:
        - type
        - items
      properties:
        type:
          type: string
          const: array
        items:
          $ref: '#/components/schemas/FormatFieldSpec'
        min_items:
          type: integer
          minimum: 0
        max_items:
          type: integer
          minimum: 1
          maximum: 100
      additionalProperties: false
    ObjectOfFieldSpecs:
      type: object
      description: >-
        Named fields, each a spec. Never a field named `__proto__`, and never
        one named `type` (that key makes the object a typed spec).
      minProperties: 1
      propertyNames:
        not:
          enum:
            - type
            - __proto__
      additionalProperties:
        $ref: '#/components/schemas/FormatFieldSpec'
    Content:
      description: |
        Arm content: whatever the caller's renderer displays — a string, a
        number, or a non-empty object of fields. Never an array or a boolean at
        the top level; nested values follow `ContentValue`.
      oneOf:
        - type: string
        - type: number
        - type: object
          minProperties: 1
          propertyNames:
            not:
              enum:
                - type
                - __proto__
          additionalProperties:
            $ref: '#/components/schemas/ContentValue'
    ContentValue:
      description: |
        A content value at any depth: a string, a finite number, an object of
        values, or an array of values. Never a boolean or null, and never a
        field named `type` or `__proto__` — the format grammar cannot represent
        them. Two rules stay in prose because JSON Schema cannot state them:
        arrays are homogeneous, and nesting stops at 4 container levels.
      oneOf:
        - type: string
        - type: number
        - type: object
          minProperties: 1
          propertyNames:
            not:
              enum:
                - type
                - __proto__
          additionalProperties:
            $ref: '#/components/schemas/ContentValue'
        - type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/ContentValue'
  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
    Conflict:
      description: The experiment is at its active-arm limit.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            arm_limit:
              value:
                error: conflict
                message: >-
                  Experiment already has 50 active arms. Deactivate an active
                  arm before activating another.
    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.