openapi: 3.1.0

info:
  title: maxclicks Public API
  version: 1.1.0
  summary: The official REST API for the maxclicks email, CRM, and automation platform.
  description: >-
    The maxclicks Public API connects customer data to prepared audiences, emails, and journeys.
    Version 1.1.0 is served under /v1. Schemas, attributes, template content, broadcast content,
    domains, senders, topics, and keys are authored in the app. This specification documents 58
    functional operations; PATCH/DELETE /events/{id} are immutable guards that always return 405.


    Authenticated operations use a Bearer API key and the owner's space permissions. Results use
    data, optionally warnings and pagination. Standard failures use error.type/code/message and
    optional issues. Event batch abort is a 422 data result, and an unresolved idempotency
    persistence failure may omit error.type.


    Seventeen operations support Idempotency-Key, as declared by their header parameter. Keep the
    same calling key, method, path, and body on retries. Recorded results include errors and webhook
    secrets; completed records are retained for 24 hours, while unresolved operations need
    reconciliation. Event acceptance instead uses eventId, and event identity changes use
    operationId plus a per-alias revision.


    HTTP acceptance does not imply finished downstream work. Events return a durable receipt and
    have a separate readiness endpoint; record deletion returns a pollable operation status;
    workflow triggers create a run; broadcast sends schedule work. Existing SDK/CLI/MCP sources wrap
    an earlier contract, so use this specification or direct HTTP for newer capabilities.


    API request limits are read=100/second, write=25/second, ai=10/minute per key. Event admission
    also has shared occurrence/backlog/receipt caps. Retry-After can be a delay in seconds or an
    HTTP date. Body size is limited to 10 MB; normalized event input is limited to 256 KiB. Offset
    traversal is capped at 10,000; ready events use cursors over a maximum 90-day query window.
  contact:
    name: maxclicks
    url: https://maxclicks.ai
    email: info@maxclicks.ai
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

externalDocs:
  description: Developer documentation
  url: https://docs.maxclicks.ai

servers:
  - url: https://api.maxclicks.ai/v1
    description: Production
  - url: https://api-stage.maxclicks.ai/v1
    description: Staging

security:
  - bearerAuth: []

tags:
  - name: Meta
    description: Identify the calling API key.
  - name: Schemas
    description: Manage the space's schemas (contact, object, event).
  - name: Attributes
    description: Read a schema's attributes. Attributes are authored in the app.
  - name: Records
    description: Keep stored contact and object data current, inspect consent history, and track
      deletion to completion.
  - name: Suppressions
    description: Manage the email do-not-contact list. Requires an admin key.
  - name: Events
    description: Accept durable business events, inspect readiness, and bind anonymous source identities
      to contacts.
  - name: Domains
    description: Read sending domains and their DNS verification status. Domains are added and verified
      in the app.
  - name: Senders
    description: Read saved sender profiles configured in the app.
  - name: Topics
    description: Read communication topics configured in the app.
  - name: Segments
    description: Generate and manage AI-defined contact filters.
  - name: Broadcasts
    description: Schedule, send, and measure mass emails. Broadcasts are authored in the app.
  - name: Templates
    description: Send stored message templates. Templates are authored in the app.
  - name: Webhooks
    description: Manage outgoing webhooks.
  - name: Workflows
    description: Trigger published journeys, pause execution, and inspect runs and paged step history.
  - name: Forms
    description: Public form submission and double opt-in confirmation.
  - name: Emails
    description: Public email recipient actions.

paths:
  /me:
    get:
      tags: [Meta]
      summary: Identify the calling API key
      description: >
        Returns the key metadata, its owning user, the bound space (if any), and the caller's role.
        Works for space-unbound keys, in which case `space` and `role` are null.
      operationId: meta.me
      responses:
        '200':
          description: The identity of the calling key.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Me' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /schemas:
    get:
      tags: [Schemas]
      summary: List schemas
      description: Lists the space's schemas, optionally filtered by type.
      operationId: schemas.list
      parameters:
        - name: type
          in: query
          description: Filter by one or more schema types. Repeat the parameter to pass several.
          required: false
          style: form
          explode: true
          schema:
            type: array
            items: { $ref: '#/components/schemas/SchemaType' }
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of schemas.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Schema' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /schemas/{schema}:
    parameters:
      - { $ref: '#/components/parameters/SchemaParam' }
    get:
      tags: [Schemas]
      summary: Get a schema
      description: Fetches a single schema by id or slug.
      operationId: schemas.get
      responses:
        '200':
          description: The schema.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Schema' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /schemas/{schema}/attributes:
    parameters:
      - { $ref: '#/components/parameters/SchemaParam' }
    get:
      tags: [Attributes]
      summary: List attributes
      description: Lists base attributes first, then custom attributes. Base ids use base:<key> and have
        null sequence and timestamps. definition.type uses the documented stored/evaluated types;
        aggregate is definition.valueSource.type, not a top-level type. Only definitions with type
        other than evaluated and no valueSource are writable. Definition metadata can include
        auto-fill settings, aggregate source configuration, or evaluated target/reference
        descriptions; generated expressions are excluded. Record GET/list does not expand evaluated
        or aggregate values. Attribute authoring is available in the app, not through public v1.
      operationId: attributes.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of attributes.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Attribute' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /schemas/{schema}/records:
    parameters:
      - { $ref: '#/components/parameters/SchemaParam' }
    get:
      tags: [Records]
      summary: List records
      description: Lists stored contact or object records, ordered by createdAt ascending then id
        ascending. Custom values are flattened at the top level alongside platform fields. Evaluated
        and aggregate attributes are not expanded. This endpoint accepts limit/offset only; it has
        no ad-hoc search, time filter, include, or trait-expansion query. Traversal is capped;
        pagination.hasMore can be false when totalCount exceeds the reachable window.
      operationId: records.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of records.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Record' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      tags: [Records]
      summary: Create a record
      description: >
        Strictly creates one contact or object record (never updates an existing one). Any `id` in
        the body is ignored. `source` is forced to `api`. If the schema has an AI auto-fill
        attribute, credits are checked first. Supports `Idempotency-Key`. Errors:
        `schema_not_found`, `identifier_conflict` (409), `insufficient_credits` (402).
      operationId: records.create
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecordInput' }
            example:
              email: sarah@example.com
              firstName: Sarah
      responses:
        '201':
          description: The created record.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecordEnvelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"

  /schemas/{schema}/records/upsert:
    parameters:
      - { $ref: '#/components/parameters/SchemaParam' }
    post:
      tags: [Records]
      summary: Upsert a record
      description: >-
        Creates or updates a contact or object. An explicit id selects that exact record or returns
        record_not_found. Otherwise contact matching uses userId first; email can match only a
        contact without userId; phone can match only a contact without userId or email. Objects
        match by externalId. Per-field identifier collisions may be skipped and reported in
        warnings; inspect the returned record. Returns 201 for creation or 200 for update. Any
        writable AI auto-fill attribute on the schema triggers the up-front credit check. Supports
        Idempotency-Key. Required writable fields without a value/default can fail with
        required_attribute_missing (400, issues names the key).
      operationId: records.upsert
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecordInput' }
            example:
              userId: customer_1042
              email: sarah@example.com
              firstName: Sarah
      responses:
        '200':
          description: The updated record.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecordEnvelope' }
        '201':
          description: The created record.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecordEnvelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"

  /schemas/{schema}/records/{id}:
    parameters:
      - { $ref: '#/components/parameters/SchemaParam' }
      - { $ref: '#/components/parameters/RecordIdParam' }
    get:
      tags: [Records]
      summary: Get a record
      operationId: records.get
      responses:
        '200':
          description: The record.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecordEnvelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
      description: Reads one contact or object in this schema. Stored custom values and platform fields
        are flattened; evaluated and aggregate attributes are not expanded. There is no
        include/expand query. Missing schema or record returns 404.
    patch:
      tags: [Records]
      summary: Update a record
      description: >-
        Partially updates a record. The path id overrides any body id. Explicit null clears a
        nullable field; omit a field to preserve it. Non-writable evaluated/aggregate keys are not
        accepted as stored inputs. Normalization and skipped per-field identity updates can produce
        warnings. The endpoint does not use Idempotency-Key. Errors: schema_not_found,
        record_not_found, identifier_conflict, or required_attribute_missing (400, issues names the
        missing key).
      operationId: records.update
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecordInput' }
            example:
              firstName: Sarah
      responses:
        '200':
          description: The updated record.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RecordEnvelope' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    delete:
      tags: [Records]
      summary: Delete a record
      operationId: records.delete
      responses:
        '200':
          description: Deletion operation status; pending is not completed erasure.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/RecordDeletionStatus"
                  warnings:
                    $ref: "#/components/schemas/Warnings"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
      description: Requests durable deletion of a contact or object. The response is an
        operationId/status/message object, not {id,deleted}. The workspace is closed for conflicting
        work while data and external resources are removed. Poll GET /record-deletions/{operationId}
        until status=completed; blocked requires reconciliation of unresolved work. The current
        handler returns HTTP 200 even when status=pending, so do not use HTTP status alone as proof
        of erasure. An unknown record returns 404 record_not_found. This endpoint does not use
        Idempotency-Key.

  /schemas/{schema}/records/{id}/audit-trail:
    parameters:
      - { $ref: '#/components/parameters/SchemaParam' }
      - { $ref: '#/components/parameters/RecordIdParam' }
    get:
      tags: [Records]
      summary: List a contact's audit trail
      description: >
        Lists the consent and communication audit trail for a contact, newest first. Contact schemas
        only. Errors: `schema_not_found`, `record_not_found`.
      operationId: records.auditTrail
      parameters:
        - name: channel
          in: query
          required: false
          description: Filter by communication channel.
          schema: { $ref: '#/components/schemas/CommunicationChannel' }
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of audit-trail entries.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/AuditTrailEntry' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /contacts/suppressions:
    get:
      tags: [Suppressions]
      summary: List suppressions
      description: >
        Lists suppressions for the caller's space, newest first. Includes global suppressions (not
        space-scoped) alongside space-scoped ones, since both block sends to the caller's contacts.
        Requires an admin key.
      operationId: suppressions.list
      parameters:
        - name: identifier
          in: query
          required: false
          description: Filter to the suppression(s) for one email address.
          schema: { type: string, format: email }
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of suppressions.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Suppression' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      tags: [Suppressions]
      summary: Create a suppression
      description: >
        Suppresses an email identifier for the space. Requires an admin key. Not Idempotency-Key
        aware. Error: `suppression_already_exists` (409; `manual` and `legal` are distinct reasons).
      operationId: suppressions.create
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SuppressionInput' }
      responses:
        '201':
          description: The created suppression.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Suppression' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /contacts/suppressions/batch:
    post:
      tags: [Suppressions]
      summary: Batch-create suppressions
      description: >
        Bulk-creates 1 to 100 suppressions in one call. Each item is validated and processed
        independently: an existing suppression for the same identifier and reason returns `exists`
        rather than failing the item. Requires an admin key. Supports `Idempotency-Key`. Always
        responds `200`.
      operationId: suppressions.batchCreate
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SuppressionBatchCreateRequest' }
      responses:
        '200':
          description: Per-item results and a summary.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/SuppressionBatchCreateResult' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"

  /contacts/suppressions/batch-remove:
    post:
      tags: [Suppressions]
      summary: Batch-delete suppressions
      description: >
        Bulk-removes 1 to 100 suppressions by id in one call. Each id is gated exactly like the
        single delete (must belong to the caller's space; reason must be `manual` or `legal`).
        Requires an admin key. Supports `Idempotency-Key`. Always responds `200`.
      operationId: suppressions.batchDelete
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SuppressionBatchDeleteRequest' }
      responses:
        '200':
          description: Per-item results and a summary.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/SuppressionBatchDeleteResult' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"

  /contacts/suppressions/{id}:
    parameters:
      - { $ref: '#/components/parameters/SuppressionIdParam' }
    delete:
      tags: [Suppressions]
      summary: Delete a suppression
      description: >-
        Deletes a manual or legal suppression. Requires admin permission and supports
        Idempotency-Key. Missing suppression returns suppression_not_found; automatic provider
        bounce/complaint suppressions cannot be removed through this endpoint. Removing a
        suppression is separate from granting contact marketing consent.
      operationId: suppressions.delete
      responses:
        '200':
          description: The suppression was removed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeletedEnvelope' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /events:
    get:
      tags: [Events]
      summary: List events
      description: >-
        Lists only ready business events for one event schema. Results are ordered by occurredAt
        descending, then id descending. The time filter is inclusive from and exclusive to; defaults
        are the seven days before now. A range wider than 90 days is clamped with a warning. Uses
        cursor pagination: no offset and no totalCount. Pending, failed, and outcome_unknown
        acceptances are absent; use events.status to inspect them. Invalid times return 400
        invalid_time_range; malformed cursors return 400 invalid_cursor. Events are read from the
        operational event store, not the analytics archive.
      operationId: events.list
      parameters:
        - name: schema
          in: query
          required: true
          description: The event schema id or slug.
          schema: { type: string }
        - name: from
          in: query
          required: false
          description: Start of the time range (ISO 8601). Defaults to 7 days before `to`.
          schema: { type: string, format: date-time }
        - name: to
          in: query
          required: false
          description: End of the time range (ISO 8601). Defaults to now.
          schema: { type: string, format: date-time }
        - name: cursor
          in: query
          required: false
          description: Opaque cursor from a previous page's `pagination.nextCursor`.
          schema: { type: string }
        - name: limit
          in: query
          required: false
          description: Page size. Defaults to 50, capped at 200.
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
      responses:
        '200':
          description: A cursor page of events.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Event' }
                  pagination: { $ref: '#/components/schemas/CursorPagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /events/{schema}:
    x-note: >-
      PATCH and DELETE /events/{id} are immutable guards that always return 405 events_immutable.
      They are not mutation operations. There is no GET /events/{id}; use GET
      /events/{schema}/{id}/status for an acceptance receipt.
    post:
      tags: [Events]
      summary: Fire an event
      description: >-
        Durably accepts one business event and returns 202 with its immutable acceptance receipt.
        Missing AI auto-fill fields are prepared asynchronously; readinessAtAcceptance is pending or
        ready. An accepted event is visible to event lists and consumers only once ready. Supply a
        stable eventId and retry the same normalized payload and occurredAt to recover the original
        receipt with isDuplicate=true. Changed input under the same eventId returns 409
        event_id_conflict; erased occurrences return 409 event_erased. Idempotency-Key is not used.
        The normalized event input is limited to 256 KiB. occurredAt must be within the previous 366
        days or five minutes into the future. contactId and objectId are optional, mutually
        exclusive explicit subjects in this space. Event admission has separate rate, backlog, and
        receipt-storage limits in addition to the API write bucket.
      operationId: events.fire
      parameters:
        - name: schema
          in: path
          required: true
          description: The event schema id or slug.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EventInput' }
            example:
              eventId: order_1042_paid
              amount: 42
      responses:
        '202':
          description: Durable event acceptance receipt; inspect readiness separately.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/FireResult' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/ServerError"
  /events/{schema}/batch:
    parameters:
      - { $ref: '#/components/parameters/SchemaParam' }
    post:
      tags: [Events]
      summary: Fire a batch of events
      description: >-
        Accepts 1–500 events of one schema. With onError=continue (default), each event commits
        independently; inspect every result, including failures, even on HTTP 200. Accepted items
        include their immutable receipt. Admission failures include retryAfterMilliseconds. With
        onError=abort, a validation or admission failure rolls back new acceptances in that
        transaction, marks earlier accepted results rolled_back, and returns HTTP 422 with
        data.results, data.summary, and data.failedIndex. Later items are not attempted. Existing
        receipts encountered as duplicates remain existing occurrences. A 422 abort response is a
        business-result data envelope, not the standard error envelope. Each item uses eventId
        deduplication; Idempotency-Key is not used. Individual events retain the 256 KiB and
        occurredAt limits, and the whole HTTP body remains limited to 10 MB.
      operationId: events.fireBatch
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EventBatchRequest' }
            example:
              events:
                - eventId: order_1042_paid
                  amount: 42
              onError: continue
      responses:
        '200':
          description: Per-item results and a summary.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/BatchFireResult' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '422':
          description: Batch aborted; earlier new acceptances were rolled back. Inspect data.results and
            data.failedIndex.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/BatchFireResult' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        "500":
          $ref: "#/components/responses/ServerError"

  /domains:
    get:
      tags: [Domains]
      summary: List domains
      description: Items omit `records` (unlike `domains.get`); call `domains.get` for DNS records.
      operationId: domains.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of domains.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Domain' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /domains/{id}:
    parameters:
      - { $ref: '#/components/parameters/DomainIdParam' }
    get:
      tags: [Domains]
      summary: Get a domain
      description: "Includes live-checked `records`. Error: `domain_not_found`."
      operationId: domains.get
      responses:
        '200':
          description: The domain.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Domain' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /senders:
    get:
      tags: [Senders]
      summary: List senders
      operationId: senders.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of senders.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Sender' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /topics:
    get:
      tags: [Topics]
      summary: List topics
      operationId: topics.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of topics.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Topic' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /topics/{id}:
    parameters:
      - { $ref: '#/components/parameters/TopicIdParam' }
    get:
      tags: [Topics]
      summary: Get a topic
      description: "Error: `topic_not_found`."
      operationId: topics.get
      responses:
        '200':
          description: The topic.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Topic' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /segments:
    get:
      tags: [Segments]
      summary: List segments
      operationId: segments.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of segments.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Segment' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      tags: [Segments]
      summary: Create a segment
      description: >
        Generates a contact filter from a natural-language `requirements` prompt (every create is an
        AI call, in the `ai` rate bucket; credits are checked first) and persists it in one call.
        The response includes a live `matchingCount`. The generated expression is never returned.
        Errors: `schema_not_found`, `invalid_segment`, `segment_generation_failed` (422),
        `segment_name_conflict` (409), `insufficient_credits` (402).
      operationId: segments.create
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SegmentCreate' }
            example:
              name: Engaged customers
              contactSchema: customers
              requirements: Contacts who have opened an email in the last 30 days.
      responses:
        '201':
          description: The created segment.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Segment' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '422': { $ref: '#/components/responses/UnprocessableEntity' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /segments/{id}:
    parameters:
      - { $ref: '#/components/parameters/SegmentIdParam' }
    get:
      tags: [Segments]
      summary: Get a segment
      description: "Error: `segment_not_found`."
      operationId: segments.get
      responses:
        '200':
          description: The segment.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Segment' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    delete:
      tags: [Segments]
      summary: Delete a segment
      description: >
        Errors: `segment_not_found`, `segment_integration_owned` (403), `segment_in_use` (409, used
        as a broadcast audience, workflow trigger, or webhook condition, or referenced by other
        entities).
      operationId: segments.delete
      responses:
        '200':
          description: The segment was deleted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeletedEnvelope' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /segments/{id}/count:
    parameters:
      - { $ref: '#/components/parameters/SegmentIdParam' }
    get:
      tags: [Segments]
      summary: Count a segment's matching contacts
      description: "Error: `segment_not_found`."
      operationId: segments.count
      responses:
        '200':
          description: The live matching count and the schema's total contact count.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [count, total]
                    properties:
                      count: { type: integer, description: Contacts currently matching the segment. }
                      total: { type: integer, description: All contacts of the segment's schema. }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /segments/{id}/contacts:
    parameters:
      - { $ref: '#/components/parameters/SegmentIdParam' }
    get:
      tags: [Segments]
      summary: List a segment's matching contacts
      description: >
        Paginated contact records currently matching the segment. `limit` is additionally
        hard-capped at `config.segment.maximumContactsPageSize` (100) regardless of the requested
        value. Error: `segment_not_found`.
      operationId: segments.listContacts
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of matching contacts.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/ContactRecord' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /broadcasts:
    get:
      tags: [Broadcasts]
      summary: List broadcasts
      operationId: broadcasts.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of broadcasts.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Broadcast' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /broadcasts/{id}:
    parameters:
      - { $ref: '#/components/parameters/BroadcastIdParam' }
    get:
      tags: [Broadcasts]
      summary: Get a broadcast
      description: "Error: `broadcast_not_found`."
      operationId: broadcasts.get
      responses:
        '200':
          description: The broadcast.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Broadcast' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    patch:
      tags: [Broadcasts]
      summary: Update a broadcast
      description: >-
        Updates name, description, scheduledFor, and/or audience before sending starts. At least one
        field is required. Audience accepts all or an existing segment for the broadcast contact
        schema; custom filter generation and content authoring are not available here. Concurrent
        changes or a planning claim can return 409 broadcast_changed; a started send cannot be
        edited. Supports Idempotency-Key. Cancel/stop/resume/redrive controls are available through
        app workflows, not this public endpoint.
      operationId: broadcasts.update
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BroadcastUpdate' }
            example:
              name: September customer update
      responses:
        '200':
          description: The updated broadcast.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Broadcast' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
  /broadcasts/{id}/send:
    parameters:
      - { $ref: '#/components/parameters/BroadcastIdParam' }
    post:
      tags: [Broadcasts]
      summary: Send a broadcast
      description: >-
        Schedules a prepared broadcast immediately or for scheduledFor and returns 202. It does not
        wait for planning or delivery. Content is authored in the app. A pre-existing send schedule,
        started send, or active planning claim returns 409 broadcast_already_sending. Readiness
        issues return 422 broadcast_invalid with issues. Supports Idempotency-Key. Read runs to
        inspect recipients; HTTP acceptance is not proof every recipient received email.
      operationId: broadcasts.send
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BroadcastSendRequest' }
            example: {}
      responses:
        '202':
          description: The broadcast was accepted for sending.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Broadcast' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '422': { $ref: '#/components/responses/UnprocessableEntity' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"

  /broadcasts/{id}/runs:
    parameters:
      - { $ref: '#/components/parameters/BroadcastIdParam' }
    get:
      tags: [Broadcasts]
      summary: List a broadcast's runs
      description: "One reduced run record per recipient contact. Error: `broadcast_not_found`."
      operationId: broadcasts.listRuns
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of broadcast runs.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/BroadcastRun' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /broadcasts/{id}/metrics:
    parameters:
      - { $ref: '#/components/parameters/BroadcastIdParam' }
    get:
      tags: [Broadcasts]
      summary: Get a broadcast's aggregated email metrics
      description: >
        `email` is omitted entirely when the broadcast has no email content yet. Error:
        `broadcast_not_found`.
      operationId: broadcasts.getMetrics
      responses:
        '200':
          description: Aggregated send/open/click/bounce metrics.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/BroadcastMetricsResponse' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /templates:
    get:
      tags: [Templates]
      summary: List templates
      operationId: templates.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of templates.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Template' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /templates/{id}:
    parameters:
      - { $ref: '#/components/parameters/TemplateIdParam' }
    get:
      tags: [Templates]
      summary: Get a template
      description: "Error: `template_not_found`."
      operationId: templates.get
      responses:
        '200':
          description: The template.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Template' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
  /templates/{templateId}/send:
    parameters:
      - name: templateId
        in: path
        required: true
        description: The template id.
        schema: { type: string }
    post:
      tags: [Templates]
      summary: Send a template
      description: >-
        Renders stored template content and sends to the recipient resolved from data. Read
        expectedDataRepresentation and send every required key; referenced records are upserted
        before rendering and configured attribute expansions are applied. Available credits and
        email allowance are required. There is no subject/body replacement or preview execution
        parameter. Supports Idempotency-Key. Pre-send validation/eligibility failures use request
        errors (email_limit_reached, sending_disabled, template_not_sendable, domain_not_verified,
        invalid_data, marketing_requires_contact, recipient_not_subscribed, recipient_suppressed).
        The result exposes emailId, status, and error. status=sent means the provider accepted the
        durable send attempt, not inbox delivery. HTTP 200 status=failed collapses retryable
        refusals, terminal refusals, cancellation, and unknown provider outcomes into one public
        state with a generic error. It does not prove non-delivery or establish whether another send
        is safe. Preserve emailId and the original operation key and reconcile in the app; do not
        create a new key to force a second send. A stored template without an email channel can
        return null status/emailId/error.
      operationId: templates.send
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TemplateSendRequest' }
            example:
              data:
                contact:
                  email: sarah@example.com
      responses:
        '200':
          description: The send was processed.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/TemplateSendResponse' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '422': { $ref: '#/components/responses/UnprocessableEntity' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhooks
      operationId: webhooks.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of webhooks.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Webhook' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      tags: [Webhooks]
      summary: Create a webhook
      description: >-
        Creates an outgoing webhook and returns signatureVerificationSecret. Store the secret
        securely; normal GET/list responses omit it. Supports Idempotency-Key, including replay of
        the original secret-bearing response. A custom filter generates through AI from
        requirements, uses the ai rate bucket and credits, and exposes only its description. Schema
        accepts an id or slug and must match the trigger. Deliveries retry network failures, HTTP
        408, 429, and 5xx with bounded backoff; deduplicate their stable X-Webhook-ID. Delivery URL
        and secret are captured when enqueued, so queued deliveries can use earlier settings. Errors
        include invalid_webhook, schema_not_found, segment_not_found, and insufficient_credits.
      operationId: webhooks.create
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookCreate' }
            example:
              url: https://example.com/webhooks/maxclicks
              trigger: contact upserted
              schemaId: customers
              condition:
                type: none
      responses:
        '201':
          description: The created webhook, including its signing secret.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/WebhookWithSecret' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '422': { $ref: '#/components/responses/UnprocessableEntity' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /webhooks/{id}:
    parameters:
      - { $ref: '#/components/parameters/WebhookIdParam' }
    get:
      tags: [Webhooks]
      summary: Get a webhook
      operationId: webhooks.get
      responses:
        '200':
          description: The webhook.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Webhook' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    patch:
      tags: [Webhooks]
      summary: Update a webhook
      description: >-
        Updates url, emailEventTypes, condition, and/or status (live or paused); at least one is
        required. Trigger and schema cannot change. A custom filter uses AI and credits. Supports
        Idempotency-Key. Setting live clears the circuit-breaker count. Already enqueued deliveries
        keep their captured URL and signing secret. Queued deliveries observed while paused are
        suppressed rather than held for later replay. Integration-owned webhooks cannot be changed.
      operationId: webhooks.update
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookUpdate' }
            example:
              status: live
      responses:
        '200':
          description: The updated webhook.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Webhook' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '422': { $ref: '#/components/responses/UnprocessableEntity' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      description: "Deletes a webhook. Supports Idempotency-Key. Missing webhooks return
        webhook_not_found; integration-owned webhooks cannot be deleted."
      operationId: webhooks.delete
      responses:
        '200':
          description: The webhook was deleted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeletedEnvelope' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /webhooks/{id}/rotate-secret:
    parameters:
      - { $ref: '#/components/parameters/WebhookIdParam' }
    post:
      tags: [Webhooks]
      summary: Rotate a webhook secret
      description: >-
        Creates a new signing secret and returns it with the webhook id and updatedAt. Store it
        securely. Supports Idempotency-Key: retry the same key to recover the original
        secret-bearing response instead of rotating again. Normal reads never reveal the secret.
        Already enqueued deliveries retain their captured secret and URL, including retries; keep
        the previous verification secret until those deliveries drain. Integration-owned webhooks
        cannot be rotated.
      operationId: webhooks.rotateSecret
      responses:
        '200':
          description: The new signing secret.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/RotatedWebhookSecret' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /workflows:
    get:
      tags: [Workflows]
      summary: List workflows
      operationId: workflows.list
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of workflows.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Workflow' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /workflows/{id}:
    x-note: >
      This path also serves `POST /workflows/{workflowStepReference}` (workflows.trigger),
      registered on the router as a catch-all single-segment POST beneath the more specific routes
      (list/get/pause/unpause/delete/runs). OpenAPI models both under one templated path item,
      matching the `/events/{schema}` merge above.
    get:
      tags: [Workflows]
      summary: Get a workflow
      description: "Error: `workflow_not_found`."
      operationId: workflows.get
      parameters:
        - name: id
          in: path
          required: true
          description: The workflow id.
          schema: { type: string }
      responses:
        '200':
          description: The workflow.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Workflow' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
    post:
      tags: [Workflows]
      summary: Trigger a workflow
      description: >-
        Starts a run at a published, unpaused incoming-webhook step. The path id is that step’s
        reference id, not the workflow id. Send the workflow’s configured JSON input as the body.
        HTTP 200 with data={} confirms run creation, not workflow completion. Errors use the
        standard JSON error envelope: workflow_step_not_found (404), invalid_request_body (400), or
        a server error for an invalid configured schema. Supports Idempotency-Key. Workflow
        authoring, run cancellation, and retry/redrive are not exposed by this public endpoint.
      operationId: workflows.trigger
      parameters:
        - name: id
          in: path
          required: true
          description: The incoming-webhook step's reference id.
          schema:
            type: string
        - $ref: "#/components/parameters/IdempotencyKeyParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Per-workflow-step payload, validated against the step's JSON schema.
              additionalProperties: true
      responses:
        '200':
          description: The workflow run was started.
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                description: "`data` is an empty object."
                properties:
                  data: { type: object }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400':
          $ref: "#/components/responses/BadRequest"
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          $ref: "#/components/responses/NotFound"
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500':
          $ref: "#/components/responses/ServerError"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"

  /workflows/{id}/pause:
    parameters:
      - { $ref: '#/components/parameters/WorkflowIdParam' }
    post:
      tags: [Workflows]
      summary: Pause a workflow
      description: >-
        Pauses the workflow: new triggers and further execution of existing runs are blocked. An
        external effect already in flight may complete. Supports Idempotency-Key. Error:
        workflow_not_found.
      operationId: workflows.pause
      responses:
        '200':
          description: The paused workflow.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Workflow' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /workflows/{id}/unpause:
    parameters:
      - { $ref: '#/components/parameters/WorkflowIdParam' }
    post:
      tags: [Workflows]
      summary: Unpause a workflow
      description: "Unpauses the workflow so eligible paused work can continue. This does not publish
        unpublished edits or redrive terminal failed runs. Supports Idempotency-Key. Error:
        workflow_not_found."
      operationId: workflows.unpause
      responses:
        '200':
          description: The unpaused workflow.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Workflow' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
          headers:
            Idempotent-Replay:
              $ref: "#/components/headers/IdempotentReplay"
            Operation-Id:
              schema:
                type: string
              description: Operation identifier for reconciliation.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          description: The operation outcome could not be confirmed. Preserve Operation-Id and the original
            key/body; reconcile before a new operation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdempotencyUnknownError"
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyParam"

  /workflows/{id}/runs:
    parameters:
      - { $ref: '#/components/parameters/WorkflowIdParam' }
    get:
      tags: [Workflows]
      summary: List a workflow's runs
      description: "Lists runs newest first. Each run embeds at most its latest 100 history entries,
        ascending within that window. Use workflows.getRun with nextHistoryBeforeIndex to read
        earlier history. Error: workflow_not_found."
      operationId: workflows.listRuns
      parameters:
        - { $ref: '#/components/parameters/LimitParam' }
        - { $ref: '#/components/parameters/OffsetParam' }
      responses:
        '200':
          description: A page of workflow runs.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WorkflowRun' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /workflows/{id}/runs/{runId}:
    parameters:
      - { $ref: '#/components/parameters/WorkflowIdParam' }
      - name: runId
        in: path
        required: true
        description: The workflow run id.
        schema: { type: string }
    get:
      tags: [Workflows]
      summary: Get a workflow run
      description: "Returns public run status and a window of at most 100 step-history records, ordered by
        index ascending within the returned window. The initial window contains the latest records.
        To read older history, pass nextHistoryBeforeIndex as history_before until it is null.
        status is running while a current step exists; otherwise failed if the run has a failed
        step, or completed. failureReason comes from the returned window and can be null even for a
        failed run whose failure is older. No internal step graph, handling state, or input/output
        data is returned. Errors: workflow_not_found, workflow_run_not_found."
      operationId: workflows.getRun
      responses:
        '200':
          description: The workflow run.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/WorkflowRun' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }
        "400":
          $ref: "#/components/responses/BadRequest"
      parameters:
        - name: history_before
          in: query
          required: false
          description: Return step-history indices strictly below this non-negative safe integer. Use the
            previous nextHistoryBeforeIndex; omit for the latest 100 entries.
          schema:
            type: integer
            minimum: 0
            maximum: 9007199254740991

  /forms/{formId}/submit:
    parameters:
      - { $ref: '#/components/parameters/FormIdParam' }
    post:
      tags: [Forms]
      summary: Submit a form
      description: >
        Public, unauthenticated form submission (the tenant is derived from the form, not an API
        key). Validates required fields, CAPTCHA, and consent, then upserts the contact or (for
        double opt-in, when the visitor opted in to email) stores a pending record and emails a
        confirmation link. A declined-consent submission always goes through the direct path even on
        a double opt-in form. Rate limits: per IP 10/60s and per form 120/60s (both fail open if
        Redis is down), plus a separate per-recipient/per-form cap on confirmation emails. Error:
        `rate_limit_exceeded`.
      operationId: forms.submit
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FormSubmission' }
            example:
              contact:
                email: sarah@example.com
                firstName: Sarah
              emailMarketingConsent: false
      responses:
        '200':
          description: The submission was accepted.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/FormSubmissionResult' }
                  warnings: { $ref: '#/components/schemas/Warnings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/ServerError' }

  /forms/{formId}/confirm/{token}:
    parameters:
      - { $ref: '#/components/parameters/FormIdParam' }
      - name: token
        in: path
        required: true
        description: The single-use double opt-in token from the confirmation email.
        schema: { type: string }
    get:
      tags: [Forms]
      summary: Confirm a double opt-in
      description: >
        Public browser navigation target for the double opt-in email link. Redeems the token,
        upserts the confirmed contact, and always redirects (302) to the hosted result page. Every
        failure path (missing/expired token, unpublished form) also redirects to a "failed" landing
        page rather than returning a JSON error. Same rate limits as `forms.submit`.
      operationId: forms.confirmDoubleOptIn
      security: []
      responses:
        '302':
          description: Redirect to the hosted confirmation result page.
          headers:
            Location:
              description: The result page URL.
              schema: { type: string, format: uri }

  /emails/{emailId}/unsubscribe:
    parameters:
      - name: emailId
        in: path
        required: true
        description: The id of the sent email.
        schema: { type: string }
    post:
      tags: [Emails]
      summary: One-click unsubscribe
      description: >
        Public RFC 8058 one-click unsubscribe target. Unsubscribes the linked contact from a topic
        or the whole email channel, then redirects (302) to the hosted result page. No rate limit at
        all on this endpoint.
      operationId: emails.unsubscribeOneClick
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [List-Unsubscribe]
              properties:
                List-Unsubscribe:
                  type: string
                  const: One-Click
      responses:
        '302':
          description: Redirect to the hosted unsubscribe result page.
          headers:
            Location:
              description: The result page URL.
              schema: { type: string, format: uri }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
  /events/{schema}/{id}/status:
    get:
      tags:
        - Events
      summary: Get event readiness
      operationId: events.status
      description: Read the current readiness of an accepted event by its maxclicks event id, including
        pending, ready, failed, and outcome_unknown states. This is distinct from the immutable
        readinessAtAcceptance returned when firing or retrying the event. data contains raw
        acceptance values until preparation is ready. A missing event or event schema returns 404.
        Acceptance and readiness do not prove that a downstream workflow, webhook, or email
        completed.
      parameters:
        - name: schema
          in: path
          required: true
          description: Event schema id or slug.
          schema:
            type: string
            example: purchase-completed
        - name: id
          in: path
          required: true
          description: The maxclicks event id from the acceptance receipt, not your eventId.
          schema:
            type: string
            example: ev_BcwDvBUeSaSDILA5tHgpmU7I
      responses:
        "200":
          description: Current event readiness and accepted/prepared data.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/EventStatus"
                  warnings:
                    $ref: "#/components/schemas/Warnings"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/ServerError"
  /event-identities:
    post:
      tags:
        - Events
      summary: Bind an anonymous event identity
      operationId: eventIdentities.identify
      description: Binds or unbinds an anonymous browser identity to a contact using a server-held API key
        with write permission. An enabled contact track definition must match sourceId and
        subjectSchemaId in the space. Send contactId=null to remove the binding. All six body fields
        are required and unknown fields are rejected. Use a UUID operationId for replay of the same
        input; a changed body under that operationId returns 409. expectedAliasRevision is the last
        revision of this specific alias as a decimal string, or "0" for a new alias. Keep the
        returned identityRevision for the next change to that alias. Revision conflicts and erased
        aliases return 409; an unavailable source returns 403. Capacity is bounded at 100,000
        aliases and 1,000,000 changes per space; exhausted capacity returns 429 without promising
        automatic recovery. Idempotency-Key is not used.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventIdentityInput"
      responses:
        "200":
          description: The committed identity revision.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/EventIdentityResult"
                  warnings:
                    $ref: "#/components/schemas/Warnings"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/ServerError"
  /record-deletions/{operationId}:
    get:
      tags:
        - Records
      summary: Get record deletion status
      operationId: records.getDeletion
      description: Poll a record-deletion operation in the caller’s space. pending means cleanup is still
        in progress; blocked means unresolved work needs reconciliation and the workspace remains
        closed; completed confirms the deletion operation finished. A missing or out-of-scope
        operation returns 404. Preserve the operationId from records.delete.
      parameters:
        - name: operationId
          in: path
          required: true
          description: The full operationId returned by records.delete.
          schema:
            type: string
            example: delete:records:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
      responses:
        "200":
          description: Current deletion status.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/RecordDeletionStatus"
                  warnings:
                    $ref: "#/components/schemas/Warnings"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/ServerError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        Your API key sent as a bearer token in the `Authorization` header: `Authorization: Bearer
        <api_key>`. The server also accepts the raw key without the `Bearer ` prefix, but clients
        should always send `Bearer`.

  headers:
    IdempotentReplay:
      description: >
        Present and `true` when this response was replayed verbatim from a prior request that used
        the same `Idempotency-Key`.
      schema: { type: boolean }

  parameters:
    LimitParam:
      name: limit
      in: query
      required: false
      description: Page size. Clamped to 1..200 with a warning if out of range.
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 200
    OffsetParam:
      name: offset
      in: query
      required: false
      description: >-
        Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400
        offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even
        if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export
        contract.
      schema:
        type: integer
        default: 0
        minimum: 0
        maximum: 10000
    IdempotencyKeyParam:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method,
        and resolved path, with a canonical request-body fingerprint. Replays recorded status/body
        with Idempotent-Replay=true, including recorded errors. Changed input returns 422
        idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work
        returns idempotency_outcome_unknown. Completed records expire after 24 hours; unresolved
        records remain for reconciliation.
      schema: { type: string, maxLength: 256 }
    SchemaParam:
      name: schema
      in: path
      required: true
      description: A schema id or slug.
      schema: { type: string }
    RecordIdParam:
      name: id
      in: path
      required: true
      description: The record's platform id.
      schema: { type: string }
    SuppressionIdParam:
      name: id
      in: path
      required: true
      description: The suppression id.
      schema: { type: string }
    DomainIdParam:
      name: id
      in: path
      required: true
      description: The domain id.
      schema: { type: string }
    TopicIdParam:
      name: id
      in: path
      required: true
      description: The topic id.
      schema: { type: string }
    SegmentIdParam:
      name: id
      in: path
      required: true
      description: The segment id.
      schema: { type: string }
    BroadcastIdParam:
      name: id
      in: path
      required: true
      description: The broadcast id.
      schema: { type: string }
    TemplateIdParam:
      name: id
      in: path
      required: true
      description: The template id.
      schema: { type: string }
    WebhookIdParam:
      name: id
      in: path
      required: true
      description: The webhook id.
      schema: { type: string }
    WorkflowIdParam:
      name: id
      in: path
      required: true
      description: The workflow id.
      schema: { type: string }
    FormIdParam:
      name: formId
      in: path
      required: true
      description: The form id.
      schema: { type: string }

  responses:
    BadRequest:
      description: >
        The request was malformed or failed validation. Also covers `malformed_json` (body is not
        valid JSON) and, on list endpoints, `offset_too_large`.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: The API key is missing or invalid.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: The key lacks permission, or the space is out of scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: The referenced resource was not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: The write conflicts with an existing resource or the resource's current state.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PaymentRequired:
      description: >
        Insufficient credits (`insufficient_credits`) for an AI-generation call or a schema with AI
        auto-fill, or the space's email allowance is exhausted (`email_limit_reached`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    UnprocessableEntity:
      description: >
        The request was well-formed but could not be processed, typically a failed AI-generation
        call or a readiness gate (which may carry `error.issues`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    TooManyRequests:
      description: The rate limit was exceeded.
      headers:
        Retry-After:
          description: Retry delay as HTTP-date or seconds when supplied. Event per-item admission failures
            report retryAfterMilliseconds inside the result instead.
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PayloadTooLarge:
      description: The request body exceeds the 10 MB limit.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    UnsupportedMediaType:
      description: The request used an unsupported content type, charset, or content encoding.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    IngestUnavailable:
      description: >
        A downstream event dependency is unavailable: the ingest pipeline (`ingest_unavailable`, on
        `events.fire`/`events.fireBatch`) or the ClickHouse-backed event archive
        (`event_archive_unavailable`, on `events.list`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ServerError:
      description: The API failed to process a valid request.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    ErrorObject:
      type: object
      required: [type, code, message]
      properties:
        type:
          type: string
          enum: [invalid_request_error, api_error]
        code:
          type: [string, 'null']
          description: A stable machine-readable code, or null.
        message:
          type: string
        issues:
          type: array
          description: >
            Present only for validation-gate failures that report more than one distinct problem at
            once (e.g. `broadcasts.send`'s `broadcast_invalid`).
          items: { type: string }

    Error:
      type: object
      required: [error]
      properties:
        error: { $ref: '#/components/schemas/ErrorObject' }

    Warnings:
      type: array
      description: Non-fatal warnings accumulated while processing the request.
      items: { type: string }
      example: []

    Pagination:
      type: object
      required: [limit, offset, totalCount, hasMore]
      properties:
        limit: { type: integer }
        offset: { type: integer }
        totalCount: { type: integer }
        hasMore:
          type: boolean
          description: True only when more rows exist AND offset + limit is below 10000. False can reflect the
            traversal cap even if totalCount is larger; it does not guarantee a full export.

    CursorPagination:
      type: object
      description: Cursor pagination envelope, used only by `events.list`.
      required: [limit, nextCursor, hasMore]
      properties:
        limit: { type: integer }
        nextCursor:
          type: [string, 'null']
          description: Opaque cursor for the next page, or null when there are no more rows.
        hasMore: { type: boolean }

    Deleted:
      type: object
      required: [id, deleted]
      properties:
        id: { type: string }
        deleted: { type: boolean, const: true }

    DeletedEnvelope:
      type: object
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/Deleted' }
        warnings: { $ref: '#/components/schemas/Warnings' }

    RecordEnvelope:
      type: object
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/Record' }
        warnings: { $ref: '#/components/schemas/Warnings' }

    SchemaType:
      type: string
      enum: [contact, object, event]

    ContactSource:
      type: string
      enum: [import, api, form, manual, workflow, integration]

    CommunicationChannel:
      type: string
      enum: [email]

    BadgeColor:
      type: string
      description: A display color for tags and topics.
      enum:
        [
          red,
          orange,
          amber,
          yellow,
          lime,
          green,
          emerald,
          teal,
          cyan,
          sky,
          blue,
          indigo,
          violet,
          purple,
          fuchsia,
          pink,
          rose,
          zinc
        ]

    SuppressionReason:
      type: string
      description: >
        All possible suppression reasons on read. Only `manual` and `legal` can be created or
        removed through the API; the rest are system-applied (bounce, complaint, unsubscribe) and
        read-only.
      enum: [manual, legal, unsubscribed, hard_bounce, soft_bounce_consecutive, spam_complaint]

    SuppressionCreateReason:
      type: string
      enum: [manual, legal]

    RoleName:
      type: string
      enum: [admin, non-paid admin, member]

    Me:
      type: object
      required: [apiKey, user, space, role]
      properties:
        apiKey:
          type: object
          required: [id, name, keyDisplay, createdAt]
          properties:
            id: { type: string }
            name: { type: [string, 'null'] }
            keyDisplay: { type: string }
            createdAt: { type: string, format: date-time }
        user:
          type: object
          required: [id, email, name]
          properties:
            id: { type: string }
            email: { type: string, format: email }
            name: { type: [string, 'null'] }
        space:
          type: [object, 'null']
          properties:
            id: { type: string }
            slug: { type: string }
            name: { type: string }
        role:
          type: [object, 'null']
          properties:
            name: { $ref: '#/components/schemas/RoleName' }
            label: { type: string }
            description: { type: string }

    Schema:
      type: object
      required:
        [
          id,
          type,
          slug,
          name,
          namePlural,
          description,
          integrationVendorName,
          attributeCount,
          createdAt,
          updatedAt
        ]
      properties:
        id: { type: string }
        type: { $ref: '#/components/schemas/SchemaType' }
        slug: { type: string }
        name: { type: string }
        namePlural: { type: string }
        description: { type: [string, 'null'] }
        integrationVendorName: { type: [string, 'null'] }
        attributeCount:
          type: integer
          description: Count of custom attribute rows only (base attributes excluded).
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    AttributeDefinition:
      type: object
      description: >-
        Attribute definition metadata. Stored definitions carry nullable; evaluated definitions use
        target=schema or json. An aggregate keeps a stored type and adds valueSource.type=aggregate;
        it is not writable. Writable stored fields have no valueSource. Optional autoFill uses
        mode=default with defaultValue, or mode=ai with optional prompt. Evaluated and aggregate
        values require app/runtime expansion and are not included in public record reads. Types
        url/email/phone/ref/object do not exist; use string formats and json as documented.
        Generated evaluated expressions are never returned.
      required: [type]
      properties:
        type:
          type: string
          enum:
            - string
            - number
            - decimal
            - boolean
            - date time
            - date only
            - time only
            - string array
            - json
            - evaluated
        nullable:
          type: boolean
          description: Present on every stored definition. Absent on `evaluated`.
        multiLine:
          type: boolean
          description: "On `type: string` only. Selects which `format` values are available."
        format:
          type: string
          description: >
            On `type: string` only. With `multiLine` false or absent: `plain text`, `email`, `phone`
            or `url`. With `multiLine: true`: `plain text` or `code`.
        jsonSchema:
          type: object
          description: "On `type: json` only. A simplified JSON Schema, including for nested structures."
        autoFill:
          type: object
          description: "Stored field: mode default with defaultValue, or mode ai with optional prompt."
          additionalProperties: true
        valueSource:
          type: object
          description: "Aggregate metadata: type=aggregate; sourceKind=event|track;
            eventSchemaId/trackDefinitionId;
            specification(operation=count|sum|latest,valueProperty,dimensionProperty,filter,windowS\
            econds); cadenceSeconds,enabled,revision. Makes this stored type non-writable."
          additionalProperties: true
        target:
          type: string
          enum:
            - schema
            - json
          description: Evaluated only. schema requires
            schemaId,cardinality,requirements,queryDescriptionMarkdown,expressionReferences. json
            requires
            jsonSchema,requirements,descriptionMarkdown,expressionReferences,expressionOutput=json|query.
        cardinality:
          type: string
          enum:
            - one
            - many
        schemaId:
          type: string
      additionalProperties: true

    Attribute:
      type: object
      required:
        [
          id,
          key,
          label,
          description,
          sequence,
          definition,
          integrationVendorName,
          createdAt,
          updatedAt
        ]
      properties:
        id:
          type: string
          description: A custom attribute id, or `base:<key>` for platform base attributes.
        key: { type: string }
        label: { type: string }
        description: { type: [string, 'null'] }
        sequence:
          type: [string, 'null']
          description: Ordering value for custom attributes; null for base attributes.
        definition: { $ref: '#/components/schemas/AttributeDefinition' }
        integrationVendorName: { type: [string, 'null'] }
        createdAt: { type: [string, 'null'], format: date-time }
        updatedAt: { type: [string, 'null'], format: date-time }

    EmailStatistics:
      type: object
      required: [sent, delivered, bounced, complained, unsubscribed, uniqueOpens, uniqueClicks]
      properties:
        sent: { type: integer }
        delivered: { type: integer }
        bounced: { type: integer }
        complained: { type: integer }
        unsubscribed: { type: integer }
        uniqueOpens: { type: integer }
        uniqueClicks: { type: integer }

    ContactRecord:
      type: object
      description: Contact record. Stored custom values are top-level; platform fields override colliding
        keys. Evaluated/aggregate values are not expanded by public record reads.
      required:
        - id
        - schemaId
        - schemaSlug
        - source
        - fullName
        - firstName
        - lastName
        - avatarUrl
        - userId
        - email
        - phone
        - subscriptions
        - topicIds
        - notes
        - tags
        - emailStatistics
        - createdAt
        - updatedAt
      properties:
        id: { type: string }
        schemaId: { type: string }
        schemaSlug: { type: string }
        source: { $ref: '#/components/schemas/ContactSource' }
        fullName: { type: [string, 'null'] }
        firstName: { type: [string, 'null'] }
        lastName: { type: [string, 'null'] }
        avatarUrl: { type: [string, 'null'] }
        userId: { type: [string, 'null'] }
        email: { type: [string, 'null'], format: email }
        phone: { type: [string, 'null'] }
        subscriptions:
          type: array
          items: { $ref: '#/components/schemas/CommunicationChannel' }
        topicIds:
          type: array
          items: { type: string }
        notes: { type: [string, 'null'] }
        tags:
          type: array
          items: { type: string }
        emailStatistics:
          oneOf:
            - { $ref: '#/components/schemas/EmailStatistics' }
            - { type: 'null' }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      additionalProperties: true

    ObjectRecord:
      type: object
      description: Object record. Stored custom values are top-level; platform fields override colliding
        keys. Evaluated/aggregate values are not expanded by public record reads.
      required: [id, schemaId, schemaSlug, externalId, notes, tags, createdAt, updatedAt]
      properties:
        id: { type: string }
        schemaId: { type: string }
        schemaSlug: { type: string }
        externalId: { type: [string, 'null'] }
        notes: { type: [string, 'null'] }
        tags:
          type: array
          items: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      additionalProperties: true

    Record:
      description: Contact or object record with stored custom values and platform fields flattened at the
        top level. Public record reads do not expand evaluated or aggregate attribute values.
      oneOf:
        - { $ref: '#/components/schemas/ContactRecord' }
        - { $ref: '#/components/schemas/ObjectRecord' }

    RecordInput:
      type: object
      description: >
        Input for creating, upserting, or updating a record. All fields are optional. For a contact
        schema the contact fields apply; for an object schema the object fields apply. Custom
        attribute keys are passed at the top level.
      properties:
        id: { type: string }
        fullName: { type: [string, 'null'] }
        firstName: { type: [string, 'null'] }
        lastName: { type: [string, 'null'] }
        avatarUrl: { type: [string, 'null'] }
        userId: { type: [string, 'null'] }
        email: { type: [string, 'null'], format: email }
        phone: { type: [string, 'null'] }
        subscriptions:
          type: array
          items: { $ref: '#/components/schemas/CommunicationChannel' }
        topicIds:
          type: array
          items: { type: string }
        externalId: { type: [string, 'null'] }
        notes: { type: [string, 'null'] }
        tags:
          type: array
          items: { type: string }
      additionalProperties: true

    AuditTrailEntry:
      type: object
      required:
        - id
        - createdAt
        - communicationChannel
        - communicationTopicIds
        - eventType
        - state
        - optInLevel
        - actorType
        - actorIdentifier
        - context
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        communicationChannel: { $ref: '#/components/schemas/CommunicationChannel' }
        communicationTopicIds:
          type: array
          items: { type: string }
        eventType:
          type: string
          enum:
            - email_verification_sent
            - email_verified
            - phone_verification_sent
            - phone_verified
            - subscribed
            - unsubscribed
            - topics_added
            - topics_removed
            - suppressed
            - unsuppressed
        state:
          type: [string, 'null']
          enum: [subscribed, not_subscribed, unsubscribed, pending, invalid, redacted, null]
        optInLevel:
          type: [string, 'null']
          enum: [single, double, unknown, null]
        actorType:
          type: string
          enum: [contact, admin, system, api]
        actorIdentifier: { type: [string, 'null'] }
        context:
          description: >
            Raw JSON context (consent text/version, IP, user agent, source URL, form id, timestamp).
            `ipAddress` and `userAgent` are stripped before being returned.

    SuppressionInput:
      type: object
      required: [identifier, reason]
      properties:
        identifier:
          type: string
          format: email
          description: The email address to suppress.
        reason: { $ref: '#/components/schemas/SuppressionCreateReason' }
        notes: { type: [string, 'null'] }

    Suppression:
      type: object
      required: [id, createdAt, communicationChannel, reason, notes, scope, identifierHash]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        communicationChannel: { $ref: '#/components/schemas/CommunicationChannel' }
        reason: { $ref: '#/components/schemas/SuppressionReason' }
        notes: { type: [string, 'null'] }
        scope:
          type: string
          enum: [space, global]
        identifierHash:
          type: string
          description: A hash of the suppressed identifier. The raw identifier is never returned.
        affectedContactCount:
          type: integer
          description: >
            Present only on `suppressions.create` and `suppressions.batchCreate` responses: contacts
            in the space whose identifier matched.

    SuppressionBatchCreateRequest:
      type: object
      required: [suppressions]
      properties:
        suppressions:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: '#/components/schemas/SuppressionInput' }

    SuppressionBatchCreateResultItem:
      type: object
      required: [index, status]
      description: >
        Suppression fields are present when `status` is `created` or `exists`; `error` is present
        when `status` is `failed`.
      allOf:
        - type: object
          properties:
            index: { type: integer }
            status:
              type: string
              enum: [created, exists, failed]
            error: { $ref: '#/components/schemas/ErrorObject' }
        - $ref: '#/components/schemas/Suppression'

    SuppressionBatchCreateResult:
      type: object
      required: [results, summary]
      properties:
        results:
          type: array
          items: { $ref: '#/components/schemas/SuppressionBatchCreateResultItem' }
        summary:
          type: object
          required: [created, exists, failed]
          properties:
            created: { type: integer }
            exists: { type: integer }
            failed: { type: integer }

    SuppressionBatchDeleteRequest:
      type: object
      required: [ids]
      properties:
        ids:
          type: array
          minItems: 1
          maxItems: 100
          items: { type: string }

    SuppressionBatchDeleteResultItem:
      type: object
      required: [index, status]
      properties:
        index: { type: integer }
        status:
          type: string
          enum: [removed, failed]
        error: { $ref: '#/components/schemas/ErrorObject' }

    SuppressionBatchDeleteResult:
      type: object
      required: [results, summary]
      properties:
        results:
          type: array
          items: { $ref: '#/components/schemas/SuppressionBatchDeleteResultItem' }
        summary:
          type: object
          required: [removed, failed]
          properties:
            removed: { type: integer }
            failed: { type: integer }

    EventInput:
      type: object
      description: Flat event acceptance input. Custom writable attribute keys are top-level and
        normalized against the event schema. Any id is ignored on fire. Reserved readiness fields
        are not caller-controlled. Supply eventId for retry deduplication. The normalized input has
        a 256 KiB ceiling.
      properties:
        eventId:
          type: [string, 'null']
          description: Caller-owned occurrence identifier, scoped to the space and event schema. Reuse with
            identical input and occurredAt for retries. If omitted or null, the server generates a
            UUID; save the returned eventId. A changed payload under the same value returns
            event_id_conflict.
        occurredAt:
          type: string
          format: date-time
          description: When the event occurred. Defaults to acceptance time; explicit values must be no more
            than 366 days old or five minutes in the future. Preserve the same explicit value on
            retries.
        contactId:
          type:
            - string
            - "null"
          description: Explicit contact subject id in this space. Mutually exclusive with objectId.
        objectId:
          type:
            - string
            - "null"
          description: Explicit object subject id in this space. Mutually exclusive with contactId.
      additionalProperties: true

    FireResult:
      type: object
      required:
        - accepted
        - id
        - eventId
        - isDuplicate
        - readinessAtAcceptance
      properties:
        accepted:
          type: integer
          const: 1
          description: One event occurrence was durably accepted or replayed.
        id:
          type: string
          description: maxclicks event id; use it with events.status.
        eventId:
          type: string
          description: Caller-supplied or generated occurrence identifier.
        isDuplicate:
          type: boolean
        readinessAtAcceptance:
          type: string
          enum:
            - pending
            - ready
          description: Immutable original acceptance state. Poll events.status for current readiness.
      example:
        accepted: 1
        id: ev_BcwDvBUeSaSDILA5tHgpmU7I
        eventId: order_1042_paid
        isDuplicate: false
        readinessAtAcceptance: pending

    BatchFireItemResult:
      type: object
      required: [index, status]
      properties:
        index: { type: integer }
        status:
          type: string
          enum:
            - accepted
            - failed
            - rolled_back
        error: { $ref: '#/components/schemas/ErrorObject' }
        id:
          type: string
        eventId:
          type:
            - string
            - "null"
        isDuplicate:
          type: boolean
        readinessAtAcceptance:
          type: string
          enum:
            - pending
            - ready
        retryAfterMilliseconds:
          type: integer
          minimum: 0
          description: Present on an event-admission failure; delay before retrying that occurrence.

    BatchFireResult:
      type: object
      required: [results, summary]
      properties:
        results:
          type: array
          items: { $ref: '#/components/schemas/BatchFireItemResult' }
        summary:
          type: object
          required: [accepted, failed]
          properties:
            accepted: { type: integer }
            failed: { type: integer }
        failedIndex:
          type: integer
          description: Present on abort. Index of the item whose failure stopped processing; later items were
            not attempted.
      example:
        results:
          - index: 0
            status: accepted
            id: ev_BcwDvBUeSaSDILA5tHgpmU7I
            eventId: order_1042_paid
            isDuplicate: false
            readinessAtAcceptance: pending
        summary:
          accepted: 1
          failed: 0

    EventBatchRequest:
      type: object
      required: [events]
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/EventInput' }
        onError:
          type: string
          enum: [continue, abort]
          default: continue

    Event:
      type: object
      description: A ready business event. Custom stored values are flattened into the top level. Pending
        or failed acceptances are available through events.status instead.
      required:
        - id
        - schemaId
        - schemaSlug
        - eventId
        - createdAt
        - receivedAt
        - occurredAt
        - readyAt
        - readinessState
        - definitionRevision
        - contactId
        - objectId
      properties:
        id: { type: string }
        schemaId: { type: string }
        schemaSlug: { type: string }
        eventId: { type: [string, 'null'] }
        createdAt: { type: string, format: date-time }
        receivedAt:
          type: string
          format: date-time
        occurredAt:
          type: string
          format: date-time
        readyAt:
          type:
            - string
            - "null"
          format: date-time
        readinessState:
          type: string
          enum:
            - pending
            - ready
            - failed
            - outcome_unknown
        definitionRevision:
          type: string
        contactId:
          type:
            - string
            - "null"
        objectId:
          type:
            - string
            - "null"
      additionalProperties: true

    DnsRecord:
      type: object
      required: [type, name, value, priority, status]
      properties:
        type: { type: string }
        name: { type: string }
        value: { type: string }
        priority: { type: [integer, 'null'] }
        status:
          type: string
          enum: [not started, pending, success, failed, temporary failure]

    Domain:
      type: object
      required: [id, createdAt, updatedAt, name, customReturnPath, verificationStarted, verifiedForSending]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        name: { type: string }
        customReturnPath: { type: string }
        verificationStarted: { type: boolean }
        verifiedForSending: { type: boolean }
        records:
          type: array
          description: Live-checked DNS records. Present on `get`, `create`, and `verify`; omitted on `list`.
          items: { $ref: '#/components/schemas/DnsRecord' }

    Sender:
      type: object
      required: [id, createdAt, updatedAt, name, description, code, domainId, email]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        name: { type: string }
        description: { type: [string, 'null'] }
        code: { type: string, description: The email local part. }
        domainId: { type: string }
        email: { type: string, format: email, description: "`code`@domain, derived." }

    Topic:
      type: object
      required: [id, createdAt, updatedAt, name, description, color, channel, private]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        name: { type: string }
        description: { type: [string, 'null'] }
        color: { $ref: '#/components/schemas/BadgeColor' }
        channel: { $ref: '#/components/schemas/CommunicationChannel' }
        private:
          type: boolean
          description: Private topics only appear in the contact preferences page if the contact has been
            granted access.

    Segment:
      type: object
      required:
        [
          id,
          createdAt,
          updatedAt,
          name,
          description,
          contactSchemaId,
          contactSchemaSlug,
          requirements,
          conditionDescriptionMarkdown,
          integrationVendorName
        ]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        name: { type: string }
        description: { type: [string, 'null'] }
        contactSchemaId: { type: string }
        contactSchemaSlug: { type: string }
        requirements:
          type: string
          description: The refined natural-language specification used to generate the condition.
        conditionDescriptionMarkdown:
          type: string
          description: Human-readable markdown description of the condition. The generated expression itself
            is never returned.
        integrationVendorName: { type: [string, 'null'] }
        matchingCount:
          type: integer
          description: >
            Present when the segment was just created, or just updated with new `requirements`;
            omitted otherwise. A live count at that moment, not kept in sync afterward (call
            `segments.count` for the current value).

    SegmentCreate:
      type: object
      required: [name, contactSchema, requirements]
      properties:
        name: { type: string }
        description: { type: [string, 'null'] }
        contactSchema: { type: string, description: A contact schema id or slug. }
        requirements: { type: string, description: Natural-language description of the filter criteria. }

    TemplateFrom:
      description: "A sender: either a verified domain with a manual slug and display name, or a saved
        sender profile."
      oneOf:
        - type: object
          required: [type, domainId, code]
          properties:
            type: { type: string, const: domain }
            domainId: { type: string }
            code: { type: string, description: Email local part. }
            name: { type: [string, 'null'] }
        - type: object
          required: [type, senderId]
          properties:
            type: { type: string, const: sender }
            senderId: { type: string }

    DataPath:
      type: array
      description: A path into a data payload, as property-key segments.
      items: { type: string }

    DataRepresentationProperty:
      description: >
        One property of a data representation. `record` properties reference records from a schema;
        `json` properties are plain JSON values described by a JSON schema. `schema` accepts an id
        or slug on input; responses always echo the slug.
      oneOf:
        - type: object
          required: [key, type, schema]
          properties:
            key: { type: string }
            type: { type: string, const: record }
            schema: { type: string, description: A schema id or slug. }
            isArray: { type: boolean, default: false }
            description: { type: [string, 'null'] }
        - type: object
          required: [key, type, jsonSchema]
          properties:
            key: { type: string }
            type: { type: string, const: json }
            jsonSchema:
              type: object
              description: A simplified JSON Schema describing the value.
            description: { type: [string, 'null'] }

    Template:
      type: object
      required:
        [
          id,
          createdAt,
          updatedAt,
          name,
          description,
          transactional,
          contentFormat,
          contentEditor,
          content,
          from,
          recipientDataPath,
          expectedDataRepresentation,
          communicationTopicId
        ]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        name: { type: [string, 'null'] }
        description: { type: [string, 'null'] }
        transactional: { type: boolean }
        contentFormat:
          type: [string, 'null']
          enum: [mjml, html, null]
        contentEditor:
          type: [string, 'null']
          enum: [rich, raw, null]
        content:
          oneOf:
            - type: object
              required: [subject, body]
              properties:
                subject: { type: [string, 'null'] }
                body: { type: [string, 'null'] }
            - type: 'null'
        from:
          oneOf:
            - { $ref: '#/components/schemas/TemplateFrom' }
            - { type: 'null' }
        recipientDataPath: { $ref: '#/components/schemas/DataPath' }
        expectedDataRepresentation:
          type: array
          items: { $ref: '#/components/schemas/DataRepresentationProperty' }
        communicationTopicId: { type: [string, 'null'] }

    TemplateSendRequest:
      type: object
      required: [data]
      properties:
        data:
          type: object
          description: >
            Payload keyed by the template's expected data-representation properties. Minimal
            identifiers suffice for record properties (e.g. `{ "email": "user@example.com" }` for a
            contact); the platform finds-or-creates the record, then reads it back for rendering.
          additionalProperties: true

    TemplateSendResponse:
      type: object
      required: [emailId, status, error]
      properties:
        emailId: { type: [string, 'null'] }
        status:
          type: [string, 'null']
          enum: [sent, failed, null]
        error:
          type: [string, 'null']
          description: A generic message when `status` is `failed`. The real failure detail stays server-side.

    BroadcastSchedulingStatus:
      type: string
      enum: [none, draft, send now, send at]

    BroadcastAudience:
      description: The broadcast's resolved audience.
      oneOf:
        - type: object
          required: [type]
          properties:
            type: { type: string, const: all }
        - type: object
          required: [type, segmentId]
          properties:
            type: { type: string, const: segment }
            segmentId: { type: string }
        - type: object
          required: [type, requirements, conditionDescriptionMarkdown]
          properties:
            type: { type: string, const: custom filter }
            requirements: { type: string }
            conditionDescriptionMarkdown: { type: string }

    BroadcastAudienceInput:
      description: >
        The audience to point a broadcast at: everyone in its contact schema, or an existing
        segment. A `custom filter` audience is authored in the maxclicks app, where a human reviews
        the generated condition before it drives a mass send, and cannot be set here.
      oneOf:
        - type: object
          required: [type]
          properties:
            type: { type: string, const: all }
        - type: object
          required: [type, segmentId]
          properties:
            type: { type: string, const: segment }
            segmentId: { type: string }

    Broadcast:
      type: object
      required:
        [
          id,
          createdAt,
          updatedAt,
          name,
          description,
          contactSchemaId,
          contactSchemaSlug,
          schedulingStatus,
          scheduledFor,
          sendingStartedAt,
          sendingFinishedAt,
          failureReason,
          audience,
          hasContent
        ]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        name: { type: [string, 'null'] }
        description: { type: [string, 'null'] }
        contactSchemaId: { type: string }
        contactSchemaSlug: { type: string }
        schedulingStatus: { $ref: '#/components/schemas/BroadcastSchedulingStatus' }
        scheduledFor: { type: [string, 'null'], format: date-time }
        sendingStartedAt: { type: [string, 'null'], format: date-time }
        sendingFinishedAt: { type: [string, 'null'], format: date-time }
        failureReason: { type: [string, 'null'] }
        audience: { $ref: '#/components/schemas/BroadcastAudience' }
        hasContent:
          type: boolean
          description: "Never includes the underlying email content itself (e.g. raw Adaptyle markup), only
            this boolean summary."

    BroadcastUpdate:
      type: object
      description: >
        At least one field must be provided. Only `name`/`description`/ `audience`/`scheduledFor`
        are updatable; content is not editable through this endpoint. Rejected once sending has
        started.
      properties:
        name: { type: [string, 'null'] }
        description: { type: [string, 'null'] }
        audience: { $ref: '#/components/schemas/BroadcastAudienceInput' }
        scheduledFor: { type: [string, 'null'], format: date-time }

    BroadcastSendRequest:
      type: object
      description: An empty or omitted body sends now; a `scheduledFor` schedules for later.
      properties:
        scheduledFor: { type: string, format: date-time }

    BroadcastRun:
      type: object
      required: [id, createdAt, contactId, contactEmail, status, failureReason, email]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        contactId: { type: string }
        contactEmail: { type: [string, 'null'] }
        status:
          type: string
          enum: [sent, failed, pending]
        failureReason: { type: [string, 'null'] }
        email:
          oneOf:
            - type: object
              required: [id, sentAt, opensCount, clicksCount, bounced, complained]
              properties:
                id: { type: string }
                sentAt: { type: [string, 'null'], format: date-time }
                opensCount: { type: integer }
                clicksCount: { type: integer }
                bounced: { type: boolean }
                complained: { type: boolean }
            - type: 'null'
      description: "A narrow public delivery projection. status is derived from email presence and
        delivery fields: no associated email is reported failed; email sentAt is sent;
        send/rejection error is failed; otherwise pending. This is not the complete internal
        planning, credit, quality-pause, or redrive state. An unsent email object can remain pending
        while operational work waits."

    EmailAggregatedMetrics:
      type: object
      required:
        [
          sent,
          delivered,
          bounced,
          deliveryDelays,
          rejected,
          failed,
          complained,
          unsubscribed,
          opens,
          uniqueOpens,
          clicks,
          uniqueClicks
        ]
      properties:
        sent: { type: integer }
        delivered: { type: integer }
        bounced: { type: integer }
        deliveryDelays: { type: integer }
        rejected: { type: integer }
        failed: { type: integer }
        complained: { type: integer }
        unsubscribed: { type: integer }
        opens: { type: integer }
        uniqueOpens: { type: integer }
        clicks: { type: integer }
        uniqueClicks: { type: integer }

    BroadcastMetricsResponse:
      type: object
      description: "`email` is omitted entirely when the broadcast has no email content yet."
      properties:
        email: { $ref: '#/components/schemas/EmailAggregatedMetrics' }

    WebhookTrigger:
      type: string
      enum:
        - contact upserted
        - contact deleted
        - object upserted
        - object deleted
        - event fired
        - email event

    EmailEventType:
      type: string
      enum:
        - scheduled
        - sending failed
        - sent
        - rejected
        - unsubscribed
        - bounced
        - complained
        - delivered
        - opened
        - clicked
        - delivery delayed

    WebhookStatus:
      type: string
      enum: [live, paused, circuit breaker]

    WebhookCondition:
      description: The webhook's resolved filter condition. The generated expression is never returned,
        only its description.
      oneOf:
        - type: object
          required: [type]
          properties:
            type: { type: string, const: none }
        - type: object
          required: [type, segmentId]
          properties:
            type: { type: string, const: segment }
            segmentId: { type: string }
        - type: object
          required: [type, description]
          properties:
            type: { type: string, const: custom filter }
            description: { type: string }

    WebhookConditionInput:
      description: >
        The filter condition to create or update a webhook with. A `custom filter` condition
        generates via AI from `requirements` (billed, `ai` rate bucket).
      oneOf:
        - type: object
          required: [type]
          properties:
            type: { type: string, const: none }
        - type: object
          required: [type, segmentId]
          properties:
            type: { type: string, const: segment }
            segmentId: { type: string }
        - type: object
          required: [type, requirements]
          properties:
            type: { type: string, const: custom filter }
            requirements:
              type: string
              description: Natural-language description of the filter criteria.

    Webhook:
      type: object
      required:
        - id
        - createdAt
        - updatedAt
        - url
        - trigger
        - emailEventTypes
        - schemaId
        - condition
        - status
        - totalCalls
        - failedCalls
        - failedCallsInARow
        - lastCalledAt
        - lastCallErrorMessage
        - integrationVendorName
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        url: { type: string, format: uri }
        trigger: { $ref: '#/components/schemas/WebhookTrigger' }
        emailEventTypes:
          type: array
          items: { $ref: '#/components/schemas/EmailEventType' }
        schemaId: { type: [string, 'null'] }
        condition: { $ref: '#/components/schemas/WebhookCondition' }
        status: { $ref: '#/components/schemas/WebhookStatus' }
        totalCalls: { type: integer }
        failedCalls: { type: integer }
        failedCallsInARow: { type: integer }
        lastCalledAt: { type: [string, 'null'], format: date-time }
        lastCallErrorMessage: { type: [string, 'null'] }
        integrationVendorName:
          type: [string, 'null']
          description: Non-null means the webhook is integration-owned and cannot be updated, deleted, or
            rotated.

    WebhookWithSecret:
      allOf:
        - { $ref: '#/components/schemas/Webhook' }
        - type: object
          required: [signatureVerificationSecret]
          properties:
            signatureVerificationSecret: { type: string }

    WebhookCreate:
      type: object
      required: [url, trigger]
      properties:
        url: { type: string, format: uri }
        trigger: { $ref: '#/components/schemas/WebhookTrigger' }
        emailEventTypes:
          type: array
          description: Required and non-empty only when trigger is `email event`.
          items: { $ref: '#/components/schemas/EmailEventType' }
        schemaId:
          type: [string, 'null']
          description: A schema id or slug. Required for every trigger except `email event`.
        condition:
          $ref: '#/components/schemas/WebhookConditionInput'

    WebhookUpdate:
      type: object
      description: At least one field must be provided. Trigger and schema are not updatable.
      properties:
        url: { type: string, format: uri }
        emailEventTypes:
          type: array
          items: { $ref: '#/components/schemas/EmailEventType' }
        condition: { $ref: '#/components/schemas/WebhookConditionInput' }
        status:
          type: string
          enum: [live, paused]

    RotatedWebhookSecret:
      type: object
      required: [id, signatureVerificationSecret, updatedAt]
      properties:
        id: { type: string }
        signatureVerificationSecret: { type: string }
        updatedAt: { type: string, format: date-time }

    Workflow:
      type: object
      required: [id, createdAt, updatedAt, name, description, paused, isDirty, published]
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        name: { type: [string, 'null'] }
        description: { type: [string, 'null'] }
        paused: { type: boolean }
        isDirty: { type: boolean, description: Has unpublished changes. }
        published: { type: boolean }

    WorkflowRunStepHistoryRecord:
      type: object
      required:
        - index
        - stepId
        - stepLabel
        - slotKey
        - enteredAt
        - leftAt
        - failureReason
      properties:
        stepId: { type: string }
        stepLabel: { type: [string, 'null'] }
        slotKey: { type: [string, 'null'] }
        enteredAt: { type: string, format: date-time }
        leftAt: { type: string, format: date-time }
        failureReason: { type: [string, 'null'] }
        index:
          type: integer

    WorkflowRun:
      type: object
      required:
        - id
        - createdAt
        - updatedAt
        - versionId
        - status
        - failureReason
        - stepHistory
        - stepCount
        - parentRunId
        - parentExecutionOrdinal
        - nextHistoryBeforeIndex
      properties:
        id: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        versionId: { type: string }
        status:
          type: string
          enum: [running, completed, failed]
        failureReason: { type: [string, 'null'] }
        stepHistory:
          type: array
          items: { $ref: '#/components/schemas/WorkflowRunStepHistoryRecord' }
        stepCount:
          type: integer
          description: Persisted execution ordinal; not the length of this history window.
        parentRunId:
          type:
            - string
            - "null"
        parentExecutionOrdinal:
          type:
            - integer
            - "null"
        nextHistoryBeforeIndex:
          type:
            - integer
            - "null"
          description: Pass as history_before to get earlier history; null when no earlier cursor is available.
      example:
        id: wfr_BcwDvBUeSaSDILA5tHgpmU7I
        createdAt: 2026-09-14T10:30:00.000Z
        updatedAt: 2026-09-14T10:30:01.000Z
        versionId: wfv_BcwDvBUeSaSDILA5tHgpmU7I
        status: running
        failureReason: null
        stepHistory:
          - index: 0
            stepId: wfs_BcwDvBUeSaSDILA5tHgpmU7I
            stepLabel: Incoming webhook
            slotKey: next
            enteredAt: 2026-09-14T10:30:00.000Z
            leftAt: 2026-09-14T10:30:01.000Z
            failureReason: null
        stepCount: 1
        parentRunId: null
        parentExecutionOrdinal: null
        nextHistoryBeforeIndex: null

    FormContactInput:
      type: object
      description: Flat contact input for the form's contact schema. Custom attribute keys are passed at
        the top level.
      properties:
        id: { type: [string, 'null'] }
        fullName: { type: [string, 'null'] }
        firstName: { type: [string, 'null'] }
        lastName: { type: [string, 'null'] }
        avatarUrl: { type: [string, 'null'] }
        userId: { type: [string, 'null'] }
        email: { type: [string, 'null'], format: email }
        phone: { type: [string, 'null'] }
        notes: { type: [string, 'null'] }
        tags:
          type: [array, 'null']
          items: { type: string }
      additionalProperties: true

    FormSubmission:
      type: object
      required: [contact]
      properties:
        contact: { $ref: '#/components/schemas/FormContactInput' }
        emailMarketingConsent: { type: [boolean, 'null'] }
        captchaToken: { type: [string, 'null'] }

    FormSubmissionResult:
      oneOf:
        - type: object
          required: [contactId]
          properties:
            contactId: { type: string }
        - type: object
          required: [status]
          properties:
            status: { type: string, const: verification_pending }
    RecordDeletionStatus:
      type: object
      required:
        - operationId
        - status
        - message
      properties:
        operationId:
          type: string
          description: Opaque deletion operation identifier. Preserve the whole value.
        status:
          type: string
          enum:
            - pending
            - blocked
            - completed
        message:
          type: string
      example:
        operationId: delete:records:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
        status: pending
        message: Record deletion is in progress.
    EventStatus:
      type: object
      required:
        - id
        - eventId
        - receivedAt
        - occurredAt
        - readyAt
        - readinessState
        - definitionRevision
        - contactId
        - objectId
        - data
      properties:
        id:
          type: string
        eventId:
          type:
            - string
            - "null"
        receivedAt:
          type: string
          format: date-time
        occurredAt:
          type: string
          format: date-time
        readyAt:
          type:
            - string
            - "null"
          format: date-time
        readinessState:
          type: string
          enum:
            - pending
            - ready
            - failed
            - outcome_unknown
        definitionRevision:
          type: string
        contactId:
          type:
            - string
            - "null"
        objectId:
          type:
            - string
            - "null"
        data:
          type: object
          additionalProperties: true
          description: Raw accepted input until readiness; prepared data when ready.
      example:
        id: ev_BcwDvBUeSaSDILA5tHgpmU7I
        eventId: order_1042_paid
        receivedAt: 2026-09-14T10:30:00.000Z
        occurredAt: 2026-09-14T10:30:00.000Z
        readyAt: null
        readinessState: pending
        definitionRevision: f2d9685136d4746456815a2b8a6d7a3f9eba98dfc4d6129497778d5037410d7a
        contactId: null
        objectId: null
        data:
          amount: 42
    EventIdentityInput:
      type: object
      additionalProperties: false
      required:
        - sourceId
        - subjectSchemaId
        - anonymousId
        - contactId
        - operationId
        - expectedAliasRevision
      properties:
        sourceId:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$
          description: Source id matching an enabled contact track definition.
          example: source_website
        subjectSchemaId:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$
          description: Contact schema id (not a slug) used by the track.
          example: sch_BcwDvBUeSaSDILA5tHgpmU7I
        anonymousId:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$
          description: Browser identity in the configured source.
          example: visitor_1042
        contactId:
          type:
            - string
            - "null"
          pattern: ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$
          description: Contact in the subject schema, or null to unbind.
          example: con_BcwDvBUeSaSDILA5tHgpmU7I
        operationId:
          type: string
          format: uuid
          description: Stable UUID for this identity change; reuse the same body on retries.
          example: e125aecb-8b4e-4c7e-8f08-8f768bced7ab
        expectedAliasRevision:
          type: string
          pattern: ^(0|[1-9][0-9]{0,17})$
          description: Last known revision for this alias. Use "0" only for a new alias.
          example: "0"
    EventIdentityResult:
      type: object
      required:
        - identityRevision
        - affectedSubjectIds
        - isDuplicate
      properties:
        identityRevision:
          type: string
          description: Committed revision as a decimal string; do not coerce to a floating-point number.
        affectedSubjectIds:
          type: array
          items:
            type: string
          description: Previous and new bound contacts whose track results can change.
        isDuplicate:
          type: boolean
    IdempotencyUnknownError:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              const: idempotency_outcome_unknown
            message:
              type: string
            operationId:
              type: string
            type:
              type: string
              description: May be omitted on a persistence failure.
