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

# Events

> Report outcomes, singly or in batches of up to 200.



## OpenAPI

````yaml POST /events
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:
  /events:
    post:
      tags:
        - events
      summary: Ingest an outcome event
      description: >
        Records that `user_id` did `name` (past tense: `purchased`, `clicked`,

        `signed_up`). `value` defaults to 1; `properties` describe the event,

        never the user; `timestamp` is the event's time and defaults to receipt.

        A caller-supplied `id` makes the event idempotent: a retry that finds

        its original reports `duplicate: true` and is still a success. Unknown

        fields are ignored. `created_at` is system-owned and rejected.


        **Batch form.** Send `events` (1–200 items); the envelope's `user_id`,

        `context` and `timestamp` apply to items that omit them. Results come
        back

        per item in input order inside a 200.
      operationId: events
      parameters:
        - name: X-Curious-Client
          in: header
          required: false
          description: |
            The Curious SDK's self-identification, `browser-js/<semver>` for the
            browser SDK. Optional for customer events. Required — together with
            a publishable key — for the system events `$pageview`, `$click` and
            `$form_submit`: without it, or under a secret key, those names are
            rejected as reserved.
          schema:
            type: string
            maxLength: 64
            pattern: >-
              ^(?:browser-js|sdk-js)/[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}(?:-[0-9A-Za-z.-]{1,32})?$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              anyOf:
                - $ref: '#/components/schemas/EventRequest'
                - $ref: '#/components/schemas/EventsBatchRequest'
            examples:
              purchase:
                summary: A purchase with its amount and properties
                value:
                  user_id: user_123
                  name: purchased
                  value: 49.99
                  properties:
                    plan: pro
                  timestamp: '2026-04-30T22:33:42Z'
              keyed:
                summary: An idempotent event — retries dedupe on `id`
                value:
                  id: evt_8f1c2a
                  user_id: user_123
                  name: clicked
                  context:
                    page: /pricing
              batch:
                summary: Batch form with envelope defaults
                value:
                  user_id: user_123
                  timestamp: '2026-04-30T22:33:42Z'
                  events:
                    - name: viewed
                      properties:
                        page: /pricing
                    - name: clicked
                      id: evt_9a2b3c
                    - user_id: user_456
                      name: signed_up
              batch_with_ignored_fields:
                summary: >-
                  Batch form — unknown envelope fields are ignored, and `events`
                  alone selects it
                value:
                  user_id: user_123
                  name: ignored
                  source: sdk
                  events:
                    - name: clicked
            x-rejected-examples:
              reserved_name:
                message: '`$`-prefixed names are reserved for Curious SDK events'
                value:
                  user_id: user_123
                  name: $pageview
              retired_as_of:
                message: >-
                  as_of was renamed to timestamp (2026-10-15); send the event's
                  time as timestamp
                value:
                  user_id: user_123
                  name: purchased
                  as_of: '2026-04-30T22:33:42Z'
              system_owned:
                message: >-
                  created_at is system-owned (row write time); send the event's
                  time as timestamp
                value:
                  user_id: user_123
                  name: purchased
                  created_at: '2026-04-30T22:33:42Z'
              bad_timestamp:
                message: >-
                  timestamp must be an ISO-8601 datetime with an explicit
                  timezone
                value:
                  user_id: user_123
                  name: purchased
                  timestamp: 2026-04-30 22:33
              timestamp_not_a_date:
                message: >-
                  timestamp must be an ISO-8601 datetime with an explicit
                  timezone
                value:
                  user_id: user_123
                  name: purchased
                  timestamp: tomorrow
      responses:
        '200':
          description: Success (singular form), or the per-item results (batch form).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/EventResponse'
                  - $ref: '#/components/schemas/EventsBatchResponse'
              examples:
                success:
                  value:
                    status: success
                duplicate:
                  summary: The retry found its original
                  value:
                    status: success
                    duplicate: true
                batch:
                  value:
                    results:
                      - index: 0
                        status: ok
                      - index: 1
                        id: evt_9a2b3c
                        status: ok
                        duplicate: true
                      - index: 2
                        status: error
                        error: invalid_request
                        message: user_id and name are required
                    duplicates: 1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/BadGateway'
        default:
          $ref: '#/components/responses/GatewayError'
components:
  schemas:
    EventRequest:
      type: object
      description: >-
        Unknown fields are ignored, except `created_at` (system-owned) and the
        retired `as_of`, which are a 400. A body with `events` is the batch
        form, whatever else it carries.
      required:
        - user_id
        - name
      additionalProperties: true
      allOf:
        - $ref: '#/components/schemas/SystemEventConstraints'
      properties:
        events:
          description: Not on a singular event; its presence selects the batch form.
          not: {}
        as_of:
          description: >-
            The wire name until 2026-10-15; rejected rather than ignored,
            because ignoring it would mean receipt time.
          not: {}
        id:
          $ref: '#/components/schemas/EventId'
        user_id:
          type: string
          minLength: 1
          pattern: \S
        name:
          $ref: '#/components/schemas/EventName'
        value:
          $ref: '#/components/schemas/EventValue'
        properties:
          $ref: '#/components/schemas/JsonObject'
          description: Facts about the event, never about the user.
        context:
          $ref: '#/components/schemas/JsonObject'
          description: Situational facts at event time.
        timestamp:
          $ref: '#/components/schemas/EventTimestamp'
        created_at:
          $ref: '#/components/schemas/SystemOwnedCreatedAt'
    EventsBatchRequest:
      type: object
      description: Unknown envelope fields are ignored.
      required:
        - events
      additionalProperties: true
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 200
          items:
            $ref: '#/components/schemas/EventsBatchItem'
        user_id:
          type: string
          minLength: 1
          pattern: \S
          description: Default for items that omit it.
        context:
          $ref: '#/components/schemas/JsonObject'
          description: Default for items that omit it.
        timestamp:
          $ref: '#/components/schemas/EventTimestamp'
          description: Default for items that omit it.
        as_of:
          description: >-
            The retired name of the envelope default; a 400, never silently
            receipt time.
          not: {}
    EventResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          const: success
        duplicate:
          type: boolean
          const: true
          description: Present when an event with this `id` already existed.
      additionalProperties: false
    EventsBatchResponse:
      type: object
      required:
        - results
        - duplicates
      properties:
        results:
          type: array
          description: One result per item, in input order.
          items:
            oneOf:
              - $ref: '#/components/schemas/EventsBatchItemOk'
              - $ref: '#/components/schemas/EventsBatchItemError'
        duplicates:
          type: integer
          minimum: 0
      additionalProperties: false
    SystemEventConstraints:
      description: |
        What the endpoint enforces on a system event (docs/events.md, Browser
        provenance): allowlisted string properties only — `$pageview` takes
        `path`, a normalized path of at most 2048 characters; `$click` and
        `$form_submit` take `element_name`, at most 256 characters — and no
        `context` (the browser SDK sends none). Applied to the singular request
        and to every batch item; a customer event is unconstrained here.
      allOf:
        - if:
            required:
              - name
            properties:
              name:
                const: $pageview
          then:
            properties:
              properties:
                type: object
                additionalProperties: false
                properties:
                  path:
                    type: string
                    maxLength: 2048
                    pattern: ^/(?!/)[^\s?#\x00-\x1f\x7f\u0080-\u009f\uD800-\uDFFF]*$
              context:
                not: {}
        - if:
            required:
              - name
            properties:
              name:
                enum:
                  - $click
                  - $form_submit
          then:
            properties:
              properties:
                type: object
                additionalProperties: false
                properties:
                  element_name:
                    type: string
                    maxLength: 256
                    pattern: ^[^\u0000\uD800-\uDFFF]*$
              context:
                not: {}
    EventId:
      type: string
      minLength: 1
      description: >-
        The caller's id for this event; makes it idempotent per organization and
        environment.
    EventName:
      type: string
      minLength: 1
      pattern: ^(?:\s*[^\s$]|\$(?:pageview|click|form_submit)$)
      description: |
        Past tense (`purchased`, `clicked`). Names starting with `$` are
        reserved for Curious SDK system events: a customer integration sending
        one gets a 400 (rename at the source, e.g. `$pageview` → `pageview`).
        The only `$` names the schema admits are the browser SDK's own —
        `$pageview`, `$click` and `$form_submit` — and the endpoint accepts
        them only under a publishable key with the SDK's client header
        (`SystemEventConstraints` bounds their properties).
    EventValue:
      type:
        - number
        - 'null'
      default: 1
      description: Finite. Null and absent both mean 1.
    JsonObject:
      type: object
      description: An open JSON object. Values may be any JSON.
    EventTimestamp:
      type: string
      pattern: >-
        ^\d{4}-\d{2}-\d{2}(?:[T
        ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|z|[+-]\d{2}(?::?\d{2})?))?$
      description: |
        When the event happened: an ISO-8601 datetime with an explicit
        timezone (`2026-04-30T22:33:42Z`, `2026-04-30 22:33:42+00:00`,
        `+00` and `+0000` offsets included) or a date, read as UTC midnight. A
        naive datetime is rejected. Must be a real calendar date and at most
        5 minutes in the future. Defaults to receipt.
    SystemOwnedCreatedAt:
      description: >-
        `created_at` is the row's write time and system-owned; sending it is a
        400. Send the event's time as `timestamp`.
      not: {}
    EventsBatchItem:
      type: object
      description: >
        A singular event; `user_id`, `context` and `timestamp` may come from the

        envelope. Unknown fields are ignored. This schema describes a valid

        item, the shape a client should send; the endpoint itself accepts any

        JSON per item and reports an item that fails validation (a missing

        `name`, a bad `timestamp`, a non-numeric `value`) inside the 200
        envelope,

        never as a 400 for the request.
      additionalProperties: true
      allOf:
        - $ref: '#/components/schemas/SystemEventConstraints'
      properties:
        id:
          $ref: '#/components/schemas/EventId'
        user_id:
          type: string
          minLength: 1
          pattern: \S
        name:
          $ref: '#/components/schemas/EventName'
        value:
          $ref: '#/components/schemas/EventValue'
        properties:
          $ref: '#/components/schemas/JsonObject'
        context:
          $ref: '#/components/schemas/JsonObject'
        timestamp:
          $ref: '#/components/schemas/EventTimestamp'
        as_of:
          description: Retired wire name; an item-level error, never silently receipt time.
          not: {}
        created_at:
          $ref: '#/components/schemas/SystemOwnedCreatedAt'
    EventsBatchItemOk:
      type: object
      required:
        - index
        - status
      properties:
        index:
          type: integer
          minimum: 0
        id:
          type: string
          description: Echoed when the item supplied one.
        status:
          type: string
          const: ok
        duplicate:
          type: boolean
          const: true
          description: >-
            Present when an event with this `id` already existed, or an earlier
            item in the batch carried the same `id`.
      additionalProperties: false
    EventsBatchItemError:
      type: object
      required:
        - index
        - status
        - error
        - message
      properties:
        index:
          type: integer
          minimum: 0
        id:
          type: string
          description: Echoed when the item supplied a non-empty one.
        status:
          type: string
          const: error
        error:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
        request_id:
          type: string
          format: uuid
          description: Present on internal faults only.
      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
    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
  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.