openapi: 3.1.0
info:
  title: BoltClaw API
  version: 1.22.0
  summary: Pages, conversations, social publishing and referrals for one BoltClaw workspace.
  description: |
    Everything the BoltClaw dashboard does with your link-in-bio pages, the conversations people
    start on them, your connected social accounts and your referral programs, from your own
    systems.

    ## Base URL

    `https://api.boltclaw.ai/v1`. `https://boltclaw.ai/api/v1` remains a supported alias: it
    reaches the same gateway and the same operations, so an integration written against it keeps
    working. Use `api.boltclaw.ai` for anything new: it is the API's own host, one hop shorter,
    and the address every example in this reference uses.

    ## Authentication

    Send your API key as `Authorization: Bearer bc_live_...`. Create one in the dashboard under
    API keys; it is shown once. A key carries scopes, and each operation names the scope it needs
    (`x-scope`). A key without it is answered `403 INSUFFICIENT_SCOPE`.

    ## Your key decides the workspace

    Every key belongs to one workspace, so no operation takes a workspace parameter.
    `GET /workspace` tells you which one. Another workspace's page, post, thread or program
    answers `404 NOT_FOUND`, never `403`: "not yours" and "not there" are the same answer, so
    nothing confirms that somebody else's object exists.

    ## Rate limits

    Each key may make a number of requests a minute set by its organisation's plan, shared by
    every operation. Every response after the key is checked carries `X-RateLimit-Limit`,
    `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds). Over the limit is
    `429 RATE_LIMITED` with `Retry-After`. Two operations draw on a second, smaller budget:
    `POST /pages` (10 an hour by default, because a create is the one call that can tell you a
    slug is taken) and `POST /inquiries/{id}/messages` (60 an hour by default, because each one
    reaches a person). Both are set per plan; `GET /usage` returns the numbers that apply to you,
    as `api_pages_created_per_hour` and `api_messages_per_hour`.

    ## Errors

    Every failure is `{ "error": "...", "code": "...", "request_id": "..." }`. Branch on `code`
    (the `ErrorCode` table lists every one); `error` is a sentence for a person and its wording
    may change. A validation error also carries `fields`, each named field with the rule it
    broke. Treat an unknown `code` as a generic failure of its HTTP status class.

    ## Request ids

    Every response carries `X-Request-Id`, and every error body carries the same value as
    `request_id`. Quote it when you contact support. Send your own `X-Request-Id` (a UUID) and
    it is used instead, so your logs and ours share one id.

    ## Pagination

    A list with `next_cursor` is paged: pass it back as `cursor` for the next page, and stop
    when it is `null`. Cursors are opaque; do not build one. `limit` is clamped, not rejected.
    Some lists are short by nature and answer `{ "data": [...] }` with no cursor at all
    (a page's links, a conversation's messages, webhooks, the webhook event catalog, a
    webhook's delivery attempts); three always answer `next_cursor: null` (accounts,
    automations, API keys).

    ## Idempotency

    Any `POST` accepts an `Idempotency-Key` header (1 to 255 printable ASCII characters; a UUID
    works). Repeat the same request with the same key within 24 hours and you get the first
    answer again, with `Idempotent-Replay: true`, instead of a second page, message or post.
    The same key with a different request is `409 IDEMPOTENCY_KEY_CONFLICT`; the same key while
    the first request is still running is `409 IDEMPOTENCY_KEY_IN_FLIGHT`. Only a success is
    kept: after a refusal, fix the request and retry with the same key. Keys are scoped to your
    workspace and the operation.

    ## Versioning

    `/v1` changes only by addition: new operations, new optional fields, new error codes, new
    enum values in responses. Tolerate fields you do not know. Removing or renaming anything,
    or changing a status code, ships as `/v2`, with `/v1` kept alongside.

    ## Not in this API

    A key cannot block a person, or read payout details or exports. A person does each in the
    dashboard.

    Managing keys and webhooks IS here, on `keys:write` and `webhooks:write`, so that rotating a
    credential on a schedule is a cron job rather than somebody clicking. Both are refused to an
    AI assistant's key, which cannot carry either scope at all: a credential acting on its own
    should not be able to issue credentials or point your data at a new address.
  contact:
    name: BoltClaw support
    url: https://boltclaw.ai/support
  license:
    name: Proprietary
    url: https://boltclaw.ai/terms
servers:
  - url: https://api.boltclaw.ai/v1
    description: Production. https://boltclaw.ai/api/v1 remains a supported alias that reaches the same gateway.
externalDocs:
  description: The API reference
  url: https://boltclaw.ai/docs/api
tags:
  - name: Meta
    description: Your key and its workspace.
  - name: API keys
    description: The workspace's API keys, and what each may do.
  - name: Pages
    description: Link-in-bio pages, served at boltclaw.ai/{slug}.
  - name: Links
    description: The links on a page, in display order.
  - name: Conversations
    description: Threads people start from a page, and the messages in them.
  - name: Webhooks
    description: The endpoints registered to receive events. Read only here.
  - name: Social accounts
    description: Connected network accounts. Connecting one happens in the dashboard.
  - name: Posts
    description: Posts to one or more connected accounts, now, scheduled, or as drafts.
  - name: Media
    description: Images and videos for posts, uploaded in two steps.
  - name: Inbox
    description: Comments, mentions and direct messages from every connected account.
  - name: Automations
    description: Comment-to-DM rules that reply to people and capture leads.
  - name: Leads
    description: Contact details people chose to give.
  - name: Usage
    description: The organisation's plan, allowances and platform charges.
  - name: Referrals
    description: Referral programs, partners, conversions, commissions and payouts.
  - name: Agents
    description: Device sign-in, approvals, and a key acting on itself.
  - name: Logs
    description: What assistants did here, and what everybody did here.
security:
  - apiKey: []
paths:
  /ping:
    get:
      operationId: ping
      tags:
        - Meta
      summary: Check a key
      description: |
        The cheapest authenticated call: confirms the key works and returns the scopes it carries.
        Needs no scope. Use it during setup and as a liveness probe.
      x-scope: none
      x-function: api-meta
      x-rate-limit-class: read
      responses:
        '200':
          description: The key is valid.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ping'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /workspace:
    get:
      operationId: getWorkspace
      tags:
        - Meta
      summary: Read the key's workspace
      description: |
        The workspace this key belongs to: its id and its name. Needs no scope. The id appears in
        no other response; it is how you tell two keys apart.
      x-scope: none
      x-function: api-meta
      x-rate-limit-class: read
      responses:
        '200':
          description: The key's workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workspace'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /keys:
    get:
      operationId: listApiKeys
      tags:
        - API keys
      summary: List API keys
      description: |
        Every key this workspace holds, newest first, including revoked and expired ones: the row
        is the record that a credential was issued here and stopped working. Never the keys
        themselves, which are not stored.

        Read `lost_scopes` on each row. Anything in it means the person who created that key no
        longer holds the permission a scope stands on, so the key is quietly doing less than it
        was issued to do.
      x-scope: keys:read
      x-permission: api_keys.manage
      x-function: api-keys
      x-rate-limit-class: read
      parameters:
        - $ref: '#/components/parameters/Limit100'
      responses:
        '200':
          description: The workspace's keys. Never paged; `next_cursor` is always null.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createApiKey
      tags:
        - API keys
      summary: Create an API key
      description: |
        Issues a key for this workspace and returns it ONCE, as `secret`. Only a hash is stored,
        so no endpoint can produce it again: store it where your servers read their secrets, and
        rotate it if you lose it.

        `Idempotency-Key` is required. Minting a credential is the one operation where a retried
        network timeout cannot be told apart from a request that never landed, and the only safe
        move without a key would be to create a second credential. A replay answers the same body
        with `secret: null`.

        What you asked for is not necessarily what you get. The scopes are bounded twice: by the
        scopes the calling key itself carries, and by the permissions the person who created the
        calling key still holds. Anything left off is listed in `dropped`, and the key is still
        created. Nothing left at all is `403 INSUFFICIENT_SCOPE`.

        The expiry is clamped to the calling key's own, so a child never outlives its parent. The
        organisation's plan sets how many live keys a workspace may hold (`402 PLAN_LIMIT`, and
        the message names the number).
      x-scope: keys:write
      x-permission: api_keys.manage
      x-function: api-keys
      x-rate-limit-class: write
      x-agent-consequence: Issues a working credential for this workspace, returned once.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreate'
      responses:
        '201':
          description: Created. `secret` is the key, and this is the only response that carries it.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
            Location:
              $ref: '#/components/headers/Location'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /keys/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateApiKey
      tags:
        - API keys
      summary: Rename a key, or change its scopes
      description: |
        Changes the name, the scopes, or both. The key itself is untouched, so whatever holds it
        keeps working: narrowing a key no longer means replacing it everywhere it is deployed.

        `scopes` is the new set in full, not a list to add. Adding one is bounded exactly as
        creating a key is, and anything left off is in `dropped`. Removing one is always allowed.
      x-scope: keys:write
      x-permission: api_keys.manage
      x-function: api-keys
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Changes what a live credential may do, at once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyUpdate'
      responses:
        '200':
          description: The key, as it now is.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyUpdated'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: revokeApiKey
      tags:
        - API keys
      summary: Revoke a key
      description: |
        Stops the key working, at once and for good. The row stays, so a credential that turns up
        in a log next month is still identifiable as one that was issued here and switched off.

        Revoking a key twice is `404`, like a key that does not exist and like another
        workspace's: the second call would otherwise move the revocation time and erase when it
        happened.
      x-scope: keys:write
      x-permission: api_keys.manage
      x-function: api-keys
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Stops a credential working at once, and anything using it fails.
      responses:
        '200':
          description: The key, now revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKey'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /keys/{id}/rotate:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: rotateApiKey
      tags:
        - API keys
      summary: Rotate a key
      description: |
        Issues a replacement with the same scopes and the same expiry, and retires this one. The
        replacement is returned ONCE, as `secret`.

        THE REPLACEMENT HAS A NEW ID. A caller that stores the id as well as the value has to
        store both again. `rotated_from` on the new key names the old one.

        `grace_seconds` keeps the old key working for a while, so the new one can reach every
        deployment before anything breaks. Zero, the default, stops it at once, which is what you
        want when you are rotating because the old value leaked. The answer says exactly when the
        old key stops, in `previous_valid_until`.

        `Idempotency-Key` is required, for the same reason it is on creating a key. A replay
        answers `secret: null`.
      x-scope: keys:write
      x-permission: api_keys.manage
      x-function: api-keys
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Issues a replacement credential and retires this one, which stops working.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyRotate'
      responses:
        '201':
          description: The replacement. `secret` is the new key, and the only response carrying it.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
            Location:
              $ref: '#/components/headers/Location'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRotated'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /pages:
    get:
      operationId: listPages
      tags:
        - Pages
      summary: List pages
      description: The workspace's pages, newest first.
      x-scope: pages:read
      x-permission: pages.view
      x-function: public-api
      x-rate-limit-class: read
      parameters:
        - $ref: '#/components/parameters/Limit200'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of pages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createPage
      tags:
        - Pages
      summary: Create a page
      description: |
        Creates a page at boltclaw.ai/{slug}. Slugs are global: `409 CONFLICT` means the name is
        taken, by anyone. A reserved slug is `400`. The organisation's plan sets how many pages it
        may hold and publish (`402 PLAN_LIMIT`). This operation draws on its own budget of 10
        attempts an hour per key (`429`).
      x-scope: pages:write
      x-permission: pages.create
      x-function: public-api
      x-rate-limit-class: write
      x-makes-public: true
      x-agent-consequence: Creates a page; with published true it is live at boltclaw.ai/{slug} at once.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PageCreate'
      responses:
        '201':
          description: Created.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Page'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /pages/{slug}:
    parameters:
      - $ref: '#/components/parameters/Slug'
    get:
      operationId: getPage
      tags:
        - Pages
      summary: Read a page
      description: One of the workspace's pages, published or not.
      x-scope: pages:read
      x-permission: pages.view
      x-function: public-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Page'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updatePage
      tags:
        - Pages
      summary: Update a page
      description: |
        Changes the fields you send. The slug cannot change: it is the page's public address, and
        renaming it would break every link and QR code pointing at it. Publishing a page counts
        against the plan's published-page allowance (`402 PLAN_LIMIT`).
      x-scope: pages:write
      x-permission: pages.edit
      x-function: public-api
      x-rate-limit-class: write
      x-boltclaw-approval: page.unpublish
      x-boltclaw-approval-when: published is false
      x-cross-tenant: 404
      x-makes-public: true
      x-agent-consequence: Changes a page; on a published page the change is live at once, and published false takes it offline.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PageUpdate'
      responses:
        '200':
          description: The updated page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Page'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deletePage
      tags:
        - Pages
      summary: Delete a page
      description: |
        Deletes the page, its links and its statistics. People who messaged it keep a read-only
        copy of their own conversation; your access to those conversations ends. Needs its own
        scope, `pages:delete`, which `pages:write` does not include.
      x-scope: pages:delete
      x-permission: pages.delete
      x-function: public-api
      x-rate-limit-class: write
      x-boltclaw-approval: page.delete
      x-cross-tenant: 404
      x-agent-consequence: Permanently deletes the page, its links and its statistics, and ends access to its conversations.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /pages/{slug}/links:
    parameters:
      - $ref: '#/components/parameters/Slug'
    get:
      operationId: listLinks
      tags:
        - Links
      summary: List a page's links
      description: |
        Every link on the page, in display order, with no cursor. Links are unlimited in practice:
        a page holds at most 1,000 links, a safety ceiling the same on every plan
        (`links_per_page` in `GET /usage`). A page that already had more when its ceiling was
        lowered keeps every one of them, and all of them are listed here.
      x-scope: pages:read
      x-permission: pages.view
      x-function: public-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The links.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createLink
      tags:
        - Links
      summary: Add a link
      description: |
        Adds a link to the page. A page holds at most 1,000 links, a safety ceiling the same on
        every plan (`links_per_page` in `GET /usage`): adding one to a page at its ceiling is
        `409 CONFLICT`. A page that had more when its ceiling was lowered keeps them all, and you can
        still edit, reorder and remove them, but nothing is added to it until it is under the
        ceiling. A `label` longer than your organisation's `link_label_chars` (120 by default) is
        `400`.
      x-scope: pages:write
      x-permission: pages.edit
      x-function: public-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-makes-public: true
      x-agent-consequence: Adds a link to the page; on a published page it is visible at once.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LinkCreate'
      responses:
        '201':
          description: Created.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Link'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /pages/{slug}/stats:
    parameters:
      - $ref: '#/components/parameters/Slug'
    get:
      operationId: getPageStats
      tags:
        - Pages
      summary: Read a page's views and clicks
      description: |
        Views and link clicks between `from` and `to` (the last 30 days by default), counted by
        BoltClaw itself. The answer starts no earlier than your plan's analytics history allows
        (`analytics_days` in `GET /usage`: 30 days on Free), whatever `from` says; the window
        actually used is echoed back, so a `from` later than the one you sent means the plan's
        history ended there. Nothing older is deleted: a larger plan sees it at once.
      x-scope: pages:read
      x-permission: analytics.view
      x-function: public-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: from
          in: query
          required: false
          description: An ISO-8601 date or timestamp. Default 30 days before `to`.
          schema:
            type: string
        - name: to
          in: query
          required: false
          description: An ISO-8601 date or timestamp, later than `from`. Default now.
          schema:
            type: string
      responses:
        '200':
          description: The counts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageStats'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /links/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateLink
      tags:
        - Links
      summary: Update a link
      description: Changes the fields you send.
      x-scope: pages:write
      x-permission: pages.edit
      x-function: public-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-makes-public: true
      x-agent-consequence: Changes a link; on a published page the change is visible at once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LinkUpdate'
      responses:
        '200':
          description: The updated link.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Link'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteLink
      tags:
        - Links
      summary: Remove a link
      description: Removes the link from its page. Ordinary page editing, so `pages:write` is enough.
      x-scope: pages:write
      x-permission: pages.edit
      x-function: public-api
      x-rate-limit-class: write
      x-boltclaw-approval: link.delete
      x-cross-tenant: 404
      x-agent-consequence: Removes the link from the page at once.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Removed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inquiries:
    get:
      operationId: listInquiries
      tags:
        - Conversations
      summary: List conversations
      description: |
        Conversations on the workspace's pages, most recently active first. Poll with
        `since=<your last poll>` to get every thread that has had a message since.
      x-scope: inquiries:read
      x-permission: messages.view
      x-function: public-api
      x-rate-limit-class: read
      parameters:
        - $ref: '#/components/parameters/Limit200'
        - $ref: '#/components/parameters/Cursor'
        - name: since
          in: query
          required: false
          description: Only threads with a message at or after this ISO-8601 date or timestamp.
          schema:
            type: string
        - name: slug
          in: query
          required: false
          description: Only threads on this page.
          schema:
            $ref: '#/components/schemas/Slug'
      responses:
        '200':
          description: A page of conversations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InquiryList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inquiries/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateInquiry
      tags:
        - Conversations
      summary: Set a conversation's status
      description: The status in your inbox, and nothing else. The words cannot be edited.
      x-scope: inquiries:write
      x-permission: messages.manage
      x-function: public-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Changes the conversation's status in the creator's inbox.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InquiryUpdate'
      responses:
        '200':
          description: The conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inquiry'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inquiries/{id}/messages:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listInquiryMessages
      tags:
        - Conversations
      summary: Read a conversation
      description: |
        The messages, oldest first. Reading them does not mark them read; call
        `POST /inquiries/{id}/read` when a person has actually opened the thread.
      x-scope: inquiries:read
      x-permission: messages.view
      x-function: public-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/Limit200'
      responses:
        '200':
          description: The messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: sendInquiryMessage
      tags:
        - Conversations
      summary: Reply in a conversation
      description: |
        Sends a message to the person who wrote in, from your page; they are emailed. A closed
        conversation is `409 CONFLICT`. Limited to 60 an hour per key and 30 an hour per
        conversation by default (`429`), the same per-conversation number the dashboard is held
        to; your plan's numbers are `api_messages_per_hour` and `thread_messages_per_hour` in
        `GET /usage`.
      x-scope: inquiries:write
      x-permission: messages.reply
      x-function: public-api
      x-rate-limit-class: write
      x-boltclaw-approval: conversation.reply
      x-cross-tenant: 404
      x-agent-consequence: Sends a message to a real person as the creator's page, and emails it to them.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageCreate'
      responses:
        '201':
          description: Sent.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageSent'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inquiries/{id}/close:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: setInquiryClosed
      tags:
        - Conversations
      summary: Close or re-open a conversation
      description: |
        While closed, neither side can write. Re-opening can collide with a newer open thread from
        the same person on the same page (`409 CONFLICT`). Closing is not blocking; blocking a
        person happens in the dashboard.
      x-scope: inquiries:write
      x-permission: messages.manage
      x-function: public-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Closes or re-opens the conversation; while it is closed neither side can write.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InquiryClose'
      responses:
        '200':
          description: The conversation.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Inquiry'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inquiries/{id}/read:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: markInquiryRead
      tags:
        - Conversations
      summary: Mark a conversation read
      description: |
        Marks every unread message from the person who wrote in as read, which they see as a read
        receipt. Call it when a person opens the thread, never from a background poll. Takes no
        body; a second call marks nothing.
      x-scope: inquiries:write
      x-permission: messages.view
      x-function: public-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Tells the person who wrote in that their messages were read.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: How many messages were marked.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarkedRead'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/events:
    get:
      operationId: listWebhookEventTypes
      tags:
        - Webhooks
      summary: List webhook event types
      description: |
        Every event an endpoint can subscribe to, with the object each one is about. Read this
        rather than hard-coding the list: an event added later appears here, and an endpoint that
        subscribed to everything receives it without being changed.
      x-scope: webhooks:read
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: read
      responses:
        '200':
          description: The catalog, in the order the dashboard shows it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEventTypeList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks:
    get:
      operationId: listWebhooks
      tags:
        - Webhooks
      summary: List webhook endpoints
      description: |
        The endpoints registered for this workspace, to see where events go and why one did not
        arrive.

        Needs `webhooks:read`. A key issued with `inquiries:read` before this operation had a scope
        of its own is still accepted here, and that acceptance will be withdrawn: issue new keys
        with `webhooks:read`.
      x-scope: webhooks:read
      x-any-scopes:
        - webhooks:read
        - inquiries:read
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: read
      responses:
        '200':
          description: The endpoints.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createWebhook
      tags:
        - Webhooks
      summary: Register a webhook endpoint
      description: |
        Register a URL and receive the signing secret, once.

        The URL must be `https` and must resolve to a public host. Loopback, private ranges and
        bare IP addresses are refused here and again at every delivery, because a hostname that is
        public today can point somewhere else tomorrow.

        `events` left out means every event, now and in future, which is what you want unless you
        have a reason not to. An explicit list is validated against the catalog and an unknown name
        is refused rather than dropped. `page_slugs` left out means every page in the workspace;
        events that are not about a page are only ever sent to an endpoint that covers all of them.

        `Idempotency-Key` is required. Minting a credential is the one operation where a retried
        network timeout cannot be told apart from a request that never landed, and the only safe
        move without a key would be to create a second endpoint. A replay answers the same body
        with `secret: null`.

        Five endpoints per workspace by default (`409`); your plan's number is
        `webhook_endpoints` in `GET /usage`.
      x-scope: webhooks:write
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: write
      x-agent-consequence: Starts sending this workspace's events to an address outside Bolt Claw.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreate'
      responses:
        '201':
          description: Created. `secret` is the signing secret, and this is the only response that carries it.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
            Location:
              $ref: '#/components/headers/Location'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookCreated'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getWebhook
      tags:
        - Webhooks
      summary: Read one webhook endpoint
      description: One endpoint's configuration and its delivery health. Never its signing secret.
      x-scope: webhooks:read
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateWebhook
      tags:
        - Webhooks
      summary: Change a webhook endpoint
      description: |
        Change the URL, the events, the pages, the description, or switch the endpoint on and off.
        Send only what you want to change.

        `enabled: false` is a pause: the endpoint and its signing secret survive, and events stop.
        That is what you want while a receiver is being redeployed: deleting the endpoint would
        destroy the secret every other environment is still using. `enabled: true` also clears the
        failure count, or the next single failure would switch it off again however long it had
        been working in between.

        Events missed while an endpoint is off are not replayed.
      x-scope: webhooks:write
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: write
      x-cross-tenant: 404
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookUpdate'
      responses:
        '200':
          description: The endpoint as it now is.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteWebhook
      tags:
        - Webhooks
      summary: Delete a webhook endpoint
      description: |
        Deletes the endpoint, its queued deliveries and its delivery log. The signing secret goes
        with it and cannot be recovered.

        To stop events without losing any of that, send `enabled: false` instead.
      x-scope: webhooks:write
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Deletes the endpoint, its queued deliveries, its delivery log and its signing secret.
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/{id}/rotate-secret:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: rotateWebhookSecret
      tags:
        - Webhooks
      summary: Rotate a signing secret
      description: |
        Replace the signing secret and receive the new one, once. The endpoint keeps its id, its
        URL and its subscriptions.

        **There is no grace window, and that is deliberate.** Rotating an API key leaves the old
        one working for a while because many systems hold it and we are the ones verifying it. A
        signing secret is verified by one receiver you control, so a window in which two secrets
        both validate is a window in which a leaked secret still validates, which is the only
        reason anybody rotates one in a hurry. The old secret stops at once: paste the new one into
        your receiver, and the deliveries in between fail their signature check and are retried.

        `Idempotency-Key` is required, for the same reason it is on registering an endpoint. A
        replay answers `secret: null`.
      x-scope: webhooks:write
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Stops the current signing secret working immediately and issues a replacement, returned once.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmptyBody'
      responses:
        '200':
          description: Rotated. `secret` is the new signing secret, and the only response carrying it.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSecretRotated'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/{id}/deliveries:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listWebhookDeliveries
      tags:
        - Webhooks
      summary: List delivery attempts
      description: |
        One row per attempt, newest first: the exact body that was signed, the status your endpoint
        answered with, up to 512 bytes of that answer, how long it took, and why it failed when it
        did.

        `delivery_id` is stable across every attempt of one occurrence, and is the value the
        `X-BoltClaw-Delivery` header carries. Key your idempotency off it.

        Attempts are kept for 30 days. The reason for a failure is one of a fixed set of sentences,
        never your own server's words.
      x-scope: webhooks:read
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/Limit100'
        - name: event
          in: query
          required: false
          description: Only attempts for this event. An unknown name is `400`.
          schema:
            $ref: '#/components/schemas/WebhookEvent'
      responses:
        '200':
          description: The attempts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDeliveryList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/{id}/deliveries/{delivery_id}/redeliver:
    parameters:
      - $ref: '#/components/parameters/Id'
      - name: delivery_id
        in: path
        required: true
        description: The `delivery_id` of an attempt. Anything that is not one of yours is `404`.
        schema:
          type: string
          format: uuid
    post:
      operationId: redeliverWebhookDelivery
      tags:
        - Webhooks
      summary: Send a delivery again
      description: |
        Queue the same body again, signed afresh. The retry ladder starts over, and the body is the
        one that was frozen when the event happened, so your receiver sees the event it already
        knows how to deduplicate rather than a second, different fact.

        Sixty redeliveries per workspace per hour (`429`). A paused endpoint is `409`.
      x-scope: webhooks:write
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: write
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmptyBody'
      responses:
        '202':
          description: Queued. The delivery has not happened yet.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookQueued'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /webhooks/{id}/test:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: sendWebhookTestEvent
      tags:
        - Webhooks
      summary: Send a test event
      description: |
        Queue a `webhook.test` delivery to this one endpoint, signed exactly as a real event is, so
        you can prove your receiver verifies a signature before a real event arrives to tell you it
        does not.

        It is sent whatever the endpoint has subscribed to and whatever pages it covers: a test you
        have to subscribe to first cannot tell you whether your subscription works. A paused
        endpoint is `409`.
      x-scope: webhooks:write
      x-permission: webhooks.manage
      x-function: api-webhooks
      x-rate-limit-class: write
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmptyBody'
      responses:
        '202':
          description: Queued. The delivery has not happened yet.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestQueued'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /accounts:
    get:
      operationId: listAccounts
      tags:
        - Social accounts
      summary: List connected accounts
      description: Every connected account and what its network supports. Never paged (`next_cursor` is always null).
      x-scope: social:read
      x-permission: social.view
      x-function: social-api
      x-rate-limit-class: read
      responses:
        '200':
          description: The accounts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /posts:
    get:
      operationId: listPosts
      tags:
        - Posts
      summary: List posts
      description: The workspace's posts, newest first, optionally filtered.
      x-scope: social:read
      x-permission: social.view
      x-function: social-api
      x-rate-limit-class: read
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/PostStatus'
        - name: from
          in: query
          required: false
          description: An ISO-8601 timestamp with an offset.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: An ISO-8601 timestamp with an offset.
          schema:
            type: string
            format: date-time
        - $ref: '#/components/parameters/Limit200'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of posts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createPost
      tags:
        - Posts
      summary: Create a post
      description: |
        One post to one or more connected accounts. Say when with exactly one of `scheduled_for`,
        `publish_now: true` or `draft: true`. A post needs a `body` or at least one media id. The
        organisation's monthly post allowance applies (`402 PLAN_LIMIT`). Posting to X is billed
        per call on top of the plan.
      x-scope: social:write
      x-permission: posts.create
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: post.publish
      x-boltclaw-approval-when: draft is not true
      x-makes-public: true
      x-spends-money: true
      x-agent-consequence: Publishes to the connected social accounts now or at scheduled_for, unless draft is true.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostCreate'
      responses:
        '201':
          description: Created.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/Upstream'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /posts/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getPost
      tags:
        - Posts
      summary: Read a post
      description: The post, its media and where it stands on each account.
      x-scope: social:read
      x-permission: social.view
      x-function: social-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The post.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updatePost
      tags:
        - Posts
      summary: Update a post
      description: Changes a draft or scheduled post. Any other is `409 CONFLICT`.
      x-scope: social:write
      x-permission: posts.create
      x-function: social-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-makes-public: true
      x-spends-money: true
      x-agent-consequence: Changes a draft or scheduled post, including when it will publish.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostUpdate'
      responses:
        '200':
          description: The updated post.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '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'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deletePost
      tags:
        - Posts
      summary: Delete a post
      description: Deletes the post from BoltClaw.
      x-scope: social:write
      x-permission: posts.delete
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: post.delete
      x-cross-tenant: 404
      x-agent-consequence: Permanently deletes the post from BoltClaw.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /posts/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: cancelPost
      tags:
        - Posts
      summary: Cancel a scheduled post
      description: Stops a post that has not published yet. Takes no body.
      x-scope: social:write
      x-permission: posts.cancel
      x-function: social-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Stops a scheduled post from publishing.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The cancelled post.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /media:
    post:
      operationId: createMediaUpload
      tags:
        - Media
      summary: Start a media upload
      description: |
        Reserves a media item and returns a signed URL to upload the file to, then call
        `POST /media/{id}/complete`. The organisation's media storage allowance applies
        (`402 PLAN_LIMIT`). A replay with the same `Idempotency-Key` answers `upload: null`: the
        upload credential is never kept, so ask for a new slot if you still need one.
      x-scope: social:write
      x-permission: posts.create
      x-function: social-api
      x-rate-limit-class: upload
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MediaCreate'
      responses:
        '201':
          description: Reserved.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MediaUpload'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /media/{id}/complete:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: completeMediaUpload
      tags:
        - Media
      summary: Finish a media upload
      description: Checks the uploaded file and makes the media item ready for posts. Before the upload finishes it is `409 CONFLICT`.
      x-scope: social:write
      x-permission: posts.create
      x-function: social-api
      x-rate-limit-class: upload
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The media item.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Media'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inbox:
    get:
      operationId: listInbox
      tags:
        - Inbox
      summary: List the inbox
      description: Comments, mentions and messages across connected accounts, newest first. Open items by default.
      x-scope: social:read
      x-permission: inbox.view
      x-function: social-api
      x-rate-limit-class: read
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - open
              - done
              - hidden
              - all
        - name: kind
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/InboxKind'
        - name: account_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/Limit200'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of items.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inbox/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getInboxThread
      tags:
        - Inbox
      summary: Read an inbox thread
      description: The item and the rest of its thread, oldest first.
      x-scope: social:read
      x-permission: inbox.view
      x-function: social-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The thread.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxThread'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: setInboxStatus
      tags:
        - Inbox
      summary: Set an item's status
      description: Open or done, in your inbox only. Hiding is `POST /inbox/{id}/hide`.
      x-scope: social:write
      x-permission: inbox.reply
      x-function: social-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InboxStatusUpdate'
      responses:
        '200':
          description: The item.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxItem'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /inbox/{id}/reply:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: replyToInboxItem
      tags:
        - Inbox
      summary: Reply to an item
      description: |
        Replies on the network as the connected account: in public to a comment, privately to a
        message. The network's own refusal is `UPSTREAM_ERROR` (422, 502 or 503) with a sentence
        that says what happened. Replying on X is billed per call on top of the plan.
      x-scope: social:write
      x-permission: inbox.reply
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: inbox.reply
      x-cross-tenant: 404
      x-makes-public: true
      x-spends-money: true
      x-agent-consequence: Posts a reply on the network as the connected account, public for a comment and private for a message.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InboxReply'
      responses:
        '201':
          description: The reply, as an outbound item.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxItem'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/Upstream'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /inbox/{id}/hide:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: hideInboxItem
      tags:
        - Inbox
      summary: Hide or unhide a comment
      description: |
        Hides the comment on the network (an empty body means hide), or `{ "hidden": false }`
        unhides it. A network that cannot hide comments is `422`.
      x-scope: social:write
      x-permission: inbox.moderate
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: inbox.hide
      x-cross-tenant: 404
      x-agent-consequence: Hides or unhides the comment on the network.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InboxHide'
      responses:
        '200':
          description: The item.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxItem'
        '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'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/Upstream'
        '503':
          $ref: '#/components/responses/UpstreamUnavailable'
  /automations:
    get:
      operationId: listAutomations
      tags:
        - Automations
      summary: List automations
      description: Every automation, with its counters. Never paged (`next_cursor` is always null).
      x-scope: social:read
      x-permission: social.view
      x-function: social-api
      x-rate-limit-class: read
      responses:
        '200':
          description: The automations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createAutomation
      tags:
        - Automations
      summary: Create an automation
      description: |
        A rule that answers a keyword in a comment or a message with a DM, and optionally a public
        reply and a request for an email or phone number. What a network supports is checked
        here (`400`). The plan sets how many automations an organisation may hold and how many
        DMs a month they may send (`402 PLAN_LIMIT`). DMs on X are billed per call on top of the plan.
      x-scope: social:write
      x-permission: automations.manage
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: automation.enable
      x-boltclaw-approval-when: enabled is not false
      x-makes-public: true
      x-spends-money: true
      x-agent-consequence: Starts replying to and messaging people automatically as the connected account, while it is enabled.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutomationCreate'
      responses:
        '201':
          description: Created.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Automation'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /automations/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateAutomation
      tags:
        - Automations
      summary: Update an automation
      description: Changes the fields you send. The account cannot change; delete it and create another.
      x-scope: social:write
      x-permission: automations.manage
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: automation.enable
      x-boltclaw-approval-when: enabled is true
      x-cross-tenant: 404
      x-makes-public: true
      x-spends-money: true
      x-agent-consequence: Changes what an automation sends; enabled true starts it.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutomationUpdate'
      responses:
        '200':
          description: The automation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Automation'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteAutomation
      tags:
        - Automations
      summary: Delete an automation
      description: Deletes the automation. Leads it captured are kept.
      x-scope: social:write
      x-permission: automations.manage
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: automation.delete
      x-cross-tenant: 404
      x-agent-consequence: Permanently deletes the automation and stops it.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /leads:
    get:
      operationId: listLeads
      tags:
        - Leads
      summary: List leads
      description: Contact details people chose to give, newest first. Needs `leads:read`, which no preset includes.
      x-scope: leads:read
      x-permission: leads.view
      x-function: social-api
      x-rate-limit-class: read
      parameters:
        - name: account_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/Limit200'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of leads.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LeadList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /leads/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    delete:
      operationId: deleteLead
      tags:
        - Leads
      summary: Delete a lead
      description: Deletes the lead's contact details. Needs `leads:read` and `social:write` together.
      x-scope: leads:read
      x-also-scopes:
        - social:write
      x-permission: leads.delete
      x-function: social-api
      x-rate-limit-class: write
      x-boltclaw-approval: lead.delete
      x-cross-tenant: 404
      x-agent-consequence: Permanently deletes the lead's contact details.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /usage:
    get:
      operationId: getUsage
      tags:
        - Usage
      summary: Read plan usage
      description: |
        The organisation's plan, its allowances, this month's use, and the platform charges billed
        on top. The charges are the organisation's bill, so they are shown only when the person who
        created the key may manage billing; for everybody else `platform_charges` is `null`.
      x-scope: social:read
      x-permission: social.view
      x-function: social-api
      x-rate-limit-class: read
      responses:
        '200':
          description: Usage.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Usage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/conversions:
    post:
      operationId: recordReferralConversion
      tags:
        - Referrals
      summary: Report a conversion
      description: |
        A lead, sale, refund or dispute. `event_id` is YOUR id for the event and the idempotency
        key: to retry, send the SAME `event_id`. A repeat answers HTTP `200` with
        `duplicate: true` and changes nothing. A `409 CONFLICT` means another event for this
        customer was being recorded at that moment: send the same request again. Never make up a
        new `event_id` to retry: that is a new conversion, and can pay a partner twice.

        A repeat answers with the conversion as it was first recorded. A lead or a sale needs
        `customer`; a sale needs `amount` and `currency`; a refund or dispute needs `refund_of`
        (the sale's `event_id`), and a refund its `amount`. Amounts are integers in minor units.
        Needs `conversions:write`, which does nothing else, so it can sit in a checkout server.
      x-scope: conversions:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-idempotent: true
      x-cross-tenant: 404
      x-agent-consequence: Records a lead, sale, refund or dispute; a sale can earn a partner a commission they will be paid.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConversionCreate'
      responses:
        '200':
          description: Already reported; the first conversion, unchanged.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversion'
        '201':
          description: Recorded.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversion'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs:
    get:
      operationId: listReferralPrograms
      tags:
        - Referrals
      summary: List programs
      description: The workspace's referral programs.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      parameters:
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of programs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProgramList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createReferralProgram
      tags:
        - Referrals
      summary: Create a program
      description: |
        A referral program with a public join page partners apply through. Values are checked
        here: an https `product_url`, `cookie_days` from 1 to 365, a known `attribution_model`,
        `payout_schedule` and `join_mode` (`400`). The plan sets how many programs an
        organisation may run.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-makes-public: true
      x-agent-consequence: Creates a program with a public join page partners can apply through.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProgramCreate'
      responses:
        '201':
          description: Created.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralProgram
      tags:
        - Referrals
      summary: Read a program
      description: The program, its join link and how many partners are waiting for a decision.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The program.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralProgram
      tags:
        - Referrals
      summary: Update a program
      description: |
        Changes the fields you send; a field left out keeps its value. Values are checked as on create.

        Sending `terms` saves new program terms and raises `terms_version`: every partner must accept
        the new version before their next payout. An AI assistant's key is asked first when the body
        carries `terms`, unless your organisation lets assistants save terms without asking. Draft
        terms from the program's own settings with `POST /v1/referrals/programs/{id}/terms/draft`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.terms.update
      x-boltclaw-approval-when: terms is sent
      x-cross-tenant: 404
      x-makes-public: true
      x-agent-consequence: Changes a program; its join page and terms change for partners at once, and new terms must be accepted by every partner before their next payout.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProgramUpdate'
      responses:
        '200':
          description: The program.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/partners:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralPartners
      tags:
        - Referrals
      summary: List a program's partners
      description: Partners by their name in this program, with all-time stats. An email appears only where the partner chose to share it. `q` keeps the partners whose display name contains it, ignoring case.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/PartnerStatus'
        - name: q
          in: query
          required: false
          description: 1 to 80 characters, without `*`. Keeps partners whose display name contains it, ignoring case.
          schema:
            type: string
            minLength: 1
            maxLength: 80
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of partners.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/approve:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: approveReferralPartner
      tags:
        - Referrals
      summary: Approve a partner
      description: |
        Approves an application. The partner gets their links, starts earning, and is emailed at
        once. Approving a partner who is already approved changes nothing and emails nobody, so a
        retry is safe. Takes no body.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.partner.approve
      x-boltclaw-approval-always: true
      x-idempotent: true
      x-cross-tenant: 404
      x-agent-consequence: Approves the partner, who is emailed at once and then earns commissions on their referrals.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The partner.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Partner'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/reject:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: rejectReferralPartner
      tags:
        - Referrals
      summary: Reject a partner
      description: Rejects an application. Takes no body.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Rejects the partner's application.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The partner.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Partner'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/commissions:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralCommissions
      tags:
        - Referrals
      summary: List a program's commissions
      description: Commissions, newest first, each with its conversion and partner.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/CommissionStatus'
        - name: partner_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - name: from
          in: query
          required: false
          schema:
            type: string
            maxLength: 64
        - name: to
          in: query
          required: false
          schema:
            type: string
            maxLength: 64
        - name: adjustments
          in: query
          required: false
          description: '`true` for adjustments only, `false` for earned commissions only.'
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of commissions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommissionList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/commissions/{id}/adjust:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: adjustReferralCommission
      tags:
        - Referrals
      summary: Adjust a commission
      description: |
        Adds an adjustment on the same conversion, never an edit in place: a signed amount in
        minor units (not 0) and your reason, which is recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.commission.adjust
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Adds a commission adjustment the partner will be paid, or owe; it is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommissionAdjust'
      responses:
        '201':
          description: The adjustment.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Commission'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/payouts:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralPayouts
      tags:
        - Referrals
      summary: List a program's payouts
      description: Payouts, newest first. Payout details and export files stay in the dashboard.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/PayoutStatus'
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of payouts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}/paid:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: markReferralPayoutPaid
      tags:
        - Referrals
      summary: Mark a payout paid
      description: |
        You paid outside BoltClaw; this records it with your reference. Send `expected_minor` (the
        amount you were shown): if refunds have moved the total since, nothing is marked and the
        answer is `409 CONFLICT` naming the new figure. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.payout.paid
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Marks the payout paid with your reference and tells the partner; it is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayoutMarkPaid'
      responses:
        '200':
          description: The payout.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payout'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/payout-providers:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralPayoutProviders
      tags:
        - Referrals
      summary: List a program's payout providers
      description: |
        The payout accounts this program can pay through, by name and state: your own PayPal,
        Wise, Stripe, Tremendous, Trolley or Tango account, or your own payout system (`webhook`).
        Never a credential. Connecting or disconnecting one is done in the dashboard, on the
        Payouts tab.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The connections.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutProviderList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payout-providers/{id}/test:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: testReferralPayoutProvider
      tags:
        - Referrals
      summary: Test a payout provider connection
      description: |
        Makes one harmless read at the provider with the stored credential and records whether it
        works, and why not. Moves no money.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The connection, as the test left it.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutProvider'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/payout-settings:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralPayoutSettings
      tags:
        - Referrals
      summary: Get a program's payout settings
      description: |
        How the program pays: its provider, the providers a promoter may choose among, the
        schedule, sending on a schedule (available only after a legal review), and the largest
        payout sent without a second confirmation.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The settings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralPayoutSettings
      tags:
        - Referrals
      summary: Change how a program pays
      description: |
        Choose the provider that pays promoters, the providers they may choose among (each must be
        connected), the day of the month a monthly batch is made, and an optional largest payout.
        `auto_send` can be turned on only once BoltClaw allows it, after a legal review; until
        then a person sends every batch. Recorded in the activity log.

        These are a send's own rails, so a key that is not an AI assistant's needs
        `referrals:payouts` as well as `referrals:write`, as sending does; an assistant needs a
        person's approval.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.payout.settings
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Changes which payout account pays this program's promoters, and how; nothing is sent.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayoutSettingsUpdate'
      responses:
        '200':
          description: The settings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutSettings'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/payout-preview:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralPayoutPreview
      tags:
        - Referrals
      summary: See who is ready to be paid
      description: |
        Ready to pay, before any batch: per currency and provider, the promoters a batch would pay
        and the provider's estimated fees (charged by that provider to your account; BoltClaw
        charges nothing), and every promoter who would wait, with why: payout details changed in
        the last 48 hours, payouts locked, an open fraud flag, below your minimum, or details the
        provider needs that they have not given yet.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: Ready to pay.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutPreview'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/payout-batches:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralPayoutBatch
      tags:
        - Referrals
      summary: Make a payout batch
      description: |
        Turns every approved commission that can be paid into draft payouts, one per promoter per
        currency, each with the provider that pays it. Nothing is sent: the batch waits for
        `send`, a file export, or marking each payout paid. Below the minimum, negative, flagged
        or held waits for the next batch.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Nothing could be paid now; no batch was made.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutBatchResult'
        '201':
          description: A batch was made.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutBatchResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payout-batches/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralPayoutBatch
      tags:
        - Referrals
      summary: Get a payout batch, ready to send
      description: |
        Every payout in the batch with its provider, whether it can be sent now (and why not),
        the provider's estimated fee, and how it went once sent. `sendable` is exactly what `send`
        would send: its payout ids, its totals per currency (send them back as
        `expected_totals`), and the payouts large enough to need confirming by name.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The batch.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutBatch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payout-batches/{id}/send:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: sendReferralPayoutBatch
      tags:
        - Referrals
      summary: Send a payout batch through your payout provider
      description: |
        Pays the batch's payouts from YOUR provider account (PayPal, Wise, Stripe, Tremendous,
        Trolley, Tango) or asks your own payout system for each (`referral.payout.requested`).
        BoltClaw never holds the money. `expected_totals` must equal what the batch would send
        now (`GET /referrals/payout-batches/{id}`, `sendable.totals`), or nothing is sent; a
        payout in `sendable.large` must be named in `confirm_large`. Each payout is sent once,
        with an idempotency key made from its id; results arrive as `referral.payout.*` events.

        Needs `referrals:payouts`, the key your own payout server holds, or an AI assistant with a
        person's approval: `referrals:write` alone never sends money. Recorded in the activity log.
      x-scope: referrals:write
      x-any-scopes:
        - referrals:write
        - referrals:payouts
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.payout.send
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Sends real money from your payout provider account to every promoter in this batch, for the totals shown.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayoutSend'
      responses:
        '200':
          description: What happened to each payout.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutSendResult'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payout-batches/{id}/cancel-auto-send:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: cancelReferralPayoutAutoSend
      tags:
        - Referrals
      summary: Stop a scheduled send
      description: |
        Stops a batch that was going to be sent by itself after its 24-hour notice. A stop: it
        always works, and the batch stays for a person to send. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Whether a scheduled send was stopped.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoSendCancelled'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralPayout
      tags:
        - Referrals
      summary: Get a payout
      description: One payout, with how it was sent and what its provider said (`delivery`).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The payout.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payout'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}/details:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralPayoutDetails
      tags:
        - Referrals
      summary: Read where a requested payout goes
      description: |
        For YOUR OWN payout system, after `referral.payout.requested`: the promoter's own payout
        details for this one payout, while it waits to be paid. Refused (409) while the promoter's
        details changed in the last 48 hours or they locked their payouts. Every read is recorded
        in the activity log, as a payout export is. Needs `referrals:payouts`, which an AI
        assistant's key never holds.
      x-scope: referrals:payouts
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: Where to send it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}/result:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: reportReferralPayoutResult
      tags:
        - Referrals
      summary: Report what your payout system did
      description: |
        For YOUR OWN payout system: the payout was `paid` (with your reference), `failed` or
        `returned` (with a reason). Failed and returned put its commissions back in Ready to pay.
        The same answer again changes nothing; a payout already settled otherwise is 409. Needs
        `referrals:payouts`, which an AI assistant's key never holds.
      x-scope: referrals:payouts
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayoutResult'
      responses:
        '200':
          description: The payout.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payout'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/tax:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralTaxSettings
      tags:
        - Referrals
      summary: Get a program's tax settings
      description: |
        Whether this program's payouts wait for a tax form (`tax_forms`) or for the promoter's own
        invoice (`invoice_required`), and whether you issue self-billed invoices for your EU and UK
        business promoters (`self_billing`). Also whether your own tax details are complete, never
        the details themselves (your EIN, legal name and addresses are entered in the dashboard),
        and the table of which payout providers count toward your 1099-NEC, each with why and its
        source. BoltClaw prepares these forms; it does not give tax advice or file anything for
        you. See [Tax forms and 1099s](/docs/referral-tax).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The settings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaxSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralTaxSettings
      tags:
        - Referrals
      summary: Change whether a program's payouts wait for tax forms or invoices
      description: |
        Send only what you change. `tax_forms`: `off`; `us_only` (a US person needs a W-9, and
        anyone else signs a short statement that they are not a US person); or `all` (everyone
        needs a W-9, W-8BEN or W-8BEN-E). A promoter whose required form is missing, expired or
        replaced waits in Ready to pay until they sign. `invoice_required`: each payout waits for
        the promoter's own invoice (a promoter who self-bills never waits for one).
        `self_billing`: you issue the invoices for promoters who accept your self-billing
        agreement; turning it on needs your invoicing details first (`409` until they are
        complete). An archived program is `409`. Recorded in the activity log with the values
        before and after.

        Turning a requirement off lets money that waited for a form be paid, so a key needs
        `referrals:payouts` as well as `referrals:write` for this call, as changing how payouts
        are sent does; an AI assistant needs a person's approval.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.tax.settings
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Changes whether this program's payouts wait for tax forms or invoices; turning a requirement off lets money that waited for a form be paid.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaxSettingsUpdate'
      responses:
        '200':
          description: The settings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaxSettings'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/tax-forms:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralTaxForms
      tags:
        - Referrals
      summary: List each promoter's tax form status
      description: |
        Every approved or pending promoter of the program with their latest tax form for your
        business: its type (W-9, W-8BEN, W-8BEN-E, or the statement that they are not a US
        person), revision, status, when it was signed and when it expires. STATUS ONLY: never a
        tax number, a legal name or anything the form says. Opening a form is done in the
        dashboard, with two-step verification. `requirement_met` is false while their payouts
        wait for a form; `waiting_since` and `forfeitable_minor` say since when, and how much.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of promoters, by membership id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaxFormStatusList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/forfeit:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: forfeitReferralCommissions
      tags:
        - Referrals
      summary: Forfeit a promoter's money held for a missing tax form
      description: |
        Gives up, for good, every approved commission of this promoter that is waiting for a tax
        form in this program: they will not be paid it, and no payout will ever include it. Only
        money held for a missing, expired or replaced form can be forfeited (`409` otherwise, and
        when nothing is waiting); what they earn afterwards is not touched. Send a `reason` of 3 to
        500 characters: it is recorded in the activity log with what was given up. BoltClaw never
        forfeits anything by itself.

        Needs `referrals:payouts` as well as `referrals:write`, as sending money does, or an AI
        assistant with a person's approval: `referrals:write` alone never gives a promoter's money
        up.
      x-scope: referrals:write
      x-any-scopes:
        - referrals:write
        - referrals:payouts
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.commission.forfeit
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Gives up, for good, this promoter's approved money that is waiting for a tax form; they will not be paid it.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForfeitRequest'
      responses:
        '200':
          description: What was given up.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ForfeitResult'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/tax-years/{year}:
    parameters:
      - $ref: '#/components/parameters/Year'
    get:
      operationId: getReferralTaxYear
      tags:
        - Referrals
      summary: Get a year's 1099-NEC data
      description: |
        Everyone your referral programs paid in a calendar year, as one person per account, with
        how each was paid (per provider and currency), their tax form's status, and whether they
        belong on your 1099-NEC (`included`) and why, in a sentence. Warnings say what to fix
        before you file: no form, no tax number, backup withholding (24%) may apply, a replaced
        form, an expired W-8, money in another currency. Only US dollars paid through the
        providers your 1099 table counts are reportable, against the year's threshold
        (`threshold_minor`). No tax number and no legal name: the files themselves (the IRS
        IRIS CSV, a general CSV and Copy B) are made in the dashboard, with two-step verification.
        BoltClaw never files, never withholds and never checks a tax number with the IRS.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The year.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaxYear'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}/invoices:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralPayoutInvoices
      tags:
        - Referrals
      summary: List a payout's invoices
      description: |
        The invoices for one payout, newest first: the self-billed invoice you issued in the
        promoter's name, the promoter's own uploaded invoice, and any they replaced. Each says
        whether it was accepted, disputed (with the promoter's reason) or replaced. Read a PDF with
        `POST /referrals/invoices/{id}/link`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The payout's invoices.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/invoices/{id}/link:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralInvoiceLink
      tags:
        - Referrals
      summary: Get a download link for an invoice
      description: |
        A link to one invoice's PDF, good for ten minutes and one download. It names a promoter's
        business, so it needs the payouts permission and every link is recorded in the activity
        log. Treat the link as a secret; ask again for a new one.
      x-scope: referrals:read
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The link.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileLink'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}/statement-link:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralStatementLink
      tags:
        - Referrals
      summary: Get a download link for a payout statement
      description: |
        A link to the payout's statement as a PDF (who paid whom, how much, when and for which
        period), good for ten minutes and one download. The statement is what a promoter without
        a self-billed or uploaded invoice keeps. Treat the link as a secret; ask again for a new
        one.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The link.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileLink'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}/invoice:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: issueReferralInvoice
      tags:
        - Referrals
      summary: Issue a paid payout's self-billed invoice
      description: |
        For a program that self-bills: issues the self-billed invoice of a paid payout that has
        none (its promoter's or your own invoicing details were missing when it was paid). A
        payout that already has one answers that invoice, so a repeat changes nothing. `409` with
        what is missing when it cannot be issued: the payout is not paid, the promoter does not
        self-bill with this program, your invoicing details are incomplete, or the promoter is in
        your own country and your domestic VAT rate is not set. Moves no money.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The invoice.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/invoices/{id}/reissue:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: reissueReferralInvoice
      tags:
        - Referrals
      summary: Issue a corrected invoice for a disputed one
      description: |
        A promoter disputed a self-billed invoice within its 14 days. Fix what they said was wrong
        (ask them to correct their own details, or correct yours in the dashboard), then call this:
        the disputed invoice becomes `void` and a new one is issued, with the next number in the
        promoter's series, from the details as they are now. It starts its own 14 days. Only a
        disputed self-billed invoice can be replaced (`409` otherwise). Recorded in the activity
        log. Moves no money.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Voids the disputed invoice and issues a corrected one in the promoter's name, with a new number; no money moves.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: The corrected invoice.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/brand:
    get:
      operationId: getReferralBrand
      tags:
        - Referrals
      summary: Get the brand your partners see
      description: |
        Your brand as partners see it on your own partner portal, the join page, the in-app panel,
        the friend's welcome and their emails: its name, logos, colours, corner radius, support
        address, sender name and whether "Powered by BoltClaw" shows. Without `program_id`, the
        workspace's brand, which every program uses; with it, that program's own override, where a
        field left `null` follows the workspace's brand. `effective` is what partners of that
        program actually see, both merged. `support_email_verified` says whether the support
        address was confirmed with a code: only then is it the reply-to of partners' emails. See
        [White-label portal](/docs/referral-portal).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      parameters:
        - name: program_id
          in: query
          required: false
          description: A program's own override. Leave it out for the workspace's brand.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The brand.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Brand'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralBrand
      tags:
        - Referrals
      summary: Change the brand your partners see
      description: |
        Send only what you change; `null` clears a field (a program's override then follows the
        workspace's brand again). `name` up to 80 characters; `accent` and `background` as
        `#rrggbb`; `radius` from 0 to 24; `sender_name` up to 60 characters, the name partners'
        emails come from (the address stays BoltClaw's); `support_email`, which becomes the
        reply-to of partners' emails only after you confirm it with a code (changing it unconfirms
        it); `show_powered_by`, off unless you turn it on. A logo or icon is uploaded first with
        `POST /referrals/brand/uploads`, then saved here by sending the `path` it answered as
        `logo_light_path`, `logo_dark_path` or `favicon_path` (or `null` to remove it). With
        `program_id`, the change is that program's override. Recorded in the activity log with the
        names of the fields changed. An AI assistant asks a person first, unless your organisation
        lets assistants change it without asking.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.brand.update
      x-agent-consequence: Changes the name, logo, colours or support address your referral partners see on your portal, join page, panel and emails.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BrandUpdate'
      responses:
        '200':
          description: The brand.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Brand'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/brand/uploads:
    post:
      operationId: createReferralBrandUpload
      tags:
        - Referrals
      summary: Get an upload URL for a logo or icon
      description: |
        A signed URL for one file of your brand: `logo_light`, `logo_dark` or `favicon`, as a PNG,
        JPEG or WebP image of at most 2 MiB (no SVG). `PUT` the file's bytes to `upload_url` with
        its content type, then save it with `PATCH /referrals/brand`, sending `path` as the slot's
        field. Nothing changes until that PATCH: an upload that is never saved is not shown to
        anyone. The URL works for one file and expires (`expires_at`); treat it as a secret.
        `upload_url` is null in a replay of the same `Idempotency-Key`: ask for a new one.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-agent-consequence: Creates a one-file upload address for your brand's logo or icon; nothing changes until the brand is saved with it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BrandUploadRequest'
      responses:
        '201':
          description: Where to upload.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandUpload'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/brand/support-email/verification:
    post:
      operationId: startReferralBrandEmailVerification
      tags:
        - Referrals
      summary: Send a code to confirm the support address
      description: |
        Emails a six-digit code to the support address saved on the brand (the workspace's, or the
        program's with `program_id`), good for 15 minutes. Confirm it with
        `POST /referrals/brand/support-email/verification/confirm`; until then partners' emails
        have no reply-to. At most five codes a day for one brand and twenty for the workspace
        (`429` past that). `409` when the address is already confirmed, `400` when none is saved.
        The answer repeats the address the code went to.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-agent-consequence: Emails a six-digit confirmation code to the support address saved on your brand.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BrandScope'
      responses:
        '200':
          description: The code was sent.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandEmailCodeSent'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/brand/support-email/verification/confirm:
    post:
      operationId: confirmReferralBrandEmailVerification
      tags:
        - Referrals
      summary: Confirm the support address with its code
      description: |
        The six-digit code from the email `POST /referrals/brand/support-email/verification` sent.
        Each code has five tries; a wrong, used or expired one is `400` (send a new one). Once
        confirmed, the address is the reply-to of partners' emails about the program, until it is
        changed. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-agent-consequence: Confirms the support address on your brand, so partners' replies to your program's emails go to it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BrandEmailCodeConfirm'
      responses:
        '200':
          description: The brand, with the address confirmed.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Brand'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/domains:
    get:
      operationId: listReferralDomains
      tags:
        - Referrals
      summary: List your own link and portal domains
      description: |
        Every domain of your own connected to your referral programs: a links domain
        (`go.example.com`, where links read `https://go.example.com/<code>`) or a partner portal
        (`partners.example.com`), each with where it stands and the exact DNS records still to set.
        See [Your own domain](/docs/referral-domains).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      responses:
        '200':
          description: The domains.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: addReferralDomain
      tags:
        - Referrals
      summary: Connect one of your own domains
      description: |
        Claims a subdomain of yours (`go.example.com` for links, `partners.example.com` for the
        partner portal; never a domain's root, a wildcard or an IP address) for every program
        (`all_programs`, the default) or the ones in `program_ids`. Nothing is served yet: the
        answer's `records` are the exact DNS records to add at your DNS host, in order. First a
        TXT record, `_boltclaw.<host>` with the value `bc-verify=<token>`, which proves the domain
        is yours; once it is seen, the CNAME record our hosting provider recommends for it appears
        in `records` (it differs per domain: never use one from another domain), and sometimes a
        second TXT record our hosting provider asks for. Check progress with
        `POST /referrals/domains/{id}/check`. A domain counts against your plan's domains from the
        moment it is added (`402` past it); `409` when it is already yours, or connected to
        another account.

        An AI assistant always asks a person first: it decides where your links and portal will
        answer. In the dashboard, adding a domain also needs two-step verification; over the API
        that approval stands in for it.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.domain.add
      x-boltclaw-approval-always: true
      x-agent-consequence: Connects a domain of yours to your referral links or partner portal; once its DNS records are set, links and pages answer on it.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DomainCreate'
      responses:
        '201':
          description: The domain, with the records to set.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/domains/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralDomain
      tags:
        - Referrals
      summary: Get one of your domains
      description: |
        One domain: where it stands (`status`, and `problem`, a sentence saying what is in the
        way), the exact DNS records to set and whether each is in place, whether a CAA record on
        your domain keeps the certificate from being issued, and, once active, its address.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The domain.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: removeReferralDomain
      tags:
        - Referrals
      summary: Remove one of your domains
      description: |
        The domain stops answering at once: links on it stop working (each still works at its
        `https://boltclaw.ai/r/<code>` address) and its portal pages are gone. It is then taken off
        our hosting provider, so no record of ours is left pointing at it; remove your own DNS
        records for it too. Recorded in the activity log. An AI assistant always asks a person
        first.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.domain.remove
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Removes a domain of yours; every referral link and portal page on it stops working at once.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Removed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainRemoved'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/domains/{id}/check:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: checkReferralDomain
      tags:
        - Referrals
      summary: Check a domain's DNS records now
      description: |
        Looks at the domain's records now instead of waiting for the next check (pending domains
        are looked at every ten minutes, active ones every day), and moves it on when they are in
        place: the TXT record proves the domain is yours, the CNAME record routes it, and then the
        certificate is issued. At most once every 15 seconds per domain: a check asked sooner
        answers the domain as it stands. Nothing is ever removed by a check.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Checks a domain's DNS records now and, when they are in place, moves its setup on.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The domain, as it stands after the check.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/listing:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralListing
      tags:
        - Referrals
      summary: Get a program's directory listing
      description: |
        What the partner directory shows about the program, and whether it is listed. The
        directory is a page inside BoltClaw that only signed-in accounts can open; it lists only
        programs whose business switched listing on, and never shows who promotes a program or
        how many do. `missing` names every condition not met yet (an active program anybody may
        apply to, terms, a reward for partners, an organisation at least a week old and not
        suspended, and a real sale through your billing system or a finished setup checklist);
        listing is refused until it is empty. `preview` is the card partners will see. `state` is
        `listed`, `unlisted`, `held` (reports from three different accounts hid it until
        BoltClaw reviews it) or `delisted`. See [Recruiting partners](/docs/referral-growth).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The listing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListingSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralListing
      tags:
        - Referrals
      summary: Change a program's directory listing
      description: |
        Sets what the partner directory shows (`category`, `pitch` of at most 280 characters,
        `countries`, `languages`, the logo's `logo_path` from
        `POST /referrals/programs/{id}/listing/logo`) and whether the program is `listed`. The
        reward summary, cookie window and payout schedule come from the program itself, never
        from text you type. Turning `listed` on is `409` with every reason while a condition is
        not met. An AI assistant asks a person first, because listing publishes the program to
        every signed-in partner; your organisation may let assistants change it without asking.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.listing.update
      x-cross-tenant: 404
      x-agent-consequence: Changes what the partner directory shows about this program, or whether it is listed for every signed-in partner to find.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListingUpdate'
      responses:
        '200':
          description: The listing, as it stands now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListingSettings'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/listing/logo:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralListingLogoUpload
      tags:
        - Referrals
      summary: Get a URL to upload the listing's logo
      description: |
        A signed URL for one PNG, JPEG or WebP file of at most 2 MiB. `PUT` the file's bytes
        there with its content type, then save it with `PATCH /referrals/programs/{id}/listing`,
        sending the answer's `path` as `logo_path`. The file is accepted only if its bytes are an
        image of that type. `upload_url` is null in a replay of the same `Idempotency-Key`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Creates a one-time address to upload the program's directory logo to; nothing changes until the listing saves it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageUploadCreate'
      responses:
        '201':
          description: Where to upload the file.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageUpload'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/broadcasts:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralBroadcasts
      tags:
        - Referrals
      summary: List a program's broadcasts
      description: |
        The program's messages to its promoters, newest first (at most 50): drafts, ones being
        sent and sent ones, with who they went to and how many were emailed, muted or failed.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The broadcasts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createReferralBroadcast
      tags:
        - Referrals
      summary: Write a broadcast to a program's promoters
      description: |
        A draft, to everyone approved in the program, one group or level, or a list of up to 500
        promoters you pick. Nothing is sent until `POST /referrals/broadcasts/{id}/send`. The
        `body` is a small Markdown subset: paragraphs, **bold**, *italic*, `[links](https://...)`
        and lists; no HTML, no images, https links only, and every link is shown with its domain.
        `{name}`, `{link}` and `{code}` are filled with each promoter's own display name, referral
        link and discount code. Subject at most 120 characters, body at most 5,000.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Writes a draft message to this program's promoters; nothing is sent until it is sent.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastCreate'
      responses:
        '201':
          description: The draft.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Broadcast'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/broadcasts/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralBroadcast
      tags:
        - Referrals
      summary: Get one broadcast
      description: |
        One broadcast: its subject and Markdown, who it goes to, the hosts it links to, whether
        its current text was test-sent, and once sent, how many promoters it reached: emailed,
        muted (they still see it on their program page), your own users in the in-app panel
        (`referrers`, never emailed by BoltClaw), failed and skipped.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The broadcast.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Broadcast'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralBroadcast
      tags:
        - Referrals
      summary: Change a draft broadcast
      description: |
        Changes a draft's subject, body or audience. A sent broadcast is a record and is `409`:
        write a new one.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Changes a draft message to this program's promoters; nothing is sent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BroadcastUpdate'
      responses:
        '200':
          description: The draft.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Broadcast'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/broadcasts/{id}/test:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: testReferralBroadcast
      tags:
        - Referrals
      summary: Send yourself a test of a draft
      description: |
        Emails the draft, as it reads now, to the address of the person who made this API key
        and nobody else, with sample values in `{name}`, `{link}` and `{code}`. At most ten per
        draft, one every 15 seconds. The dashboard sends a broadcast only after a test of its
        current text.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Emails a test of this draft to the key's own creator only; no promoter receives anything.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The draft, with where the test went.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastTested'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/broadcasts/{id}/send:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: sendReferralBroadcast
      tags:
        - Referrals
      summary: Send a broadcast to a program's promoters
      description: |
        Sends the draft. Every approved promoter it names sees it on their program page; those
        with a BoltClaw account are emailed (unless they muted the program's messages: every email
        has a one-click unsubscribe), a few a minute; your own users in the in-app panel are never
        emailed by BoltClaw, so `referral.broadcast.sent` tells your webhook, and
        `GET /referrals/broadcasts/{id}/referrers` names them for your own message. A program may
        send 2 broadcasts in any 24 hours and 10 in any 30 days (`429` past either). An AI
        assistant always asks a person first: it messages people.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.broadcast.send
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Sends this message to the program's promoters; those with an account are emailed, and it cannot be taken back.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The broadcast, sending or sent.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Broadcast'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/broadcasts/{id}/referrers:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralBroadcastReferrers
      tags:
        - Referrals
      summary: List your own users a broadcast reached
      description: |
        The referrers (your own users, made by your server) a sent broadcast reached, 200 a page,
        by your own id for each (`external_id`), with their name, link and code, so your system can
        send them the message itself. BoltClaw emails none of them.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of referrers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BroadcastReferrerList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/imports/preview:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: previewReferralImport
      tags:
        - Referrals
      summary: Check a partner list before importing it
      description: |
        Checks up to 500 rows (as `rows`, or a `csv` with a header row: `name`, `email`, and
        optionally `code`, `group`, `rate`, `balance`, `currency`) against the program and writes
        nothing: rows that would fail and why (a code taken, a person already invited or
        promoting, a group that does not exist, a balance in another currency). Addresses are
        hashed on arrival and never stored. Send a larger file in parts. See
        [Recruiting partners](/docs/referral-growth).
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Checks a list of partners against the program without importing anything.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportRequest'
      responses:
        '200':
          description: What an import would do.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportPreview'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/imports:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralImports
      tags:
        - Referrals
      summary: List a program's imports
      description: The partner lists brought into the program, newest first, with how many people each invited and how many claimed.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The imports.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: commitReferralImport
      tags:
        - Referrals
      summary: Import a partner list
      description: |
        Makes an invite reserved for each row, all or nothing (`409` with the first row's reason
        when any fails: preview first). Each invite holds that person's vanity code (no one else
        can take it meanwhile), group, custom rate and opening balance. BoltClaw sends nobody an
        email: the answer has each person's personal link, and you send it from your own email.
        When they sign in through it they are approved (if your plan has room) with their code,
        group and rate, and an opening balance becomes a bonus paid by hand ("Balance brought
        over") only then. `import_id` continues an import begun by an earlier part of the same
        file. A file with opening balances also needs `referrals:payouts` on the key. An AI
        assistant always asks a person first. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.import.commit
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Imports a list of partners as personal invites, with any opening balances they are owed, paid when each person claims their invite.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportCommit'
      responses:
        '201':
          description: The import, with each person's link.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportCommitted'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/imports/{id}/invites:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralImportInvites
      tags:
        - Referrals
      summary: List an import's personal links
      description: |
        Everyone in one import with their personal link and whether they claimed it, 1,000 a page,
        for the file you send from your own email. The links are credentials (each joins one
        person), so this needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of invites.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportInviteList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/library:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralLibrary
      tags:
        - Referrals
      summary: List a program's file library
      description: |
        The images, text snippets and links partners see on their program page (an item with a
        `group_id` only by that group's), in order. An image item is shown once its file is in
        (`ready`).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The library.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LibraryItemList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createReferralLibraryItem
      tags:
        - Referrals
      summary: Add to a program's file library
      description: |
        An `image` (its file follows through `POST /referrals/library/{id}/upload`), a `snippet`
        of at most 2,000 characters whose `{link}` and `{code}` partners see filled with their
        own (with the program's disclosure line after it), or a `link` to a brand guide or press
        kit (https only; its domain is shown). `group_id` shows it to one group only; `in_panel`
        shows a snippet to your own users in the in-app panel too. At most 200 items a program.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Adds an image, a text snippet or a link to the program's file library, for partners to use.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LibraryItemCreate'
      responses:
        '201':
          description: The item.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LibraryItem'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/library/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateReferralLibraryItem
      tags:
        - Referrals
      summary: Change a library item
      description: |
        Changes an item's title, text, link, group, place in the order, or whether a snippet is in
        the panel. For an image, `image_path` saves the file uploaded through
        `POST /referrals/library/{id}/upload`; it is accepted only if its bytes are a PNG, JPEG or
        WebP image.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Changes an item partners see in the program's file library.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LibraryItemUpdate'
      responses:
        '200':
          description: The item.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LibraryItem'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteReferralLibraryItem
      tags:
        - Referrals
      summary: Delete a library item
      description: |
        The item and its file are gone; partners no longer see it. An AI assistant asks a person
        first, unless your organisation lets assistants do it without asking.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.library.delete
      x-cross-tenant: 404
      x-agent-consequence: Deletes an item from the program's file library; partners no longer see it.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LibraryItemDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/library/{id}/upload:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralLibraryUpload
      tags:
        - Referrals
      summary: Get a URL to upload a library image
      description: |
        A signed URL for an image item's file: one PNG, JPEG or WebP of at most 2 MiB (no SVG,
        HTML or PDF). `PUT` the bytes there with their content type, then save it with
        `PATCH /referrals/library/{id}` and the answer's `path` as `image_path`. `upload_url` is
        null in a replay of the same `Idempotency-Key`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Creates a one-time address to upload a library image to; nothing changes until the item saves it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageUploadCreate'
      responses:
        '201':
          description: Where to upload the file.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageUpload'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/link-hosts:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralLinkHosts
      tags:
        - Referrals
      summary: List the sites a program's links may go to
      description: |
        A partner's link can open any page of your site: `https://boltclaw.ai/r/<code>?to=/pricing`
        goes to your product URL's host plus `/pricing`, and `&host=docs.example.com` to another
        of your hosts. Those other hosts are the program's `allowed_hosts`, each used only once
        you prove it is yours with the TXT record shown here (`_boltclaw.<host>` with the value
        `bc-verify=<token>`), then `POST /referrals/programs/{id}/link-hosts/check`. Anything
        else falls back to the product URL: a link is never an open redirect.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The hosts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkHostList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/link-hosts/check:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: checkReferralLinkHost
      tags:
        - Referrals
      summary: Check a site's TXT record now
      description: |
        Looks up `_boltclaw.<host>` now for one of the program's allowed hosts; when it holds the
        value shown, links may go to that host. A check that finds the record gone takes that
        away again. At most once every 10 seconds per host.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Checks one of your sites' DNS records now, so the program's links may go to it once it is proven yours.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - host
              additionalProperties: false
              properties:
                host:
                  type: string
                  maxLength: 300
                  description: One of the program's allowed hosts.
      responses:
        '200':
          description: The hosts, after the check.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkHostList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/leaderboard:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralLeaderboard
      tags:
        - Referrals
      summary: Read a program's leaderboard
      description: |
        Every promoter with something counted, ranked by the program's leaderboard metric over its
        period (or the `metric` and `period` you ask for), with each one's membership id, name and,
        for one of your own users, your `external_id`. This is your view: promoters see only the
        first places, each named as its promoter chose (hidden, initials or their name), and money
        only when the board shows amounts. What counts is counted activity: a sale no person or
        rule holds, not refunded in full, not disputed; on a waitlist, confirmed friends. Counted
        again at most every 10 minutes. See [Campaigns](/docs/referral-campaigns).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: metric
          in: query
          required: false
          description: What to rank by. Left out, the program's own setting.
          schema:
            $ref: '#/components/schemas/BoardMetric'
        - name: period
          in: query
          required: false
          description: Over which window. Left out, the program's own setting.
          schema:
            $ref: '#/components/schemas/BoardPeriod'
      responses:
        '200':
          description: The leaderboard.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Leaderboard'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/contests:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralContests
      tags:
        - Referrals
      summary: List a program's contests
      description: |
        A program's contests, newest first, each with its prizes and what it is now (`scheduled`,
        `running`, `ended`, `awarded` or `cancelled`; the clock decides, not a job).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The contests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContestList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createReferralContest
      tags:
        - Referrals
      summary: Create a contest
      description: |
        A window (up to a year), a metric as a leaderboard's, who may enter (every promoter, or one
        group or level), a minimum to qualify, and prizes by place: cash (paid as a bonus by hand)
        or a reward that is not cash (credit, a gift card, a free period or a perk). Standings count
        only what happens inside the window. Nothing is paid until a person awards the prizes after
        it ends. At most 10 contests a program may be scheduled or running at once.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Creates a contest with prizes the program's promoters compete for; nothing is paid until a person awards them.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContestCreate'
      responses:
        '201':
          description: The contest.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contest'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/contests/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralContest
      tags:
        - Referrals
      summary: Read a contest and its standings
      description: |
        The contest, its standings (each promoter's place among those who reached the minimum,
        whether they are under review, and the prize the place wins as things stand) and whether
        the prizes may be awarded now (`award.possible`), and if not, why and from when.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The contest.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContestDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralContest
      tags:
        - Referrals
      summary: Change a contest
      description: |
        Before it starts, anything. Once it has started, only its name and description (`409` for
        anything else): its window, what it counts, who may enter, its minimum and its prizes stay
        as promoters saw them. Once it is awarded or cancelled, nothing (`409`).
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Changes a contest's name, dates or prizes, within what may still change once it has started.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContestUpdate'
      responses:
        '200':
          description: The contest.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contest'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/contests/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: cancelReferralContest
      tags:
        - Referrals
      summary: Cancel a contest
      description: |
        Ends a contest that was not awarded, with a reason promoters in it are shown. No prize is
        paid. An AI assistant always asks a person first. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.contest.cancel
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Cancels this contest; no prize is paid, and the promoters competing in it see the reason.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
      responses:
        '200':
          description: The contest, cancelled.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contest'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/contests/{id}/award:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: awardReferralContest
      tags:
        - Referrals
      summary: Award a contest's prizes
      description: |
        Gives each winning place its prize, once: a cash prize as a bonus paid by hand ("Contest:
        <name>, 1st place"), approved at once and paid with the next payout; any other as a reward
        ready to give. Refused (`409`, with the reason) before the contest ends, while any winner is
        under review, and until the last hold on what the winners counted has passed
        (`award.awardable_at` on the contest). BoltClaw never awards by itself. Needs the
        `referrals.payouts` permission. A key that is not an AI assistant's must also hold the
        `referrals:payouts` scope (`403 INSUFFICIENT_SCOPE` otherwise), as an import with opening
        balances must; an AI assistant always asks a person first, and the person who approves may
        not be one of the winners. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.contest.award
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Pays the contest's prizes to its winning places, cash prizes as bonuses with the next payout; it cannot be done twice.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The contest, awarded, and what each place was given.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContestAward'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/entries:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralEntries
      tags:
        - Referrals
      summary: List a waitlist's entries
      description: |
        A waitlist program's line, in place order: each entry's place (null until they confirm),
        confirmed friends, whether they are disqualified and why, and who referred them. Places move
        as friends confirm, so a page is the line as it was when read. No address is ever in an
        entry: people who joined on the hosted page are exported to CSV in the dashboard only.
        `filter` is `all` (default), `confirmed`, `unconfirmed`, `disqualified` or `invited`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: filter
          in: query
          required: false
          schema:
            type: string
            enum:
              - all
              - confirmed
              - unconfirmed
              - disqualified
              - invited
        - $ref: '#/components/parameters/Limit200'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of entries.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEntryList'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: upsertReferralEntry
      tags:
        - Referrals
      summary: Add a person to a waitlist, or update them
      description: |
        Your own sign-up form's call: your id for the person (`external_id`), the referral code
        they arrived with if any, and later `verified: true` once they confirmed their address
        (that cannot be taken back). `201` for a new entry, `200` for an existing one. An entry
        counts toward the organisation's referrers, never its partners. An `email`, when you send
        one, is kept only as a keyed fingerprint for the self-referral check: never stored or
        returned. A key may create at most a few hundred new entries an hour (`429` past it).
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Adds one of your own users to the waitlist, or updates their entry; it sends nobody anything.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WaitlistEntryUpsert'
      responses:
        '200':
          description: The entry, updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEntry'
        '201':
          description: The entry, new.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/entries/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralEntry
      tags:
        - Referrals
      summary: Read a waitlist entry
      description: One entry, with the person's own view of their place (`state`), as the in-app panel shows it to them.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The entry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEntryDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralEntry
      tags:
        - Referrals
      summary: Confirm an entry, or set its status
      description: |
        `verified: true` once the person confirmed their address (it moves the friend who referred
        them up; it cannot be taken back); `status` is `waiting`, `invited` or `joined`, for your own
        record. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Confirms a waitlist entry or changes its status; confirming moves the friend who referred them up the line.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WaitlistEntryUpdate'
      responses:
        '200':
          description: The entry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEntry'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/entries/{id}/disqualify:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: disqualifyReferralEntry
      tags:
        - Referrals
      summary: Disqualify a waitlist entry
      description: |
        Takes the entry out of the line (it keeps its row) and stops it moving anybody up, with a
        reason the person never sees. An AI assistant always asks a person first. Recorded in the
        activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.waitlist.disqualify
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Takes this person out of the waitlist's line, and their friends stop counting for them.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
      responses:
        '200':
          description: The entry.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/entries/{id}/requalify:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: requalifyReferralEntry
      tags:
        - Referrals
      summary: Put a disqualified entry back in line
      description: Undoes a disqualification, with a reason. The entry takes its place by its join order again. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Puts a disqualified person back in the waitlist's line.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
      responses:
        '200':
          description: The entry.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/entries/invite:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: inviteReferralEntries
      tags:
        - Referrals
      summary: Invite waitlist entries
      description: |
        Marks up to 500 entries `invited` and emails the ones who joined on the hosted page your
        message (a subject of one line, at most 140 characters, and a body in the broadcasts'
        Markdown subset). Their addresses never leave BoltClaw. An entry from your own form is
        marked only, for your own system to email. An AI assistant always asks a person first.
        Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.waitlist.invite
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Marks these waitlist entries invited and emails your message to the ones who joined on the hosted page.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WaitlistInvite'
      responses:
        '200':
          description: How many were invited and emailed.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistInvited'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/second-tier:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralSecondTier
      tags:
        - Referrals
      summary: Read a program's second tier
      description: |
        Whether a partner earns a share of what the partners they recruit earn, and how much: at
        most 20% of the recruit's own cash commission on sales, for 1 to 24 months after the
        recruit joined, one level only, never on a partner's own purchases. With the clause the
        terms carry while it is on, and why it is fenced. See [Two-tier](/docs/referral-campaigns#two-tier).
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The second tier.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SecondTierSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralSecondTier
      tags:
        - Referrals
      summary: Switch a program's second tier on or off
      description: |
        `enabled: true` with `percent_bps` (1 to 2000) and `months` (1 to 24) pays a recruiter that
        share of each recruit's cash commission on sales, from the next sale on; the clause is added
        to the program's terms and every approved partner is asked to accept the new version before
        their next payout. `enabled: false` stops it (what was earned stays). Off by default.
        Switching it on is always asked first for an AI assistant. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.tier.update
      x-boltclaw-approval-always: true
      x-boltclaw-approval-when: enabled is true
      x-cross-tenant: 404
      x-agent-consequence: Turns the second tier on or off; on, recruiters earn a share of their recruits' commissions and every partner must accept the new terms.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SecondTierUpdate'
      responses:
        '200':
          description: The second tier.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SecondTierSettings'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/waitlist/end:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: endReferralWaitlist
      tags:
        - Referrals
      summary: End a waitlist
      description: |
        Ends the waitlist for good: nobody new can join, through the hosted page or through
        `POST /referrals/programs/{id}/entries`, and everybody already in keeps their place and
        may still be updated or invited. The addresses the hosted page holds are deleted 90 days
        after the end (`POST /referrals/programs/{id}/waitlist/forget` deletes them now); once an
        address is gone, its entrant's own link stops working. Ending again keeps the first end.
        A program that is not a waitlist is `409`. To stop new entries for a while instead, set
        `waitlist.closed` on the program. An AI assistant always asks a person first. Recorded in
        the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.waitlist.end
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Ends the waitlist for good, so nobody new can join; the addresses its hosted page holds are deleted 90 days later.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: When the waitlist ended (`forgotten` is 0).
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEnded'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/waitlist/forget:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: forgetReferralWaitlist
      tags:
        - Referrals
      summary: Delete a waitlist's addresses now
      description: |
        Deletes every address the waitlist's hosted page holds, and every entrant's own link, now,
        and ends the waitlist if it had not ended (as `POST /referrals/programs/{id}/waitlist/end`
        does). Entries stay in the line by your own `external_id`; nothing can be emailed to them
        any more. It cannot be undone. A program that is not a waitlist is `409`. An AI assistant
        always asks a person first. Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.waitlist.forget
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Deletes every address the waitlist's hosted page holds, now and for good, and ends the waitlist.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: When the waitlist ended, and how many entries' addresses were deleted by this call.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistEnded'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/waitlist/rotate-page:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: rotateReferralWaitlistPage
      tags:
        - Referrals
      summary: Give the hosted waitlist page a new address
      description: |
        Makes a new random address for the waitlist's hosted page and stops the old one at once,
        for a page whose address was posted somewhere it should not be or is being flooded.
        Referral links (`/r/<code>`) keep working: they always send people to the current address.
        Update the address wherever you published it. An AI assistant always asks a person first: a
        new address stops the one already published.
        Recorded in the activity log.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.waitlist.rotate
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Replaces the hosted waitlist page's address; the old address stops answering at once.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The page's new address.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaitlistPageAddress'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/codes:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralCodes
      tags:
        - Referrals
      summary: List a program's discount codes
      description: |
        Every partner discount code of the program, newest first (up to 500, in one page), and the
        discount a friend gets with one (`friend_discount`, from the program's friend's discount
        rule, or null when it gives none). A shop's own system reads it to keep its coupons in step:
        Bolt Claw's WooCommerce plugin reads it every hour. A code that is not `active` was switched
        off: switch it off in your shop too. `stripe_promotion_code` and `shopify_discount` are the
        code in your own Stripe or in the program's linked Shopify shop, once it is one there.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The codes, and the friend's discount.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscountCodeList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/invites:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralInvites
      tags:
        - Referrals
      summary: List a program's invites
      description: |
        Every partner invite of the program and whether it is still usable, newest first, a page
        at a time. An invite's link is never shown again here: whoever holds it may join, so it is
        shown once, when the invite is made.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The invites, without their links.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteSummaryList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createReferralInvite
      tags:
        - Referrals
      summary: Create an invite to a program
      description: |
        A link that lets one person (or up to `max_uses` people) join the program as a partner,
        optionally until `expires_at` (within a year). In an invite-only program it is the only
        way in. Bolt Claw emails nobody: send the link from your own system, a Zap or a message.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Creates a link that lets whoever holds it join your program as a partner; it is not available to AI assistants, and this call is refused for an assistant's key.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteCreate'
      responses:
        '201':
          description: The invite, with its link.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invite'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/shopify-codes:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: syncReferralShopifyCodes
      tags:
        - Referrals
      summary: Create the program's codes in its Shopify shop
      description: |
        Keeps every partner code of the program in step with the Shopify shop linked to it through
        Bolt Claw's Shopify app: an active code becomes a discount in the shop, at the friend's
        discount (percent or amount off, once per customer, or for that many months of a
        subscription), and a code switched off in Bolt Claw is switched off in the shop. A code the
        shop already has with the same text is matched, not made again; running this twice makes
        nothing twice. At most 50 codes a call: `remaining` says how many wait for the next one.
        With no linked shop, or no friend's discount rule, nothing is made and `reason` says why.
        Linking a shop is done from the shop's own Shopify admin, never through this API. Recorded
        in the activity log. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.shopify.codes
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Creates or switches off discount codes in your own Shopify shop, which changes what your customers pay at checkout.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: What was made, matched and switched off.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShopifyCodesResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/privacy-requests:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralPrivacyRequests
      tags:
        - Referrals
      summary: List a program's privacy requests
      description: |
        What Shopify's privacy webhooks asked of this program, newest first (up to 200, in one
        page): a customer asking for their data (`data_request`, waiting for you to answer it within
        30 days of `received_at`, so `due_by`), and the erasures Bolt Claw did when Shopify asked
        (`customer_redact`, `shop_redact`), with counts only. Read a data request to see what Bolt
        Claw holds, give it to your customer, then mark it answered.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrivacyRequestList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/privacy-requests/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralPrivacyRequest
      tags:
        - Referrals
      summary: Read a privacy request
      description: |
        One request, and for a data request still waiting, everything Bolt Claw holds about the
        customer it named in this program (`data`): your customer id, who referred them, their
        sales, refunds and rewards, and the referral link they were given. Bolt Claw holds no name,
        email address, postal address or phone number, and `data.note` says so in words you can pass
        on. Each read of `data` is recorded in the activity log.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The request, and what Bolt Claw holds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrivacyRequestDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/privacy-requests/{id}/answer:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: answerReferralPrivacyRequest
      tags:
        - Referrals
      summary: Mark a privacy request answered
      description: |
        Says you gave your customer what Bolt Claw holds about them. The Shopify customer and order
        ids the request named are forgotten in the same step, and the request reads `answered`.
        Only a waiting data request is answered (`400` otherwise). Recorded in the activity log.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Marks a customer's data request answered and forgets the Shopify ids it named.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The request, answered.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrivacyRequest'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/data-subjects/{external_id}:
    parameters:
      - $ref: '#/components/parameters/SubjectExternalId'
    get:
      operationId: getReferralDataSubject
      tags:
        - Referrals
      summary: Look up what is held about one of your people
      description: |
        Answers a privacy request from one of your own customers or in-app referrers: everything
        Bolt Claw holds about them in this workspace, by the id your system uses for them (the
        `external_id` you sent with their conversions or when you made them a referrer, or the
        payment processor's customer id). For a customer: each program they were referred in, their
        conversions and the rewards they were given. For a referrer: their display name, status,
        links, commissions and rewards. An email fingerprint is shown only as held or not, never its
        value; Bolt Claw holds no name, email address, postal address or phone number of a customer.
        Each lookup is recorded in the activity log, with counts only. Nobody with that id is `404`.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: Everything held about the person in this workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataSubject'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: eraseReferralDataSubject
      tags:
        - Referrals
      summary: Erase one of your people's referral data
      description: |
        Erases a customer's or an in-app referrer's referral data at their request, in every program
        of this workspace. Their ids and fingerprints are removed and cannot come back; amounts,
        commissions and payouts stay as anonymous rows, because your books need them. A referrer's
        name, payout details and panel sessions go and their links stop counting. Copies of webhook
        bodies that carried the id are scrubbed. Recorded in the activity log, with counts only. An AI
        assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.subject.erase
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Permanently erases a customer's or referrer's referral data in every program; the amounts stay on record without them.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Erased, with what was erased counted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataSubjectErased'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/terms/draft:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: draftReferralProgramTerms
      tags:
        - Referrals
      summary: Draft program terms
      description: |
        Writes the program's terms from Bolt Claw's template: you answer a few questions (your
        business's legal name and contact address, who may join, the minimum age, countries left
        out, how many months money held for a missing tax form waits before it may be forfeited, the
        governing law and courts) and every other fact is read from the program's own settings: the
        cookie window, attribution, the hold, the minimum payout, the schedule, tax forms, invoices,
        self-billing, the second tier, levels and the disclosure line. Nothing is saved. To use the
        text, send it as `terms` to `PATCH /v1/referrals/programs/{id}`. The text ends with a notice
        that it is based on a template and is not legal advice. See [Program terms](/docs/referral-terms).

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TermsDraftRequest'
      responses:
        '200':
          description: The drafted terms, and the settings they were read from.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TermsDraft'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/export:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: exportReferralReport
      tags:
        - Referrals
      summary: Export a report as a CSV download link
      description: |
        A single-use link to one of the program's reports as a CSV file, valid for ten minutes:
        `commissions` (each with the sentence saying why it is that amount), `conversions`,
        `partners` (partners and your own in-app referrers; an email only when the partner chose to
        share it with you), `payouts`, `grants` (rewards that are not cash) or `contests` (each
        contest's standings and prizes). Filter by `from` and `to` (a date like `2026-10-01`, or an
        instant) and by `status` (for conversions, the event). The file is never in the response:
        open `url` within ten minutes, once. It is built when it is opened, from what is true then,
        and only if the key that asked is still live and its creator may still read the report.

        At most 25,000 rows a file (narrow the dates for more; a file cut there says so in its
        name), and 60 links an hour for each key (a platform setting; the dashboard counts each
        person apart, so an assistant never uses up yours). Recorded in the activity log with the
        filters, never a row.

        Needs `referrals:read`. The `payouts` report also needs `referrals:payouts` (which no AI
        assistant holds) and the permission to pay partners.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      x-agent-consequence: Makes a ten-minute, single-use download link to this program's report; nothing changes.
      parameters:
        - name: type
          in: query
          required: true
          schema:
            type: string
            enum:
              - commissions
              - conversions
              - partners
              - payouts
              - grants
              - contests
        - name: from
          in: query
          required: false
          description: Rows at or after this date (`2026-10-01`) or instant.
          schema:
            type: string
            maxLength: 40
        - name: to
          in: query
          required: false
          description: Rows before this date or instant.
          schema:
            type: string
            maxLength: 40
        - name: status
          in: query
          required: false
          description: 'One status of that report''s rows: a commission''s, a partner''s, a payout''s, a grant''s or a contest''s; for `conversions`, the event (`lead`, `sale`, `refund`, `dispute`, `bonus`).'
          schema:
            type: string
            maxLength: 20
      responses:
        '200':
          description: The link, once. Only its fingerprint is kept.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportLink'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/pause:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: pauseReferralProgram
      tags:
        - Referrals
      summary: Pause a program
      description: |
        Stops a program taking new clicks, signups and sales: its links still open your site, but
        nothing new is credited until it is resumed. What partners already earned is kept, and
        payouts of it go on. A stop, so it is never asked first and keeps working during
        maintenance.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Pauses this program; its links stop crediting new clicks and sales until it is resumed.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The program, paused.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/resume:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: resumeReferralProgram
      tags:
        - Referrals
      summary: Resume a program
      description: |
        Starts a paused or archived program again: its links credit clicks, signups and sales from
        now on. Nothing that happened while it was paused is credited afterwards. An AI assistant
        resumes a paused program at once; bringing back an archived one asks a person first
        (archiving is a person's decision, and so is undoing it).

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.program.restore
      x-boltclaw-approval-always: true
      x-boltclaw-approval-when: the program is archived
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Resumes this program; its links credit new clicks and sales again.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The program, active.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/archive:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: archiveReferralProgram
      tags:
        - Referrals
      summary: Archive a program
      description: |
        Ends a program: it is frozen, its links stop crediting anything, partners can no longer
        join, and it no longer counts against your plan's programs. What partners earned is kept
        and can still be paid. Resuming it starts it again. An AI assistant always asks a person
        first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.program.archive
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Archives this program; every partner's link stops crediting, and nobody can join until it is resumed.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The program, archived.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/signup-link/rotate:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: rotateReferralSignupLink
      tags:
        - Referrals
      summary: Give a program a new signup link
      description: |
        Makes a new signup link for the program and stops the old one at once, for a link that was
        posted somewhere it should not be. Partners already in the program, and their referral
        links, are not affected; invites stay valid. An AI assistant always asks a person first: a
        new link stops the one already shared.
        Recorded in the activity log.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.signup.rotate
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Replaces this program's signup link; the old link stops letting anyone apply.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The program, with its new signup link.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/ban:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: banReferralPartner
      tags:
        - Referrals
      summary: Ban a partner
      description: |
        Shuts a partner out of the program: their links and codes stop crediting, and what they are
        owed and not yet paid is not paid. Only a person decides this; a fraud rule never bans
        anybody. An AI assistant always asks a person first. Recorded in the activity log.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.partner.ban
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Bans this partner; their links stop crediting and their unpaid commissions are not paid.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The partner, banned.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Partner'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/unban:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: unbanReferralPartner
      tags:
        - Referrals
      summary: Lift a partner's ban
      description: |
        Lets a banned partner back into the program, approved. Their links and codes credit again
        from now on. An AI assistant always asks a person first, as for approving a partner; the
        partner themselves can never be the person who decides. Recorded in the activity log.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.partner.unban
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Lets this banned partner back into the program; their links credit again from now on.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The partner, approved again.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Partner'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/codes:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralCode
      tags:
        - Referrals
      summary: Give a partner a discount code
      description: |
        A discount code that credits this partner when a customer types it at checkout. Leave
        `code` out for the program's suggestion (its code prefix and the partner's name). For
        approved partners only; a code is unique in the program, ignoring case. When the program
        has a friend's discount and your Stripe is connected, the code is also made as a
        promotion code in your Stripe, and in a Shopify shop linked to the program; otherwise it
        only tells BoltClaw which partner a code belongs to. An AI assistant always asks a person
        first (never the partner it is for): a code changes what customers pay and who is paid.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.code.create
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Gives this partner a discount code that credits them when a customer types it, made in your own Stripe or Shopify shop when they are connected.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CodeCreate'
      responses:
        '201':
          description: The code.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscountCode'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/codes/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateReferralCode
      tags:
        - Referrals
      summary: Link a code to a Stripe promotion code
      description: |
        Tells BoltClaw which promotion code in your own Stripe this code is (`promo_…`), so a sale
        where a customer typed it at Stripe Checkout is credited even before Stripe's own events
        taught BoltClaw. `null` forgets it. Usually unnecessary: BoltClaw learns it from Stripe's
        promotion-code events. An AI assistant always asks a person first, and the approval page
        names the promotion code: every sale that uses it is credited to this partner, your own
        campaign codes included.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.code.link
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Records which Stripe promotion code this discount code is; every sale that uses that promotion code is credited to this code's partner.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CodeUpdate'
      responses:
        '200':
          description: The code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscountCode'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/codes/{id}/deactivate:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: deactivateReferralCode
      tags:
        - Referrals
      summary: Switch a discount code off
      description: |
        Stops a code crediting its partner: a sale where a customer types it is no longer
        attributed. What it earned before is kept. A code switched off stays off; give the partner a
        new one instead. A stop, so it is never asked first and keeps working during maintenance.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Switches this discount code off; sales with it are no longer credited to its partner.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The code, switched off.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscountCode'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/links:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralLinks
      tags:
        - Referrals
      summary: List a program's referral links
      description: |
        Every referral link of the program (`/r/<code>`), or one partner's with `partner_id`: the
        partner's main link and the links BoltClaw placed for them (a bio block, a post, an
        automation, shown by type only), archived ones included. Newest first, a page at a time.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: partner_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The links.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralLinkList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/links/{id}/archive:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: archiveReferralLink
      tags:
        - Referrals
      summary: Archive a referral link
      description: |
        Stops one of a partner's links crediting new clicks (it still opens your site). What it
        earned is kept. An AI assistant asks a person first unless your organisation lets
        assistants archive links without asking.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.link.archive
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Archives this partner's referral link; it stops crediting new clicks.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The link, archived.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralLink'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/invites/{id}/revoke:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: revokeReferralInvite
      tags:
        - Referrals
      summary: Revoke an invite
      description: |
        Stops an invite letting anyone join. People who already joined with it stay. A stop, so it
        is never asked first and keeps working during maintenance.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Revokes this invite; nobody else can join with it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The invite, revoked, without its link.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/commissions/{id}/approve:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: approveReferralCommission
      tags:
        - Referrals
      summary: Approve a commission before its hold ends
      description: |
        Makes a pending commission payable now, before the program's refund window ends, with your
        reason. A refund after this still takes it back from the next payout. Not for a commission
        a fraud flag is holding: decide the flag instead. The permission to pay partners is
        needed; a key that is not an AI assistant's also needs `referrals:payouts`, and an AI
        assistant always asks a person first (never the partner it pays). Recorded in the activity
        log with the reason.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.commission.approve
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Makes this commission payable now, before its refund window ends.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommissionApproveRequest'
      responses:
        '200':
          description: The commission, approved.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Commission'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/payouts/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: cancelReferralPayout
      tags:
        - Referrals
      summary: Cancel a payout
      description: |
        Takes an unpaid payout out of its batch, as `cancelled` (or `failed`, when you tried to pay
        it and the money did not arrive). Its commissions go back to Ready to pay, so nothing a
        partner earned is lost. A payout a provider is still handling is `409` until the provider
        answers. The permission to pay partners is needed. Cancelling a draft is a stop. Marking
        one `failed`, or cancelling one that was exported or sent, lets its commissions be paid
        again (it may already have been paid by hand): an AI assistant asks a person first, who
        decides at a two-step session and is never the partner it pays, and a key needs
        `referrals:payouts` for it. Recorded in the activity log.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.payout.reopen
      x-boltclaw-approval-always: true
      x-boltclaw-approval-when: the payout is not a draft (it was exported or sent)
      x-cross-tenant: 404
      x-agent-consequence: Cancels this unpaid payout, or marks it failed; its commissions go back to Ready to pay and may be paid again.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayoutCancel'
      responses:
        '200':
          description: The payout.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payout'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/levels/recalculate:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: recalculateReferralLevels
      tags:
        - Referrals
      summary: Recalculate partners' levels now
      description: |
        Moves the program's partners to the highest level they qualify for, now, instead of at the
        daily run: never down past a level that keeps what was earned, and never a partner you
        pinned. Answers how many moved. Each move is recorded in the activity log.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Moves this program's partners to the levels they qualify for now, as the daily run would.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: How many partners moved.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LevelRecalculation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/rewards:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralRewards
      tags:
        - Referrals
      summary: List a program's reward rules
      description: |
        Every reward rule of the program: for everyone, for a group or level, and for single
        partners, with the conditions each applies under and a one-sentence `summary`. See
        [Reward rules](/docs/rewards) for how one rule is picked for each sale.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The program's rules.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RewardList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createReferralReward
      tags:
        - Referrals
      summary: Add a reward rule
      description: |
        A rule for everyone (`scope` `program`), a group or level, or one partner, with conditions
        on a sale's products, prices, plans, intervals, first payment or renewal, currency and
        amount. Each audience may have one rule with no conditions per event (`409` for a second).
        A `bonus` is a flat amount paid once per window when a partner crosses its `milestone`.
        A program holds at most 200 rules. The new rule applies to sales from now on.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Adds a reward rule; matching sales from now on pay partners by it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RewardCreate'
      responses:
        '201':
          description: The rule.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Reward'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/rewards/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateReferralReward
      tags:
        - Referrals
      summary: Update a reward rule
      description: |
        Changes the fields you send, in place. A rule's event and audience are fixed: add a new
        rule for a different one. What was already earned is a stored amount and never changes;
        the new terms apply to sales from now on.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Changes what a reward rule pays on sales from now on.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RewardUpdate'
      responses:
        '200':
          description: The rule.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Reward'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteReferralReward
      tags:
        - Referrals
      summary: Delete a reward rule
      description: |
        Removes the rule. What it already earned stays earned; it stops paying from now on, and
        the next most specific rule pays instead. An AI assistant's key is always asked first.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.reward.delete
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Deletes a reward rule; partners stop earning by it from now on.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/groups:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralGroups
      tags:
        - Referrals
      summary: List a program's groups and levels
      description: |
        Plain groups you place partners in, and levels: groups with a rank and a minimum that the
        daily level job moves partners into. Each with how many partners are in it now.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The program's groups and levels.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createReferralGroup
      tags:
        - Referrals
      summary: Add a group or level
      description: |
        A plain group (no `rank`), or a level (`rank` 1 to 100, and above the first a `minimum`).
        Give it its own rates with `POST /referrals/programs/{id}/rewards` and `scope` `group`.
        Names and ranks are unique in a program; a program holds at most 50 groups and levels.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Adds a partner group or level to the program.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupCreate'
      responses:
        '201':
          description: The group.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/groups/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateReferralGroup
      tags:
        - Referrals
      summary: Update a group or level
      description: Changes the fields you send. A minimum is only for a level; a group without a rank has none.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Changes a partner group or level, which can change who the level job moves.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupUpdate'
      responses:
        '200':
          description: The group.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteReferralGroup
      tags:
        - Referrals
      summary: Delete a group or level
      description: |
        Removes the group and its own rules. Its partners stay in the program, in no group, and
        what they earned stays earned. An AI assistant's key is always asked first.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.group.delete
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Deletes a partner group or level and its rates; its partners fall back to the program's rates.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateReferralPartner
      tags:
        - Referrals
      summary: Move a partner to a group
      description: |
        Puts the partner in a group or level of their program (`group_id`), or in none (`null`).
        `pinned: true` keeps the daily level job from moving them again.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Moves a partner to another group or level, which changes the rates they earn from now on.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerUpdate'
      responses:
        '200':
          description: The partner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Partner'
        '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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/bonuses:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralBonus
      tags:
        - Referrals
      summary: Pay a partner a bonus
      description: |
        A flat fee or one-off bonus by hand ("200.00 for the launch video"), in the program's
        currency, with a reason the partner reads. It holds for `hold_days` (0 by default, so it
        is ready to pay at once), then is paid like any commission. Only an approved partner can
        be paid one. Recorded in the activity log, and announced as `referral.bonus.created`.
        An AI assistant's key is always asked first.

        `Idempotency-Key` is required: a retried network timeout must never pay a partner twice.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.commission.bonus
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Adds money the partner will be paid; it is audited and the partner is told.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BonusCreate'
      responses:
        '201':
          description: The bonus, as a commission.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Commission'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/embed-tokens:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: createReferralEmbedToken
      tags:
        - Referrals
      summary: Create a referral panel session for one of your users
      description: |
        Call this from YOUR server when a signed-in user opens the page that shows their referrals.
        It makes the user a referrer in the program if they are not one yet (by the `external_id`
        you send, which is your own id for that user), and returns a short-lived token your page
        hands to the Bolt Claw panel: `BoltClawReferrals.mount(element, { token, embedKey })`. See
        [Referral panel](/docs/referral-panel).

        The token is shown once, in this response, and is the only thing that ever goes to the
        browser. It works for that one referrer in that one program, lives `ttl_seconds` (60 to
        86400, default 3600), and ends early if you call `POST /referrals/programs/{id}/embed/revoke-sessions`
        or the referrer is erased. A replay of the same `Idempotency-Key` returns `token: null`:
        mint a new one instead.

        Send `mode: "preview"` and no `referrer` for a session that shows sample data, to try your
        look and copy before launch.

        Needs `referrals:embed` (the "Referral panel" key preset), or `referrals:write`. Never put
        this key in a browser or a mobile app, and an AI assistant's key can never mint a session.
        The panel must be switched on for the program (`embed_enabled`), or the answer is `409`.
      x-scope: referrals:embed
      x-any-scopes:
        - referrals:embed
        - referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Creates a short-lived credential that a person's browser will hold; it is not available to AI assistants, and this call is refused for an assistant's key.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmbedTokenCreate'
      responses:
        '200':
          description: The session. `token` is shown here and nowhere else.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbedTokenCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/referrers:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralReferrers
      tags:
        - Referrals
      summary: List a program's referrers
      description: |
        The people you gave a referral link through the panel or the API, newest first, with what
        each link has done. A referrer is one of YOUR users; they have no Bolt Claw account and
        are not counted as partners. `search` matches the start of your `external_id`.

        Needs `referrals:read`, or `referrals:embed`.
      x-scope: referrals:read
      x-any-scopes:
        - referrals:read
        - referrals:embed
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/ReferrerStatus'
        - name: search
          in: query
          required: false
          description: The start of the `external_id`, at most 200 characters.
          schema:
            type: string
            maxLength: 200
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of referrers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferrerList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: upsertReferralReferrer
      tags:
        - Referrals
      summary: Create or update a referrer
      description: |
        Makes one of your users a referrer, or updates the one your `external_id` already names:
        `201` when it was created, `200` when it already existed. Only the fields you send change;
        send `null` to clear one. The `email`, if you send one, is only used to recognise the
        same person again and is never stored, returned or shown to anyone. A new referrer is
        announced as `referral.referrer.created`.

        `code` is the vanity part of their link (`/r/jane`), 3 to 40 lowercase letters and digits
        with single hyphens, chosen only when the referrer is created: after that it is already
        in links they have shared, and sending one is `409`. `409` also when the code is taken.

        You do not need this before minting a panel session, which creates the referrer itself;
        use it to create referrers ahead of time or to keep their name current.

        Needs `referrals:embed`, or `referrals:write`. A referrer past the plan's allowance is
        `402`.
      x-scope: referrals:embed
      x-any-scopes:
        - referrals:embed
        - referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Creates or updates a referrer, a person who then holds a referral link in the program.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReferrerUpsert'
      responses:
        '200':
          description: The referrer already existed; the fields you sent are updated.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferrerView'
        '201':
          description: The referrer was created.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferrerView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/referrers/{external_id}:
    parameters:
      - $ref: '#/components/parameters/Id'
      - $ref: '#/components/parameters/ExternalId'
    get:
      operationId: getReferralReferrer
      tags:
        - Referrals
      summary: Read one referrer's panel
      description: |
        Exactly what the panel shows this referrer, as data: their link, the offer, their numbers,
        a history row for each friend (up to 50, newest first) with a stable `reason_code`, how
        they are paid, and the share text. Use it to build your own screen. `{external_id}` is
        your own id for the user and must be percent-encoded.

        The View has no names, emails or ids of your customers: a friend is "Friend 3". An erased
        referrer, or one that does not exist, is `404`.

        Needs `referrals:read`, or `referrals:embed`.
      x-scope: referrals:read
      x-any-scopes:
        - referrals:read
        - referrals:embed
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The referrer's panel View.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferrerPanelView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/referrers/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateReferralReferrer
      tags:
        - Referrals
      summary: Approve or ban a referrer
      description: |
        Sets a referrer's `status` to `approved` or `banned`. `{id}` is the referrer's `id` from
        `ReferrerView`, not your `external_id`. A banned referrer earns nothing from then on and
        keeps what was already earned; approving one again can be `402` if the plan's referrer
        allowance is full. An erased referrer cannot be changed (`409`). An AI assistant bans one at
        once; approving one asks a person first, as letting a banned partner back in does.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.referrer.approve
      x-boltclaw-approval-always: true
      x-boltclaw-approval-when: status is approved
      x-cross-tenant: 404
      x-agent-consequence: Approves or bans a referrer, which decides whether their link earns rewards from now on.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReferrerStatusUpdate'
      responses:
        '200':
          description: The referrer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferrerView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimit'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: eraseReferralReferrer
      tags:
        - Referrals
      summary: Erase a referrer
      description: |
        Erases a person's referral data on their request. The name, the payout details, the
        recognition of their address and their panel sessions are removed and cannot come back;
        what they earned stays in the commission and payout records as an anonymous row, because
        your books need it. Their link stops counting. `{id}` is the referrer's `id` from
        `ReferrerView`. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.referrer.erase
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Permanently erases a referrer's name and payout details and ends their link; what they earned stays on record without them.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: Erased.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/embed/revoke-sessions:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: revokeReferralEmbedSessions
      tags:
        - Referrals
      summary: End every live panel session
      description: |
        Every panel token minted for this program stops working at once, wherever it is held; each
        open panel shows "This link to your referrals has expired" and asks your page for a new
        one. Use it if a token may have leaked. Sessions you mint afterwards work as usual.
        `embed.generation` in the program goes up by one.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Ends every open referral panel session for the program; people see the panel as expired until your site mints a new one.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The program, with its new `embed.generation`.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/embed/rotate-key:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: rotateReferralEmbedKey
      tags:
        - Referrals
      summary: Rotate the panel's public key
      description: |
        Gives the program a new public `embed.key`. The key is not a secret (it is in your page
        and in the panel's address) but it names the panel's settings, so rotating it makes every
        install that still carries the old one stops showing the panel until you update `embedKey`
        there. Sessions already minted are not ended by a rotation; to end those as well, call
        `POST /referrals/programs/{id}/embed/revoke-sessions`. An AI assistant always asks a person
        first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.embed.rotate
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Replaces the panel's public key; every page that still embeds the old key stops showing the panel until it is updated.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The program, with its new `embed.key` and `embed.panel_url`.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Program'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/grants:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralGrants
      tags:
        - Referrals
      summary: List a program's rewards that are not cash
      description: |
        The free months, credit, perks, gift cards and friend's discounts the program has given or
        owes, newest first. Each says what it is (`summary`), whose it is (`recipient`: one of your
        own users with a link by your `external_id`, an affiliate by their name in the program, or
        the friend by your id for them), and where it stands.

        A grant waits out the program's hold as `pending`, then is `ready`: give it in your own
        system and say so with `POST /referrals/grants/{id}/fulfil`, or let Bolt Claw give it
        through your Stripe if you set a restricted key. A refund or dispute of the sale cancels
        one not yet given and marks one already given `reversed`. A friend's discount is recorded
        as `fulfilled` with the sale, when the sale shows it was used.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/GrantStatus'
        - name: type
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/GrantType'
        - name: recipient
          in: query
          required: false
          description: '`promoter` for the people who shared, `referee` for the friends.'
          schema:
            type: string
            enum:
              - promoter
              - referee
        - $ref: '#/components/parameters/Limit100'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of grants.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrantList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/grants/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralGrant
      tags:
        - Referrals
      summary: Read one reward that is not cash
      description: |
        One grant, as the list shows it. Read it from a `referral.grant.*` webhook event's
        `grant_id` before you give it.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The grant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/grants/{id}/fulfil:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: fulfilReferralGrant
      tags:
        - Referrals
      summary: Say a reward was given
      description: |
        You gave the reward in your own system (a month added, credit applied, a perk sent); this
        records it with your `reference`, tells the person on their page and raises
        `referral.grant.fulfilled`. Only a `ready` grant can be given: one still in its hold, or
        cancelled, is `409`. Sending the same reference again answers the grant as it is and
        changes nothing, so a retry is safe; a different reference for a grant already given is
        `409`. Recorded in the activity log. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.grant.fulfil
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Records that a reward was given with your reference and tells the person; it is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GrantFulfil'
      responses:
        '200':
          description: The grant, now `fulfilled`.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/grants/{id}/fail:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: failReferralGrant
      tags:
        - Referrals
      summary: Say a reward could not be given
      description: |
        The reward cannot be given as it stands (the account was closed, the perk is out of
        stock): this records your `reason`, shown to you and not to the person, and leaves the
        grant `failed`, so you can still give it later with `POST /referrals/grants/{id}/fulfil`.
        A grant already given or cancelled is `409`. Recorded in the activity log. An AI
        assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.grant.fail
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Records that a reward could not be given, with your reason; it is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GrantFail'
      responses:
        '200':
          description: The grant, now `failed`.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/grants/{id}/send:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: sendReferralGrant
      tags:
        - Referrals
      summary: Give a gift card through your Tremendous
      description: |
        Gives a ready gift card to the promoter who earned it, from YOUR Tremendous account, at
        the email in their payout details, and records it fulfilled with Tremendous's order id.
        The order's id is made from the grant's, so pressing again never buys a second card. A
        friend's gift card cannot be sent this way (BoltClaw holds no address for a friend).
        `202` means Tremendous did not answer in time: try again in ten minutes.

        Needs `referrals:payouts`, or an AI assistant with a person's approval, given by someone
        signed in with two-step verification who is not the promoter it pays. A grant whose
        promoter or sale has an open fraud flag waits. Recorded in the activity log.
      x-scope: referrals:write
      x-any-scopes:
        - referrals:write
        - referrals:payouts
      x-permission: referrals.payouts
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.grant.fulfil
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Buys the gift card from your Tremendous balance and sends it to the promoter.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The grant, given.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
        '202':
          description: Tremendous did not answer in time; the grant waits. Try again in ten minutes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/offers/{code}:
    parameters:
      - $ref: '#/components/parameters/LinkCode'
    get:
      operationId: getReferralOffer
      tags:
        - Referrals
      summary: Read the friend's offer for a referral link
      description: |
        What a person who arrived through this link is offered, in the words the welcome shows
        (`offer`), and whether THIS customer may have it at your checkout (`eligible`). Call it
        from your server before you create a checkout: with `customer_id` (your own id for the
        customer, when they have one) and `new_customer`, a customer who is the referrer
        themselves, or was already referred by someone else, is not eligible, with a stable
        `reason_code` and a sentence to show instead of an error.

        `coupon` is the coupon Bolt Claw made for the friend's discount in your own Stripe (with a
        restricted key), ready for `discounts: [{ coupon }]`; `offer.stripe.promotion_code` is the
        referrer's own promotion code, when they have one.

        A link that does not exist, is archived, or is another workspace's answers `200` with
        `offer: null` and `reason_code: unknown_code`, exactly as one that never existed. `coupon`
        is null whenever `eligible` is false.

        Needs `referrals:read`, or `referrals:embed`.
      x-scope: referrals:read
      x-any-scopes:
        - referrals:read
        - referrals:embed
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      parameters:
        - name: customer_id
          in: query
          required: false
          description: Your own id for the customer, at most 255 characters.
          schema:
            type: string
            minLength: 1
            maxLength: 255
        - name: new_customer
          in: query
          required: false
          description: '`false` when the customer has paid you before: the offer is for new customers.'
          schema:
            type: string
            enum:
              - 'true'
              - 'false'
      responses:
        '200':
          description: The offer and whether this customer may have it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/customers/{external_id}/referral:
    parameters:
      - $ref: '#/components/parameters/Id'
      - $ref: '#/components/parameters/CustomerExternalId'
    get:
      operationId: getReferralCustomerReferral
      tags:
        - Referrals
      summary: Read who invited a customer
      description: |
        For your own screens ("Invited by Jane"): whether this customer of yours arrived through
        the program, when, and how (`link` or `discount_code`). `invited_by` is the inviter's
        name only when they agreed to it being shown; otherwise null. A customer nobody referred
        answers with every field null; one the program has never seen is `404`.

        Needs `referrals:read`, or `referrals:embed`.
      x-scope: referrals:read
      x-any-scopes:
        - referrals:read
        - referrals:embed
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The customer's referral.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerReferral'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/stripe-codes:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: syncReferralStripeCodes
      tags:
        - Referrals
      summary: Create the program's codes in Stripe
      description: |
        Makes one coupon in your own Stripe from the friend's discount rule ("Referral: 20% off for
        3 months"), then a promotion code for every active code of the program that Stripe does not
        have yet, so a friend can type their referrer's code at Stripe Checkout. A code that
        already exists in Stripe with the same text is linked, not made again; running this twice
        makes nothing twice. Needs a restricted Stripe key, set in the dashboard (Integration tab);
        without one, or without a friend's discount rule, the answer is `409`. Recorded in the
        activity log. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.stripe.codes
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Creates a coupon and promotion codes in your own Stripe account, which changes what your customers pay at checkout.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: What was made and linked.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeCodesResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/integration-status:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralIntegrationStatus
      tags:
        - Referrals
      summary: Read a program's live setup status
      description: |
        Whether each part of the setup works, from what Bolt Claw has actually seen for this
        program: the website tag, billing (any connected billing system, or sales through this
        API), signups, sales, discount codes, the in-app panel and the friend's welcome. Each part
        is `connected`, `warnings` (connected, with something to fix) or `not_connected`, with one
        sentence saying what was seen and one saying what to do. A part the program's own setup
        does not use is `skipped` and does not count toward `overall`. Test data never makes a part
        green: only live events do.

        `integrations` lists every connected billing system as its card shows it: which one, how it
        proves its deliveries, when the last verified event arrived and how many were refused since.
        No credential is ever returned, not even in part.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The program's setup, part by part.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntegrationStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/integration-status/tag-check:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: checkReferralTag
      tags:
        - Referrals
      summary: Check the website tag on the program's product page
      description: |
        Fetches the program's product URL once, from Bolt Claw's server, and looks for the website
        tag (`/referrals/v2.js`, or the older `v1.js`) in a script tag, then updates the setup
        status. Only `https`, only the program's own hosts, never a private address (checked after
        the name is resolved, and again on every redirect), five seconds and 512 KB at most. A tag
        your site adds with JavaScript is not visible to this check: open your site with the verify
        link from the dashboard instead. A few checks per program an hour.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Fetches your program's product page once from Bolt Claw's server to see whether the website tag is on it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: What the check found.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagCheck'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/test-sale:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: sendReferralTestSale
      tags:
        - Referrals
      summary: Send a test sale through the program
      description: |
        Runs the real path with TEST data, step by step: the program's test partner ("Test
        partner", made once) and its link, a click on it, a signup, a sale of 100.00 in the
        program's currency with two items, then a partial refund of 25.00. Each step answers with
        what it caused: the conversion with its commissions and grants, the friend's offer, and how
        many webhook deliveries it queued for endpoints that receive test events.

        Test data is never payable, never counts toward a limit or a total, emails nobody, and
        reaches only webhook endpoints that switched test events on. `Clear test data` deletes it
        all. The program must be active.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-agent-consequence: Records a test click, signup, sale and refund through your program, credited to its test partner; nothing is payable and only endpoints that receive test events are told.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: Every step, in order.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestSale'
        '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/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/test-data:
    parameters:
      - $ref: '#/components/parameters/Id'
    delete:
      operationId: clearReferralTestData
      tags:
        - Referrals
      summary: Delete a program's test data
      description: |
        Deletes every TEST row of the program: the conversions, commissions, rewards, customers and
        clicks that test sales, `test: true` conversions, a test-mode Stripe or a sandbox recorded,
        and the program's test partner with its link. Live data is never touched. Recorded in the
        activity log. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.testdata.clear
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Deletes every test conversion, commission, reward, customer and click of this program, and its test partner. Live data is not touched.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      responses:
        '200':
          description: How many of each were deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestDataCleared'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/clicks:
    post:
      operationId: recordReferralClick
      tags:
        - Referrals
      summary: Record a click your own server saw
      description: |
        For a partner's link that opens straight on your own site (`?via=<code>`), reported by your
        server, for example by the SDK's `referralsMiddleware()`. Send the link's code and what your
        server saw: the visitor's IP address and user agent are hashed and classed exactly as a
        click on `/r/<code>` is, and never stored. It counts toward the click allowance like any
        click, and earns nothing by itself: a click is never a conversion. A robot is answered
        `recorded: false` and stored nowhere; a code that is not one of your links is `404`.

        Needs `conversions:write`, like reporting a conversion.
      x-scope: conversions:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-agent-consequence: Records one click on one of your partners' links, as your own server saw it.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReferralClickCreate'
      responses:
        '200':
          description: Not stored (a robot, or the month's click allowance is used).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralClickRecorded'
        '201':
          description: Recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReferralClickRecorded'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/flags:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: listReferralFlags
      tags:
        - Referrals
      summary: List a program's review queue
      description: |
        The flags waiting for a decision (or, with `status`, the ones already decided), newest
        first, up to 200. A flag is raised by a rule, never decided by one: the risk engine only
        ever HOLDS (a longer hold on what a conversion earned, or `risk_review` for a person to
        look at), and nothing here bans, rejects or confirms anybody by itself. Each flag names
        the partner by their name in this program, and the conversion, commission or link it is
        about; `score` is the risk score (0 to 100) behind a `risk_review` flag.

        Filter with `rule`, `partner_id`, `min_score` and `older_than_days`.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: status
          in: query
          required: false
          description: '`open` (the default), `dismissed` or `confirmed`.'
          schema:
            type: string
            enum:
              - open
              - dismissed
              - confirmed
        - name: rule
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/FlagRule'
        - name: partner_id
          in: query
          required: false
          description: One partner's membership id.
          schema:
            type: string
            format: uuid
        - name: min_score
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 100
        - name: older_than_days
          in: query
          required: false
          description: Only flags at least this many days old.
          schema:
            type: integer
            minimum: 0
            maximum: 3650
        - name: reason
          in: query
          required: false
          description: Only `risk_review` flags whose score had this reason, for example `fast_conversion`.
          schema:
            type: string
            enum:
              - fast_conversion
              - paid_ads
              - datacenter_click
              - disposable_customer
              - email_lookalike
              - no_click_pattern
              - code_leak_spike
              - refund_farming
              - conversion_rate_outlier
              - new_partner
      responses:
        '200':
          description: The flags.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlagList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/flags/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralFlag
      tags:
        - Referrals
      summary: Read one flag in full
      description: |
        Everything a person needs to decide a flag: the score and every reason as a sentence (what
        was seen, in counts and times: "Sale 4 seconds after the click", never an address, a card
        or an email), the timeline of the customer's click, signup, sale and refunds with the
        seconds between them, and the partner's history in this program (age, clicks and customers
        in 30 days, refunds in 60, and their other flags). The partner never sees any of it: their
        side says only "Under review".

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The flag, its reasons, timeline and the partner's history.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlagDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/flags/{id}/dismiss:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: dismissReferralFlag
      tags:
        - Referrals
      summary: Dismiss a flag (not fraud)
      description: |
        Not fraud: the flag is closed and what it held goes on. A held commission matures at its
        hold (or on the next sweep if that has passed) and joins a payout like any other. A
        flag already decided is `409`. Recorded in the activity log with your note. An AI
        assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.flag.dismiss
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Closes the flag as not fraud, so the commission it held can be paid; it is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FlagDecision'
      responses:
        '200':
          description: The flag, now `dismissed`.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlagResolution'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/flags/{id}/confirm:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: confirmReferralFlag
      tags:
        - Referrals
      summary: Confirm a flag as fraud
      description: |
        Fraud: every commission the flag holds that is still `pending` or `approved` becomes
        `fraud`, which never reaches a payout, and a reward not yet given is cancelled. With
        `ban_partner: true` the partner is banned from the program too; nothing bans anybody
        unless you say so. A commission already in a payout is `409`. Recorded in the activity log
        with your note. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.flag.confirm
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Marks what the flag held as fraud so it is never paid, and bans the partner if asked; it is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FlagConfirm'
      responses:
        '200':
          description: The flag, now `confirmed`, and what it did.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlagResolution'
        '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'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/flags/dismiss:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: dismissReferralFlags
      tags:
        - Referrals
      summary: Dismiss many flags at once
      description: |
        Dismisses up to 200 open flags of one program with one reason, each decided and recorded in
        the activity log on its own. A flag that is not this program's, not open, or holds a
        commission already in a payout is left as it was and listed in `skipped` with why. An AI
        assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.flag.dismiss
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Closes each listed flag as not fraud, so what they held can be paid; each is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FlagsBulk'
      responses:
        '200':
          description: How many were decided, and the ones left as they were.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlagsResolution'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/flags/confirm:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: confirmReferralFlags
      tags:
        - Referrals
      summary: Confirm many flags as fraud at once
      description: |
        Confirms up to 200 open flags of one program as fraud with one reason, each decided and
        recorded in the activity log on its own: what each held becomes `fraud`, and with
        `ban_partners: true` their partners are banned. Flags left as they were are listed in
        `skipped` with why. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.flag.confirm
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Marks what each listed flag held as fraud so it is never paid, and bans their partners if asked; each is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FlagsBulkConfirm'
      responses:
        '200':
          description: How many were decided, and the ones left as they were.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlagsResolution'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/partners/{id}/hold-payouts:
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      operationId: holdReferralPartnerPayouts
      tags:
        - Referrals
      summary: Hold a partner's payouts
      description: |
        "Hold this promoter's payouts": opens a `manual_hold` flag on the partner, and nothing they
        earned is paid while it is open. A stop, never a decision: what they earned stays theirs
        until you dismiss or confirm the flag. Holding a partner already held answers the open
        flag with `already: true`. Recorded in the activity log.

        One API key may place at most 30 holds an hour (a platform setting), answered `429
        RATE_LIMITED` past it: only a person releases a hold. The dashboard is not counted.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-cross-tenant: 404
      x-idempotent: true
      x-agent-consequence: Holds every payout to this partner until a person decides; nothing is taken away.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FlagDecision'
      responses:
        '200':
          description: Already held; the open flag.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutHold'
        '201':
          description: Held; the new flag.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayoutHold'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/risk-policy:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralRiskPolicy
      tags:
        - Referrals
      summary: Read a program's fraud settings
      description: |
        What holds this program's money: the score at which a conversion's earnings wait longer
        (`hold_at`, and `extra_hold_days`), the score at which a person is asked (`review_at`), the
        fewest seconds from click to signup, sale or shop order that look human, whether paid
        search on the brand is allowed (with campaign ids that are), and whether new partners wait
        longer. `policy` is what is in force (your values, else the platform's); `custom` is only
        what you set. `alert_email` emails the program's managers when a promoter needs a look, at
        most once per promoter a day.

        Needs `referrals:read`.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The settings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RiskPolicyView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateReferralRiskPolicy
      tags:
        - Referrals
      summary: Change a program's fraud settings
      description: |
        Replaces what you set with `policy` (send `null` to go back to the platform's values) and
        switches the alert email. Every value is checked: `review_at` is never below `hold_at`, a
        campaign id is letters, digits and `_.:-`. Applies from the next conversion; what was
        already assessed keeps its hold. Recorded in the activity log with the values before and
        after. An AI assistant's key is always asked first.

        Needs `referrals:write`.
      x-scope: referrals:write
      x-permission: referrals.manage
      x-function: referrals-api
      x-rate-limit-class: write
      x-boltclaw-approval: referral.risk.update
      x-boltclaw-approval-always: true
      x-cross-tenant: 404
      x-agent-consequence: Changes the thresholds and rules that decide when this program's payouts are held; it is audited.
      parameters:
        - $ref: '#/components/parameters/BoltClawApproval'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RiskPolicyUpdate'
      responses:
        '200':
          description: The settings now.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RiskPolicyView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '428':
          $ref: '#/components/responses/ApprovalRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /referrals/programs/{id}/stats:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getReferralProgramStats
      tags:
        - Referrals
      summary: Read a program's stats
      description: Clicks, leads, sales, revenue and commission over a window, in total, per partner and, with `bucket`, over time.
      x-scope: referrals:read
      x-permission: referrals.view
      x-function: referrals-api
      x-rate-limit-class: read
      x-cross-tenant: 404
      parameters:
        - name: from
          in: query
          required: false
          schema:
            type: string
            maxLength: 64
        - name: to
          in: query
          required: false
          schema:
            type: string
            maxLength: 64
        - name: bucket
          in: query
          required: false
          schema:
            type: string
            enum:
              - day
              - week
              - month
      responses:
        '200':
          description: The stats.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProgramStats'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /auth/device:
    post:
      operationId: startDeviceAuthorization
      tags:
        - Agents
      summary: Start a device sign-in
      description: |
        How an AI assistant or a command-line tool gets a key, without anybody pasting one.

        **Your program generates the key.** Make 32 random bytes, render them as 43 base62
        characters, prefix them with `bc_live_`, and send only the SHA-256 of the whole string
        plus the first 8 characters of the random body. The key itself must never leave the
        machine it was made on, and there is no field here that would carry it. BoltClaw stores
        the hash, exactly as it stores every other key's, so no secret of ours exists at any
        moment, not even between the approval and your next poll.

        You get back a short code and a URL. Show both to the person, poll
        `POST /v1/auth/device/poll` every `interval` seconds, and once they approve, the key you
        already hold starts working.

        Ask for the permissions you need and no more: the person sees them, can untick any of
        them, and the key is narrowed again to what they themselves may grant. Permissions an
        assistant may never hold, such as managing API keys or webhooks, are refused here rather
        than silently dropped later.

        This call needs no API key, and it is limited per address rather than per key.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: auth
      x-keyless: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceAuthorizationStart'
      responses:
        '201':
          description: The request is waiting for a person.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceAuthorization'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /auth/device/poll:
    post:
      operationId: pollDeviceAuthorization
      tags:
        - Agents
      summary: Poll a device sign-in
      description: |
        Has a person decided yet? Send the `device_code` from `POST /v1/auth/device`, no more
        often than `interval` seconds apart.

        `status` is `pending` while you wait, `slow_down` if you are asking too fast (wait
        `interval` seconds and carry on), `approved` once a person has said yes, `denied` if they
        said no, and `expired` if nobody decided in time. A code that was never issued, or has been
        cleared away since, answers `expired` too, exactly like one that lapsed: the answer never
        tells you whether a code existed. Either way, start a new sign-in.

        An approval answers with the key's id, its prefix, the permissions it ended up with and
        when it expires. It never answers with a key: you made it, so you already have it.

        It also names the workspace the person chose, by id and name. Apart from `GET /workspace`
        and one route reserved for the BoltClaw MCP server, this is the only answer in the API that
        carries a workspace id, and it goes only to the holder of the device code: the program
        that started the sign-in, which is about to hold a key for that workspace and could read
        the same id with it.

        This call needs no API key.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: auth
      x-keyless: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceAuthorizationPoll'
      responses:
        '200':
          description: Where the sign-in has got to.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceAuthorizationStatus'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /approvals/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getApprovalStatus
      tags:
        - Agents
      summary: Read an approval you asked for
      description: |
        Has the person decided yet? This is the one thing a key may read about an approval, and
        only about its own: an approval another key asked for is `404`, exactly like one that
        never existed.

        You get the status and the deadline, not the summary: the summary was written for the
        person who decides, and you already know what you asked for. A `pending` or `approved`
        ticket past its deadline reads as `expired`.

        Cheap and safe to poll while you wait, and left out of the agent call log so that waiting
        does not bury the calls that did something. Needs no scope.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: read
      x-cross-tenant: 404
      responses:
        '200':
          description: The approval, as it stands.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /auth/key/extend:
    post:
      operationId: extendOwnApiKey
      tags:
        - Agents
      summary: Extend the key you are using
      description: |
        A key approved for an AI assistant lasts 90 days. Within 30 days of its expiry it can push
        that out by another 90, up to a year from the day it was issued, and then a person signs
        in again.

        Nothing else can be extended this way: a key a person created in the dashboard has an
        expiry they chose, and this call will not second-guess it. The extension also stops if the
        person who approved the connection can no longer approve it, or if the organisation has
        turned AI assistant access off, which is what makes it safe to let a credential renew
        itself.

        Acts only on the key that made the call, so it needs no scope. Safe to call on a schedule:
        outside the 30-day window it answers `extended: false` and changes nothing.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: auth
      x-agent-consequence: Extends the expiry of the key making the call. Nothing else changes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Whether it moved, and when it now expires.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyExtendResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /auth/key/revoke:
    post:
      operationId: revokeOwnApiKey
      tags:
        - Agents
      summary: Revoke the key you are using
      description: |
        Sign out, for real. Deleting a key from a configuration file leaves it working; this ends
        it. Use it when a person disconnects your program, and when you are uninstalling.

        Acts only on the key that made the call, so it needs no scope, and it works even when the
        organisation has turned AI assistant access off: handing a credential back must never be
        the thing you cannot do. It cannot be undone, and it is safe to repeat.

        If the key is the root of a connection that spans several workspaces, every key in that
        connection ends with it.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: auth
      x-agent-consequence: Permanently ends the key making the call, and every key in its connection.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The key is revoked.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyRevokeResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /auth/key/revoke-oauth:
    post:
      operationId: revokeOauthConnection
      tags:
        - Agents
      summary: End a connection
      description: |
        Ends a whole AI connection, from its root, so no workspace is left reachable. Reserved
        for the BoltClaw MCP server; anything else is `404`.

        It takes a key ID rather than a key, because the MCP server reaches it exactly when it no
        longer holds one: a person signing the assistant out of their client revokes the grant
        whose encrypted store was the only copy of the key. The answer is the same for a key id
        that was a connection's root and for one that was never anything, so it tells a caller
        nothing about which ids exist.

        This call needs no API key.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: auth
      x-mcp-worker-only: true
      x-keyless: true
      x-agent-consequence: Permanently ends every key in the connection.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectionRevoke'
      responses:
        '200':
          description: The connection is revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyRevokeResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /auth/key/workspaces:
    get:
      operationId: listConnectionWorkspaces
      tags:
        - Agents
      summary: List a connection's workspaces
      description: |
        Which workspaces this connection reaches today. Re-resolved on every call against the live
        permissions of the person who approved it, so a workspace they can no longer reach drops
        out with nothing to revoke. Reserved for the BoltClaw MCP server; anything else is `404`.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: read
      x-mcp-worker-only: true
      responses:
        '200':
          description: The workspaces, and which already have a key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionWorkspaceList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: registerConnectionWorkspace
      tags:
        - Agents
      summary: Register a connection's workspace
      description: |
        Registers the key for one workspace of a connection, from a hash the caller derived and a
        prefix to display. Creates nothing if that workspace already has a live key, and answers
        the one it has. Reserved for the BoltClaw MCP server; anything else is `404`.
      x-scope: none
      x-function: api-agents
      x-rate-limit-class: write
      x-mcp-worker-only: true
      x-agent-consequence: Registers one more key for this connection, in a workspace it already reaches.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectionWorkspaceRegister'
      responses:
        '200':
          description: That workspace already had a key.
          headers:
            Idempotent-Replay:
              $ref: '#/components/headers/IdempotentReplay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionWorkspaceKey'
        '201':
          description: Registered.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionWorkspaceKey'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /logs/agent-calls:
    get:
      operationId: listAgentCalls
      tags:
        - Logs
      summary: List agent calls
      description: |
        Every request an AI assistant made in this workspace, newest first: which connection, which
        tool it said it was running, which operation, what it answered, how long it took, and the
        approval it carried if it carried one. Kept for 90 days.

        Page with `before`, an ISO-8601 timestamp from the last row of the previous page.
      x-scope: logs:read
      x-permission: logs.view
      x-function: api-agents
      x-rate-limit-class: read
      parameters:
        - name: before
          in: query
          required: false
          description: An ISO-8601 timestamp with an offset. Returns calls older than it.
          schema:
            type: string
            format: date-time
        - $ref: '#/components/parameters/Limit100'
      responses:
        '200':
          description: A page of calls.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentCallList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /logs/audit:
    get:
      operationId: listAuditLog
      tags:
        - Logs
      summary: List the activity log
      description: |
        What everybody did in this workspace, newest first: the same list the dashboard shows under
        Settings, Activity. Each entry names the action, what it acted on, when, and who by, as a
        label rather than an account: a member's display name, the name of an API key, or BoltClaw
        support.

        Values that look like a credential are replaced with `[redacted]` before the entry leaves
        the database, so a log is never a place to find one.
      x-scope: logs:read
      x-permission: logs.view
      x-function: api-agents
      x-rate-limit-class: read
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit100'
      responses:
        '200':
          description: A page of entries.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditEntryList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer bc_live_...`: `bc_live_` and 43 letters and digits. A key belongs to
        one workspace and carries scopes. Only a hash is stored, so a lost key can only be replaced.
        A malformed, unknown or revoked key is `401 UNAUTHORIZED`, and the answer never says which.
  parameters:
    Cursor:
      name: cursor
      in: query
      required: false
      description: The `next_cursor` from the previous page. Opaque; an invalid one is `400`.
      schema:
        type: string
        maxLength: 512
    Limit200:
      name: limit
      in: query
      required: false
      description: Page size, 1 to 200. Above 200 is clamped; anything else is the default.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    Limit100:
      name: limit
      in: query
      required: false
      description: Page size, 1 to 100. Above 100 is clamped; anything else is the default.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    Slug:
      name: slug
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Slug'
    Id:
      name: id
      in: path
      required: true
      description: An id this API gave you. Anything that is not one of yours is `404`.
      schema:
        type: string
        format: uuid
    SubjectExternalId:
      name: external_id
      in: path
      required: true
      description: |
        The id your system uses for the person: the `external_id` you sent with their conversions or
        when you made them a referrer, or the payment processor's customer id. Percent-encode it in
        the path. An id nobody in this workspace has is `404`. The decoded id is at most 255 characters;
        the encoded segment may be longer.
      schema:
        type: string
        minLength: 1
        maxLength: 2300
        pattern: ^(?:[A-Za-z0-9._~-]|%[0-9A-Fa-f]{2})+$
    ExternalId:
      name: external_id
      in: path
      required: true
      description: |
        Your own id for the user, as you sent it in `external_id`. It can hold any printable
        characters, so percent-encode it in the path (`user%2F1`). An id that is not one of your
        referrers is `404`. The decoded id is at most 200 characters; the encoded segment may be longer.
      schema:
        type: string
        minLength: 1
        maxLength: 1800
        pattern: ^(?:[A-Za-z0-9._~-]|%[0-9A-Fa-f]{2})+$
    CustomerExternalId:
      name: external_id
      in: path
      required: true
      description: |
        Your own id for the customer, as you sent it in a conversion's `customer.id`. Percent-encode
        it in the path. The decoded id is at most 255 characters; the encoded segment may be longer.
      schema:
        type: string
        minLength: 1
        maxLength: 2300
        pattern: ^(?:[A-Za-z0-9._~-]|%[0-9A-Fa-f]{2})+$
    LinkCode:
      name: code
      in: path
      required: true
      description: The code at the end of a referral link (`/r/jane`), lower case.
      schema:
        type: string
        minLength: 3
        maxLength: 40
        pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
    Year:
      name: year
      in: path
      required: true
      description: |
        A calendar year, four digits (`2026`). A year before 2026, or after this one, is `400`;
        anything that is not four digits is `404`.
      schema:
        type: string
        minLength: 4
        maxLength: 4
        pattern: ^[0-9]{4}$
    BoltClawApproval:
      name: BoltClaw-Approval
      in: header
      required: false
      description: |
        The id of an approval a person has decided, from a `428` you were given earlier. Only an
        AI assistant's request is ever gated; send the identical request again with this header
        once the person has approved it. An id that is spent, lapsed, for another request, or for
        another key is `409 APPROVAL_INVALID`.
      schema:
        type: string
        format: uuid
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Optional. One key per logical operation, 1 to 255 printable ASCII characters (a UUID
        works). The same key and request within 24 hours replays the first success with
        `Idempotent-Replay: true`. The same key with another request is
        `409 IDEMPOTENCY_KEY_CONFLICT`; while the first is running, `409 IDEMPOTENCY_KEY_IN_FLIGHT`.
        A malformed key is `400`.
      schema:
        type: string
        minLength: 1
        maxLength: 255
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Required on this operation, which mints a credential exactly once. One key per logical
        operation, 1 to 255 printable ASCII characters (a UUID works). The same key and request
        within 24 hours replays the first success with `Idempotent-Replay: true` and `secret:
        null`. The same key with another request is `409 IDEMPOTENCY_KEY_CONFLICT`; while the
        first is running, `409 IDEMPOTENCY_KEY_IN_FLIGHT`. A missing or malformed key is `400`.
      schema:
        type: string
        minLength: 1
        maxLength: 255
  headers:
    Location:
      description: The path of the object this operation created.
      schema:
        type: string
    XRequestId:
      description: This request's id; the same value as `request_id` in an error body.
      schema:
        type: string
        format: uuid
    IdempotentReplay:
      description: '`true` when this is the stored answer to an earlier request with the same Idempotency-Key.'
      schema:
        type: string
        enum:
          - 'true'
    RateLimitLimit:
      description: Requests a minute this key may make, from its organisation's plan.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests left in the current minute.
      schema:
        type: integer
    RateLimitReset:
      description: Unix seconds at which the current minute ends.
      schema:
        type: integer
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
  schemas:
    DeviceAuthorizationStart:
      type: object
      required:
        - client_name
        - scopes
        - key_hash
        - key_prefix
      additionalProperties: false
      description: |
        The key is made on your machine and never sent. `key_hash` is the SHA-256 of the whole
        `bc_live_...` string, as 64 lowercase hex characters; `key_prefix` is the first 8
        characters of the random body after the prefix, and is what a person sees in the list of
        connections.
      properties:
        client_name:
          type: string
          minLength: 1
          maxLength: 80
          description: What you are called, shown to the person deciding. Never trusted for anything.
        client_host:
          type: string
          minLength: 1
          maxLength: 120
          description: The machine you are running on, shown beside the name so a person can tell two terminals apart.
        scopes:
          type: array
          minItems: 1
          maxItems: 25
          description: The permissions you are asking for. Ask for what you need and no more.
          items:
            $ref: '#/components/schemas/Scope'
        key_hash:
          type: string
          pattern: ^[0-9a-f]{64}$
        key_prefix:
          type: string
          pattern: ^[0-9A-Za-z]{8}$
        flow:
          type: string
          enum:
            - device
            - oauth
          default: device
          description: |
            `device` for a program somebody is sitting at. `oauth` is the hosted BoltClaw MCP
            server acting for a person, and only it may ask for one.
        redirect_host:
          type: string
          minLength: 1
          maxLength: 253
          description: 'Only with `flow: oauth`. The host the person will be sent back to, shown to them before they approve.'
    DeviceAuthorization:
      type: object
      required:
        - device_code
        - user_code
        - verification_uri
        - verification_uri_complete
        - expires_in
        - expires_at
        - interval
      properties:
        device_code:
          type: string
          description: Your half of the exchange. Keep it, poll with it, and never show it to anybody.
        user_code:
          type: string
          description: What the person types, as `XXXX-XXXX`. Show it to them.
        verification_uri:
          type: string
          format: uri
        verification_uri_complete:
          type: string
          format: uri
          description: The same page with the code filled in. Use it when you can show a link or a QR code.
        expires_in:
          type: integer
          description: Seconds until the request lapses.
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        interval:
          type: integer
          description: Seconds to wait between polls. Asking faster is answered `slow_down`.
    DeviceAuthorizationPoll:
      type: object
      required:
        - device_code
      additionalProperties: false
      properties:
        device_code:
          type: string
          minLength: 16
          maxLength: 200
    DeviceAuthorizationStatus:
      type: object
      required:
        - status
      description: |
        `pending` while you wait, `slow_down` if you are asking too fast, `approved` once a person
        has said yes, `denied` if they said no, `expired` if nobody decided in time. A code that
        was never issued answers `expired` as well, so the answer never says whether one existed.
      properties:
        status:
          type: string
          enum:
            - pending
            - slow_down
            - approved
            - denied
            - expired
        interval:
          type: integer
          description: Only with `slow_down`. Wait this many seconds.
        key:
          type: object
          description: Only with `approved`. Never the key itself, which you already hold.
          required:
            - id
            - key_prefix
            - scopes
            - expires_at
          properties:
            id:
              type: string
              format: uuid
            key_prefix:
              type: string
            scopes:
              type: array
              items:
                $ref: '#/components/schemas/Scope'
            expires_at:
              type:
                - string
                - 'null'
              format: date-time
            connection_id:
              type:
                - string
                - 'null'
              format: uuid
        workspace:
          type: object
          description: |
            Only with `approved`. The workspace the person chose. One of the few answers that
            carries a workspace id, because it goes only to the holder of the device code: the
            program that started the sign-in, whose new key belongs to this workspace.
          required:
            - id
            - name
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        callback_nonce_hash:
          type: string
          description: Only for the `oauth` flow, and only to the client that started it.
    ApprovalStatus:
      type: object
      required:
        - id
        - action
        - status
        - expires_at
        - decided_at
      properties:
        id:
          type: string
          format: uuid
        action:
          $ref: '#/components/schemas/ApprovalAction'
        status:
          type: string
          enum:
            - pending
            - approved
            - denied
            - used
            - expired
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        decided_at:
          type:
            - string
            - 'null'
          format: date-time
    ApprovalAction:
      type: string
      description: |
        What an assistant asked to do. The actions in the first group are the ones an organisation
        may decide to stop being asked about, or waive for one connection for a few hours; the
        rest are always asked, because they move somebody else's money or change somebody else's
        access.
      enum:
        - post.publish
        - post.delete
        - inbox.reply
        - inbox.hide
        - conversation.reply
        - automation.enable
        - automation.delete
        - page.delete
        - page.unpublish
        - link.delete
        - lead.delete
        - referral.payout.paid
        - referral.commission.adjust
        - referral.partner.approve
        - social.account.disconnect
        - referral.commission.bonus
        - referral.reward.delete
        - referral.group.delete
        - referral.referrer.erase
        - referral.grant.fulfil
        - referral.grant.fail
        - referral.stripe.codes
        - referral.testdata.clear
        - referral.flag.dismiss
        - referral.flag.confirm
        - referral.risk.update
        - referral.payout.send
        - referral.payout.settings
        - referral.commission.forfeit
        - referral.tax.settings
        - referral.brand.update
        - referral.domain.add
        - referral.domain.remove
        - referral.listing.update
        - referral.library.delete
        - referral.broadcast.send
        - referral.import.commit
        - referral.contest.award
        - referral.contest.cancel
        - referral.waitlist.disqualify
        - referral.waitlist.invite
        - referral.tier.update
        - referral.waitlist.end
        - referral.waitlist.forget
        - referral.shopify.codes
        - referral.terms.update
        - referral.subject.erase
        - referral.program.archive
        - referral.partner.ban
        - referral.partner.unban
        - referral.commission.approve
        - referral.code.create
        - referral.link.archive
        - referral.program.restore
        - referral.signup.rotate
        - referral.waitlist.rotate
        - referral.embed.rotate
        - referral.referrer.approve
        - referral.code.link
        - referral.payout.reopen
    KeyExtendResult:
      type: object
      required:
        - extended
        - expires_at
      properties:
        extended:
          type: boolean
          description: False when it was not yet near enough to its expiry, or is already at its limit.
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        reason:
          type: string
          description: Why it did not move, when it did not.
    KeyRevokeResult:
      type: object
      required:
        - revoked
      properties:
        revoked:
          type: boolean
          const: true
    ConnectionWorkspaceList:
      type: object
      required:
        - connection_id
        - all_workspaces
        - data
        - next_cursor
      description: |
        Answered only to the BoltClaw MCP server, which keeps one key per workspace of a connection
        and needs each workspace's id to do it. Anything else asking is answered `404`.
      properties:
        connection_id:
          type: string
          format: uuid
        all_workspaces:
          type: boolean
          description: The connection follows the organisation, including workspaces made after it was approved.
        data:
          type: array
          items:
            type: object
            required:
              - id
              - name
              - is_default
              - key_id
              - generation
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              is_default:
                type: boolean
              key_id:
                type:
                  - string
                  - 'null'
                format: uuid
              generation:
                type: integer
                minimum: 0
                description: |
                  How many of this connection's keys for the workspace were revoked. The MCP server
                  derives the workspace's key with it, so a workspace taken out and put back gets a new
                  key; a revoked key is never revived.
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ConnectionRevoke:
      type: object
      required:
        - key_id
      additionalProperties: false
      properties:
        key_id:
          type: string
          format: uuid
          description: The id of any key in the connection. Its root, and every sibling, end with it.
    ConnectionWorkspaceRegister:
      type: object
      required:
        - workspace_id
        - key_hash
        - key_prefix
      additionalProperties: false
      properties:
        workspace_id:
          type: string
          format: uuid
        key_hash:
          type: string
          pattern: ^[0-9a-f]{64}$
        key_prefix:
          type: string
          pattern: ^[0-9A-Za-z]{8}$
    ConnectionWorkspaceKey:
      type: object
      required:
        - key_id
        - created
        - scopes
      properties:
        key_id:
          type: string
          format: uuid
        created:
          type: boolean
          description: False when that workspace already had a live key.
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
    AgentCall:
      type: object
      required:
        - id
        - key_id
        - client_name
        - tool
        - operation_id
        - method
        - status
        - duration_ms
        - approval_id
        - created_at
      properties:
        id:
          type: string
          format: uuid
        key_id:
          type:
            - string
            - 'null'
          format: uuid
        client_name:
          type:
            - string
            - 'null'
          description: What the assistant called itself when it signed in.
        tool:
          type:
            - string
            - 'null'
          description: The tool it said it was running. A label it chose, never acted on.
        operation_id:
          type: string
        method:
          type: string
          enum:
            - GET
            - POST
            - PATCH
            - DELETE
        status:
          type: integer
        duration_ms:
          type:
            - integer
            - 'null'
        approval_id:
          type:
            - string
            - 'null'
          format: uuid
        created_at:
          type: string
          format: date-time
    AgentCallList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AgentCall'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    AuditEntry:
      type: object
      required:
        - id
        - action
        - resource
        - resource_id
        - created_at
        - actor
        - details
      properties:
        id:
          type: string
        action:
          type: string
        resource:
          type:
            - string
            - 'null'
        resource_id:
          type:
            - string
            - 'null'
          description: |
            The id of what it acted on. `null` when that is the workspace itself or its
            organisation (the `workspace.*` and `impersonation.*` entries): a log entry never names
            either of those by id.
        created_at:
          type: string
          format: date-time
        actor:
          type: object
          required:
            - kind
            - label
          description: Who did it, as a label. Never an account id and never an email address.
          properties:
            kind:
              type: string
              enum:
                - member
                - former
                - api_key
                - support
                - system
            label:
              type: string
        details:
          type: object
          additionalProperties: true
          description: What changed. Anything that looks like a credential is `[redacted]`.
    AuditEntryList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AuditEntry'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ErrorEnvelope:
      type: object
      required:
        - error
        - code
        - request_id
      description: Every failed response has this shape.
      properties:
        error:
          type: string
          description: |
            A sentence for a person. It never contains SQL, a stack trace, or anything belonging
            to another customer. Branch on `code`, not on this: wording may change within v1.
        code:
          $ref: '#/components/schemas/ErrorCode'
        request_id:
          type: string
          format: uuid
          description: This request's id, also sent as `X-Request-Id`. Quote it to support.
        fields:
          type: object
          additionalProperties:
            type: string
          description: On a validation error, each field the request got wrong and the rule it broke.
        approval:
          $ref: '#/components/schemas/ApprovalRef'
    ApprovalRef:
      type: object
      required:
        - id
        - url
        - expires_at
        - status
      description: |
        Present when an AI assistant's request waits for a person to approve it. Show `url` to the
        person; once they approve, repeat the identical request with the approval id.
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        status:
          type: string
          enum:
            - pending
    ErrorCode:
      type: string
      description: |
        Stable, machine-readable codes. New ones may be added within v1: treat an unknown code as
        a generic failure of its HTTP status class.

        | Code | HTTP | What it means |
        |---|---|---|
        | `UNAUTHORIZED` | 401 | No key, a malformed one, or one that does not exist or was revoked. The answer never says which. |
        | `FORBIDDEN` | 403 | Refused for a reason no scope would change. The message says what to do. |
        | `NOT_FOUND` | 404 | No such object, or another workspace's. Deliberately indistinguishable. Also an unknown path. |
        | `VALIDATION_ERROR` | 400, 422 | The request or a value in it is not acceptable. `fields` names each field where it can. |
        | `CONFLICT` | 409 | The request clashes with the object's current state: a taken slug, a closed conversation, a full page. |
        | `RATE_LIMITED` | 429 | Too many requests. Wait for `Retry-After`. |
        | `METHOD_NOT_ALLOWED` | 405 | The path exists; the method does not. `Allow` lists the ones that do. |
        | `PAYLOAD_TOO_LARGE` | 413 | The body is over 64 KB. |
        | `INTERNAL_ERROR` | 500, 503 | Ours. Safe to retry, with the same `Idempotency-Key`. |
        | `ALREADY_EXISTS` | 409 | Reserved; not answered today. |
        | `PLAN_LIMIT` | 402 | The organisation's plan does not allow more of this. The message names the allowance. |
        | `INSUFFICIENT_SCOPE` | 403 | The key is valid; it was not issued for this operation. |
        | `ORGANISATION_SUSPENDED` | 403 | The key's organisation is suspended. Its published pages stay live. |
        | `UPSTREAM_ERROR` | 422, 502, 503 | A social network refused or failed the call. The message says what happened. |
        | `UNAVAILABLE` | 503 | Dashboard only; `/v1` answers `500 INTERNAL_ERROR` instead. |
        | `THREAD_ALREADY_OPEN` | 409 | Dashboard only: the person already has an open conversation on that page. |
        | `MFA_REQUIRED` | 401 | Dashboard only: the session owes its second sign-in step. |
        | `REAUTH_REQUIRED` | 401 | Dashboard only: changing where money goes needs a sign-in in the last 15 minutes. |
        | `TWO_STEP_REQUIRED` | 403 | Dashboard only: sending payouts or connecting a payout account needs two-step verification. |
        | `IDEMPOTENCY_KEY_CONFLICT` | 409 | The `Idempotency-Key` was already used with a different request. |
        | `IDEMPOTENCY_KEY_IN_FLIGHT` | 409 | The first request with this `Idempotency-Key` is still running. |
        | `AGENT_ACCESS_DISABLED` | 403 | An AI assistant's key, in an organisation that has turned assistants off. |
        | `APPROVAL_REQUIRED` | 428 | A person must approve this AI assistant's request first. Nothing has happened. |
        | `APPROVAL_PENDING` | 428 | The approval has not been decided yet. Nothing has happened. |
        | `APPROVAL_DENIED` | 403 | A person declined the request. Nothing has happened. |
        | `APPROVAL_INVALID` | 409 | The approval is spent, has lapsed, or is for another request or another key. |
      x-dashboard-only:
        - THREAD_ALREADY_OPEN
        - MFA_REQUIRED
        - UNAVAILABLE
        - REAUTH_REQUIRED
        - TWO_STEP_REQUIRED
      x-reserved:
        - ALREADY_EXISTS
      enum:
        - UNAUTHORIZED
        - FORBIDDEN
        - NOT_FOUND
        - VALIDATION_ERROR
        - CONFLICT
        - RATE_LIMITED
        - METHOD_NOT_ALLOWED
        - PAYLOAD_TOO_LARGE
        - INTERNAL_ERROR
        - ALREADY_EXISTS
        - PLAN_LIMIT
        - INSUFFICIENT_SCOPE
        - ORGANISATION_SUSPENDED
        - UPSTREAM_ERROR
        - UNAVAILABLE
        - THREAD_ALREADY_OPEN
        - MFA_REQUIRED
        - REAUTH_REQUIRED
        - TWO_STEP_REQUIRED
        - IDEMPOTENCY_KEY_CONFLICT
        - IDEMPOTENCY_KEY_IN_FLIGHT
        - AGENT_ACCESS_DISABLED
        - APPROVAL_REQUIRED
        - APPROVAL_PENDING
        - APPROVAL_DENIED
        - APPROVAL_INVALID
    Ping:
      type: object
      required:
        - pong
        - key_kind
        - scopes
        - lost_scopes
      properties:
        pong:
          type: boolean
          const: true
        key_kind:
          type: string
          enum:
            - secret
          description: BoltClaw keys are secret keys, for servers. There are no browser keys.
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
          description: What the key may do right now.
        lost_scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
          description: |
            Scopes the key was issued with and no longer has, because the person who created it
            lost the permission behind them or left the workspace. Normally empty. Anything here
            explains a `403 INSUFFICIENT_SCOPE` on an operation the key was created for.
    Workspace:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    Scope:
      type: string
      description: |
        What a key may be issued to do. A key carries one or more, and every operation names the
        one it needs. A key never carries more than the person who created it: if they lose the
        permission a scope stands on, the key loses the scope on its next request, and `GET /ping`
        reports it in `lost_scopes`.

        `logs:read` reads the workspace's activity log and the log of what AI assistants did
        (`GET /logs/audit` and `GET /logs/agent-calls`). `workspace:read` gates nothing:
        `GET /ping` and `GET /workspace` need no scope at all.

        `keys:read`, `keys:write`, `webhooks:read` and `webhooks:write` cannot be carried by a key
        an AI assistant holds, whatever is asked for: issuing credentials and pointing your data at
        a new address are things a person does, or a system a person deployed.

        `referrals:embed` is for the server that shows your own users their referrals: it mints panel
        sessions (`POST /referrals/programs/{id}/embed-tokens`), creates, updates and reads referrers,
        and nothing else. It cannot touch a program, a partner, a commission or a payout, so a leaked
        key can show somebody a referral panel and do no more. It cannot be carried by a key an AI
        assistant holds. `referrals:write` is also accepted where this scope is, but gives far more.

        `referrals:payouts` is for your own payout system: it reads where a requested payout goes,
        reports what happened to it, and sends a payout batch without an AI assistant's approval
        step. `referrals:write` alone never sends money. It cannot be carried by a key an AI
        assistant holds.
      enum:
        - pages:read
        - pages:write
        - pages:delete
        - inquiries:read
        - inquiries:write
        - social:read
        - social:write
        - leads:read
        - conversions:write
        - referrals:read
        - referrals:write
        - referrals:embed
        - referrals:payouts
        - keys:read
        - keys:write
        - webhooks:read
        - webhooks:write
        - logs:read
        - workspace:read
    ApiKeyOrigin:
      type: string
      enum:
        - dashboard
        - api
        - device
        - oauth
      description: |
        How the key came to exist: `dashboard` in the BoltClaw dashboard, `api` minted by another
        key through this API, `device` or `oauth` approved for an AI assistant. Never an input to
        what a key may do.
    ApiKey:
      type: object
      required:
        - id
        - name
        - prefix
        - scopes
        - effective_scopes
        - lost_scopes
        - origin
        - client_name
        - last_used_at
        - last_used_ip
        - expires_at
        - rotated_from
        - revoked_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type:
            - string
            - 'null'
          maxLength: 60
        prefix:
          type:
            - string
            - 'null'
          description: |
            The first 8 characters of the key's random body, so a key is recognisable in a list.
            Never the key.
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
          description: What the key was issued to do.
        effective_scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
          description: |
            What the key may do right now: `scopes`, minus anything whose permission the person
            who created it no longer holds. This is what a request is judged against.
        lost_scopes:
          type: array
          items:
            $ref: '#/components/schemas/Scope'
          description: |
            `scopes` minus `effective_scopes`. Normally empty. Anything here means the person who
            created the key lost the permission behind it, or left the workspace: restore their
            permission, or issue the key again from somebody who holds it.
        origin:
          $ref: '#/components/schemas/ApiKeyOrigin'
        client_name:
          type:
            - string
            - 'null'
          maxLength: 120
          description: The client a person approved for an assistant's key. Null for every other key.
        last_used_at:
          type:
            - string
            - 'null'
          format: date-time
        last_used_ip:
          type:
            - string
            - 'null'
          description: |
            The address the key was last seen from, recorded at most once a minute. For your own
            eyes: nothing reads it back, and no decision depends on it.
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the key stops working, or null for a key that does not expire.
        rotated_from:
          type:
            - string
            - 'null'
          format: uuid
          description: The key this one replaced. Rotation makes a new key, with a new id.
        revoked_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            When the key stops working. A value in the FUTURE is a rotation grace window that is
            still open: the key works until then.
        created_at:
          type: string
          format: date-time
    ApiKeyList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ApiKey'
        next_cursor:
          $ref: '#/components/schemas/NullCursor'
    ApiKeyCreate:
      type: object
      required:
        - name
        - scopes
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 60
          description: What this key is for, so it can be told apart from the next one in a list.
        scopes:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/Scope'
          description: |
            Asked for, not granted. A scope the calling key does not itself carry, or whose
            permission its creator does not hold, is left off and listed in `dropped`.
        expires_in_days:
          type: integer
          minimum: 1
          maximum: 365
          description: |
            Omit for a key that does not expire. Clamped to the calling key's own expiry, so a
            child never outlives its parent.
    ApiKeyUpdate:
      type: object
      minProperties: 1
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 60
        scopes:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/Scope'
          description: |
            The new set, in full. The key itself does not change, so whatever holds it keeps
            working.
    ApiKeyRotate:
      type: object
      additionalProperties: false
      properties:
        grace_seconds:
          type: integer
          minimum: 0
          maximum: 86400
          description: |
            How long the old key keeps working, so the new one can reach every deployment first.
            Zero, the default, stops it at once, which is what you want when the old value leaked.
    ApiKeyCreated:
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          required:
            - dropped
            - secret
          properties:
            dropped:
              type: array
              items:
                $ref: '#/components/schemas/Scope'
              description: Scopes that were asked for and not granted. Compare it with `scopes`.
            secret:
              type:
                - string
                - 'null'
              description: |
                The key itself: `bc_live_` and 43 letters and digits. SHOWN ONCE. Only a hash is
                stored and no endpoint can produce it again. Null on an idempotent replay, which
                tells you the key was already created; rotate it if you lost the value.
    ApiKeyUpdated:
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          required:
            - dropped
          properties:
            dropped:
              type: array
              items:
                $ref: '#/components/schemas/Scope'
              description: Scopes that were asked for and not granted.
    ApiKeyRotated:
      allOf:
        - $ref: '#/components/schemas/ApiKeyCreated'
        - type: object
          required:
            - previous_valid_until
          properties:
            previous_valid_until:
              type:
                - string
                - 'null'
              format: date-time
              description: |
                When the key this replaced stops working. Null when it stopped at once.
    Deleted:
      type: object
      required:
        - deleted
        - id
      properties:
        deleted:
          type: boolean
          const: true
        id:
          type: string
          format: uuid
    NullCursor:
      type: 'null'
      description: Always null; this list is never paged.
    NextCursor:
      type:
        - string
        - 'null'
      description: Pass back as `cursor` for the next page; null on the last page.
    Slug:
      type: string
      minLength: 1
      maxLength: 63
      pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
      description: Lowercase letters, digits and single hyphens. Reserved names are refused.
    Theme:
      type: object
      additionalProperties: false
      description: The page's colours and style. Each value is a string of 1 to 64 characters.
      properties:
        background:
          type: string
          minLength: 1
          maxLength: 64
        surface:
          type: string
          minLength: 1
          maxLength: 64
        text:
          type: string
          minLength: 1
          maxLength: 64
        muted:
          type: string
          minLength: 1
          maxLength: 64
        accent:
          type: string
          minLength: 1
          maxLength: 64
        accentText:
          type: string
          minLength: 1
          maxLength: 64
        border:
          type: string
          minLength: 1
          maxLength: 64
        font:
          type: string
          minLength: 1
          maxLength: 64
        buttonStyle:
          type: string
          minLength: 1
          maxLength: 64
        radius:
          type: string
          minLength: 1
          maxLength: 64
    Social:
      type: object
      required:
        - platform
        - url
      additionalProperties: false
      properties:
        platform:
          type: string
          pattern: ^[a-z0-9-]{1,32}$
        url:
          type: string
          maxLength: 2048
          pattern: ^(https?://|mailto:|tel:)
    Page:
      type: object
      required:
        - id
        - slug
        - title
        - bio
        - avatar_url
        - theme
        - socials
        - contact_enabled
        - contact_note
        - official_channel
        - published
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        slug:
          $ref: '#/components/schemas/Slug'
        title:
          type:
            - string
            - 'null'
        bio:
          type:
            - string
            - 'null'
        avatar_url:
          type:
            - string
            - 'null'
          format: uri
        theme:
          $ref: '#/components/schemas/Theme'
        socials:
          type: array
          items:
            $ref: '#/components/schemas/Social'
        contact_enabled:
          type: boolean
        contact_note:
          type:
            - string
            - 'null'
          description: The creator's own words above the message box.
        official_channel:
          type: boolean
          description: The creator's promise that this is the only channel they answer through.
        published:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PageList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Page'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    PageCreate:
      type: object
      required:
        - slug
      additionalProperties: false
      properties:
        slug:
          $ref: '#/components/schemas/Slug'
        title:
          type:
            - string
            - 'null'
          maxLength: 120
          description: 120 characters by default; your organisation's number is `page_title_chars` in `GET /usage`.
        bio:
          type:
            - string
            - 'null'
          maxLength: 600
          description: 600 characters by default; your organisation's number is `page_bio_chars` in `GET /usage`.
        theme:
          $ref: '#/components/schemas/Theme'
        socials:
          type: array
          maxItems: 12
          description: |
            12 icons by default; your organisation's number is `social_icons_per_page` in
            `GET /usage`. A page already holding more may save the same number or fewer.
          items:
            $ref: '#/components/schemas/Social'
        contact_enabled:
          type: boolean
        contact_note:
          type:
            - string
            - 'null'
          maxLength: 300
          description: 300 characters by default; your organisation's number is `contact_note_chars` in `GET /usage`.
        official_channel:
          type: boolean
        published:
          type: boolean
    PageUpdate:
      type: object
      minProperties: 1
      additionalProperties: false
      description: At least one field. The slug cannot change.
      properties:
        title:
          type:
            - string
            - 'null'
          maxLength: 120
          description: 120 characters by default; your organisation's number is `page_title_chars` in `GET /usage`.
        bio:
          type:
            - string
            - 'null'
          maxLength: 600
          description: 600 characters by default; your organisation's number is `page_bio_chars` in `GET /usage`.
        theme:
          $ref: '#/components/schemas/Theme'
        socials:
          type: array
          maxItems: 12
          description: |
            12 icons by default; your organisation's number is `social_icons_per_page` in
            `GET /usage`. A page already holding more may save the same number or fewer.
          items:
            $ref: '#/components/schemas/Social'
        contact_enabled:
          type: boolean
        contact_note:
          type:
            - string
            - 'null'
          maxLength: 300
          description: 300 characters by default; your organisation's number is `contact_note_chars` in `GET /usage`.
        official_channel:
          type: boolean
        published:
          type: boolean
    PageDeleted:
      type: object
      required:
        - deleted
        - slug
        - message
      properties:
        deleted:
          type: boolean
          const: true
        slug:
          $ref: '#/components/schemas/Slug'
        message:
          type: string
    PageStats:
      type: object
      required:
        - slug
        - from
        - to
        - views
        - clicks
        - links
      properties:
        slug:
          $ref: '#/components/schemas/Slug'
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        views:
          type: integer
        clicks:
          type: integer
        links:
          type: array
          items:
            type: object
            required:
              - id
              - label
              - clicks
            properties:
              id:
                type: string
                format: uuid
              label:
                type:
                  - string
                  - 'null'
              clicks:
                type: integer
    Link:
      type: object
      required:
        - id
        - label
        - url
        - icon
        - position
        - visible
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        label:
          type:
            - string
            - 'null'
        url:
          type:
            - string
            - 'null'
        icon:
          type:
            - string
            - 'null'
        position:
          type: integer
        visible:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    LinkList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          maxItems: 5000
          description: |
            At most 1,000 for a page by default: a safety ceiling the same on every plan. A page that
            had more when its ceiling was lowered keeps them, and every one is listed, up to 5,000,
            the most any page can ever hold.
          items:
            $ref: '#/components/schemas/Link'
    LinkCreate:
      type: object
      required:
        - url
      additionalProperties: false
      properties:
        label:
          type:
            - string
            - 'null'
          maxLength: 120
          description: 120 characters by default; your organisation's number is `link_label_chars` in `GET /usage`.
        url:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: ^(https?://|mailto:|tel:)
        icon:
          type:
            - string
            - 'null'
          maxLength: 32
        position:
          type: integer
          minimum: 0
          maximum: 10000
        visible:
          type: boolean
    LinkUpdate:
      type: object
      minProperties: 1
      additionalProperties: false
      properties:
        label:
          type:
            - string
            - 'null'
          maxLength: 120
          description: 120 characters by default; your organisation's number is `link_label_chars` in `GET /usage`.
        url:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: ^(https?://|mailto:|tel:)
        icon:
          type:
            - string
            - 'null'
          maxLength: 32
        position:
          type: integer
          minimum: 0
          maximum: 10000
        visible:
          type: boolean
    InquiryStatus:
      type: string
      enum:
        - new
        - read
        - replied
        - archived
        - spam
    Inquiry:
      type: object
      required:
        - id
        - page_slug
        - channel
        - participant
        - subject
        - status
        - closed_at
        - last_message_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        page_slug:
          type:
            - string
            - 'null'
          description: The page it was started on.
        channel:
          type: string
          enum:
            - page
        participant:
          type:
            - object
            - 'null'
          description: |
            The person who wrote in, as they are known on this page only: `id` is stable on this
            page and unrelated to the same person on any other page. There is no email.
          required:
            - id
            - display_name
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type:
                - string
                - 'null'
        subject:
          type:
            - string
            - 'null'
        status:
          $ref: '#/components/schemas/InquiryStatus'
        closed_at:
          type:
            - string
            - 'null'
          format: date-time
        last_message_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    InquiryList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Inquiry'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    InquiryUpdate:
      type: object
      required:
        - status
      additionalProperties: false
      properties:
        status:
          $ref: '#/components/schemas/InquiryStatus'
    InquiryClose:
      type: object
      required:
        - closed
      additionalProperties: false
      properties:
        closed:
          type: boolean
    Message:
      type: object
      required:
        - id
        - from
        - body
        - read_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        from:
          type: string
          enum:
            - you
            - them
          description: '`you` is the creator''s side, whoever on the team wrote it; `them` is the person who wrote in.'
        body:
          type: string
        read_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    MessageList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Message'
    MessageCreate:
      type: object
      required:
        - body
      additionalProperties: false
      properties:
        body:
          type: string
          minLength: 1
          maxLength: 5000
    MessageSent:
      type: object
      required:
        - id
      properties:
        id:
          type: string
          format: uuid
    MarkedRead:
      type: object
      required:
        - marked_read
      properties:
        marked_read:
          type: integer
          minimum: 0
    WebhookEvent:
      type: string
      description: |
        An event an endpoint can subscribe to. `GET /webhooks/events` returns the live list with a
        description of each; read that rather than hard-coding this enum, because an event added
        later appears there first.
      enum:
        - conversation.message.created
        - social.inbox.item.created
        - social.post.published
        - social.post.failed
        - lead.created
        - referral.partner.applied
        - referral.partner.approved
        - referral.conversion.created
        - referral.commission.created
        - referral.bonus.created
        - referral.referrer.created
        - referral.referrer.conversion
        - referral.referrer.reward_earned
        - referral.referrer.payout_details_changed
        - referral.commission.reversed
        - referral.payout.paid
        - referral.grant.ready
        - referral.grant.fulfilled
        - referral.grant.cancelled
        - referral.grant.reversed
        - referral.referee.invite_ready
        - referral.risk.alert
        - referral.payout.sent
        - referral.payout.failed
        - referral.payout.returned
        - referral.payout.requested
        - referral.tax_form.signed
        - referral.tax_form.expiring
        - referral.invoice.issued
        - referral.invoice.disputed
        - referral.broadcast.sent
        - referral.partner.imported_claimed
        - referral.contest.ended
        - referral.milestone.reached
        - referral.waitlist.entry_created
        - referral.waitlist.position_changed
        - webhook.test
    WebhookEventType:
      type: object
      required:
        - event
        - resource
        - label
        - description
      properties:
        event:
          $ref: '#/components/schemas/WebhookEvent'
        resource:
          type: string
          description: What the event is about, and what you fetch to get the content.
        label:
          type: string
        description:
          type: string
    WebhookEventTypeList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
    Webhook:
      type: object
      required:
        - id
        - url
        - events
        - description
        - page_slugs
        - enabled
        - disabled_at
        - consecutive_failures
        - last_success_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        events:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/WebhookEvent'
          description: Null for every event, now and in future.
        description:
          type:
            - string
            - 'null'
        page_slugs:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Slug'
          description: Null for every page, now and in future.
        enabled:
          type: boolean
          description: False while the endpoint is paused. Events are not queued and are not replayed later.
        disabled_at:
          type:
            - string
            - 'null'
          format: date-time
        consecutive_failures:
          type: integer
        last_success_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        test_events:
          type: boolean
          description: |
            Whether this endpoint also receives TEST events (referral test sales, test-mode and
            sandbox events, `test: true` conversions). A test delivery's body carries
            `"test": true`; a live one never does.
    WebhookList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Webhook'
    WebhookCreate:
      type: object
      additionalProperties: false
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          minLength: 12
          maxLength: 2048
          description: An `https` URL at a public host. Loopback, private ranges and bare IP addresses are refused.
        events:
          type:
            - array
            - 'null'
          maxItems: 100
          items:
            $ref: '#/components/schemas/WebhookEvent'
          description: |
            Which events this endpoint hears. Leave it out, or send null, for every event now and
            in future. An unknown name is refused rather than dropped.
        page_slugs:
          type:
            - array
            - 'null'
          maxItems: 100
          items:
            $ref: '#/components/schemas/Slug'
          description: |
            Which pages this endpoint hears about. Leave it out, or send null, for every page now
            and in future. Events that are not about a page only ever reach an endpoint that
            covers all of them.
        description:
          type:
            - string
            - 'null'
          maxLength: 100
        test_events:
          type: boolean
          description: 'Also receive TEST events, each with `"test": true` in its body (default false).'
    WebhookUpdate:
      type: object
      additionalProperties: false
      description: Send only the fields you want to change. An empty object is `400`.
      properties:
        url:
          type: string
          format: uri
          minLength: 12
          maxLength: 2048
        events:
          type:
            - array
            - 'null'
          maxItems: 100
          items:
            $ref: '#/components/schemas/WebhookEvent'
        page_slugs:
          type:
            - array
            - 'null'
          maxItems: 100
          items:
            $ref: '#/components/schemas/Slug'
        description:
          type:
            - string
            - 'null'
          maxLength: 100
        enabled:
          type: boolean
          description: |
            False pauses the endpoint, keeping it and its signing secret. True resumes it and
            clears the failure count.
        test_events:
          type: boolean
          description: 'Also receive TEST events, each with `"test": true` in its body.'
    WebhookCreated:
      allOf:
        - $ref: '#/components/schemas/Webhook'
        - type: object
          required:
            - secret
            - warning
          properties:
            secret:
              type:
                - string
                - 'null'
              description: |
                The signing secret, shown once. `null` on an idempotent replay: the endpoint
                exists, and the value is not stored anywhere it could be read again. Rotate it if
                you did not record it.
            warning:
              type: string
    WebhookSecretRotated:
      type: object
      required:
        - id
        - secret
        - warning
      properties:
        id:
          type: string
          format: uuid
        secret:
          type:
            - string
            - 'null'
          description: The new signing secret, shown once. `null` on an idempotent replay.
        warning:
          type: string
    WebhookDeleted:
      type: object
      required:
        - deleted
        - id
      properties:
        deleted:
          type: boolean
          enum:
            - true
        id:
          type: string
          format: uuid
    WebhookDelivery:
      type: object
      required:
        - id
        - delivery_id
        - event
        - attempt
        - payload
        - status
        - response_status
        - response_excerpt
        - error
        - duration_ms
        - created_at
      properties:
        id:
          type: string
          format: uuid
        delivery_id:
          type: string
          format: uuid
          description: |
            Stable across every attempt of one occurrence, and the value the `X-BoltClaw-Delivery`
            header carries. Key your idempotency off it.
        event:
          $ref: '#/components/schemas/WebhookEvent'
        attempt:
          type: integer
          minimum: 1
        payload:
          type: object
          additionalProperties: true
          description: The exact body that was signed and sent. Identifiers only, by design.
        status:
          type: string
          enum:
            - delivered
            - failed
        response_status:
          type:
            - integer
            - 'null'
        response_excerpt:
          type:
            - string
            - 'null'
          description: |
            At most 512 bytes of what your endpoint answered. Null when there was nothing to keep,
            or when the address finally reached was not one we would deliver to.
        error:
          type:
            - string
            - 'null'
          description: |
            Why it failed, as one of a fixed set of sentences. Null when it succeeded. Never your
            own server's words.
        duration_ms:
          type:
            - integer
            - 'null'
        created_at:
          type: string
          format: date-time
    WebhookDeliveryList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDelivery'
    WebhookQueued:
      type: object
      required:
        - id
        - status
        - delivery_id
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
        delivery_id:
          type: string
          format: uuid
          description: The new occurrence id. The body is the one that was frozen when the event happened.
    WebhookTestQueued:
      type: object
      required:
        - id
        - event
        - status
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          enum:
            - webhook.test
        status:
          type: string
          enum:
            - pending
    EmptyBody:
      type: object
      additionalProperties: false
      description: This operation takes no fields. Send `{}`, or no body at all.
    Network:
      type: string
      enum:
        - instagram
        - facebook
        - threads
        - bluesky
        - x
        - tiktok
        - linkedin
        - youtube
        - pinterest
    Capabilities:
      type: object
      required:
        - publish
        - comments
        - hide_comment
        - dms
        - private_reply
        - webhooks
        - max_body_length
        - max_media
        - text_only
        - video
      properties:
        publish:
          type: boolean
        comments:
          type: boolean
        hide_comment:
          type: boolean
        dms:
          type: boolean
        private_reply:
          type: boolean
        webhooks:
          type: boolean
        max_body_length:
          type: integer
        max_media:
          type: integer
        text_only:
          type: boolean
        video:
          type: boolean
    Account:
      type: object
      required:
        - id
        - network
        - handle
        - display_name
        - avatar_url
        - status
        - last_error
        - connected_at
        - capabilities
      properties:
        id:
          type: string
          format: uuid
        network:
          $ref: '#/components/schemas/Network'
        handle:
          type:
            - string
            - 'null'
        display_name:
          type:
            - string
            - 'null'
        avatar_url:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - active
            - needs_reauth
            - disconnected
        last_error:
          type:
            - string
            - 'null'
        connected_at:
          type: string
          format: date-time
        capabilities:
          $ref: '#/components/schemas/Capabilities'
    AccountList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Account'
        next_cursor:
          $ref: '#/components/schemas/NullCursor'
    Media:
      type: object
      required:
        - id
        - mime_type
        - bytes
        - url
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
        mime_type:
          type: string
        bytes:
          type:
            - integer
            - 'null'
        url:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - pending
            - ready
        created_at:
          type: string
          format: date-time
    MediaCreate:
      type: object
      required:
        - mime_type
        - filename
        - bytes
      additionalProperties: false
      properties:
        mime_type:
          type: string
          enum:
            - image/jpeg
            - image/png
            - image/webp
            - image/gif
            - video/mp4
            - video/quicktime
        filename:
          type: string
          minLength: 1
          maxLength: 255
        bytes:
          type: integer
          minimum: 1
          maximum: 536870912
    MediaUpload:
      type: object
      required:
        - media
        - upload
      properties:
        media:
          $ref: '#/components/schemas/Media'
        upload:
          type:
            - object
            - 'null'
          description: Where to upload the file, once. Null on an idempotent replay.
          required:
            - signed_url
            - token
            - path
          properties:
            signed_url:
              type: string
            token:
              type: string
            path:
              type: string
    PostStatus:
      type: string
      enum:
        - draft
        - scheduled
        - publishing
        - published
        - partial
        - failed
        - cancelled
    Target:
      type: object
      required:
        - id
        - account_id
        - network
        - handle
        - status
        - external_url
        - last_error
        - published_at
        - metrics
      properties:
        id:
          type: string
          format: uuid
        account_id:
          type: string
          format: uuid
        network:
          $ref: '#/components/schemas/Network'
        handle:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - pending
            - publishing
            - processing
            - published
            - failed
            - cancelled
        external_url:
          type:
            - string
            - 'null'
        last_error:
          type:
            - string
            - 'null'
        published_at:
          type:
            - string
            - 'null'
          format: date-time
        metrics:
          type:
            - object
            - 'null'
          additionalProperties:
            type: number
          description: The network's own counters, by the network's own names.
    PostOptions:
      type: object
      description: Per-network options, keyed by network, in each network's own vocabulary.
      propertyNames:
        $ref: '#/components/schemas/Network'
      additionalProperties:
        type: object
    Post:
      type: object
      required:
        - id
        - body
        - media
        - options
        - status
        - scheduled_for
        - published_at
        - source
        - created_at
        - updated_at
        - targets
      properties:
        id:
          type: string
          format: uuid
        body:
          type: string
        media:
          type: array
          items:
            $ref: '#/components/schemas/Media'
        options:
          $ref: '#/components/schemas/PostOptions'
        status:
          $ref: '#/components/schemas/PostStatus'
        scheduled_for:
          type:
            - string
            - 'null'
          format: date-time
        published_at:
          type:
            - string
            - 'null'
          format: date-time
        source:
          type: string
          enum:
            - dashboard
            - api
            - mcp
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        targets:
          type: array
          items:
            $ref: '#/components/schemas/Target'
    PostList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Post'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    PostCreate:
      type: object
      required:
        - account_ids
      additionalProperties: false
      description: |
        Exactly one of `scheduled_for`, `publish_now: true` or `draft: true`, and a `body` or at
        least one media id.
      properties:
        body:
          type: string
          maxLength: 5000
        media_ids:
          type: array
          maxItems: 20
          items:
            type: string
            format: uuid
        account_ids:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            format: uuid
        scheduled_for:
          type: string
          format: date-time
        publish_now:
          type: boolean
        draft:
          type: boolean
        options:
          $ref: '#/components/schemas/PostOptions'
    PostUpdate:
      type: object
      minProperties: 1
      additionalProperties: false
      properties:
        body:
          type: string
          maxLength: 5000
        media_ids:
          type: array
          maxItems: 20
          items:
            type: string
            format: uuid
        account_ids:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            format: uuid
        scheduled_for:
          type:
            - string
            - 'null'
          format: date-time
        options:
          $ref: '#/components/schemas/PostOptions'
    InboxKind:
      type: string
      enum:
        - comment
        - dm
        - mention
    InboxItem:
      type: object
      required:
        - id
        - account_id
        - network
        - kind
        - direction
        - thread_key
        - post_external_id
        - author_handle
        - author_name
        - body
        - permalink
        - status
        - read_at
        - replied_at
        - occurred_at
        - automation_id
      properties:
        id:
          type: string
          format: uuid
        account_id:
          type: string
          format: uuid
        network:
          $ref: '#/components/schemas/Network'
        kind:
          $ref: '#/components/schemas/InboxKind'
        direction:
          type: string
          enum:
            - inbound
            - outbound
        thread_key:
          type: string
        post_external_id:
          type:
            - string
            - 'null'
        author_handle:
          type:
            - string
            - 'null'
        author_name:
          type:
            - string
            - 'null'
        body:
          type: string
        permalink:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - open
            - done
            - hidden
        read_at:
          type:
            - string
            - 'null'
          format: date-time
        replied_at:
          type:
            - string
            - 'null'
          format: date-time
        occurred_at:
          type: string
          format: date-time
        automation_id:
          type:
            - string
            - 'null'
          format: uuid
    InboxList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/InboxItem'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    InboxThread:
      type: object
      required:
        - item
        - thread
      properties:
        item:
          $ref: '#/components/schemas/InboxItem'
        thread:
          type: array
          items:
            $ref: '#/components/schemas/InboxItem'
    InboxStatusUpdate:
      type: object
      required:
        - status
      additionalProperties: false
      properties:
        status:
          type: string
          enum:
            - open
            - done
    InboxReply:
      type: object
      required:
        - text
      additionalProperties: false
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 5000
          description: Trimmed; not blank.
    InboxHide:
      type: object
      additionalProperties: false
      properties:
        hidden:
          type: boolean
          default: true
    Automation:
      type: object
      required:
        - id
        - account_id
        - network
        - name
        - trigger
        - keywords
        - match_mode
        - post_external_ids
        - public_replies
        - dm_message
        - link_url
        - link_label
        - collect
        - collect_prompt
        - use_ai
        - enabled
        - triggered_count
        - dms_sent
        - leads_captured
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        account_id:
          type: string
          format: uuid
        network:
          $ref: '#/components/schemas/Network'
        name:
          type: string
        trigger:
          type: string
          enum:
            - comment_keyword
            - dm_keyword
        keywords:
          type: array
          items:
            type: string
        match_mode:
          type: string
          enum:
            - contains
            - exact
        post_external_ids:
          type:
            - array
            - 'null'
          items:
            type: string
        public_replies:
          type: array
          items:
            type: string
        dm_message:
          type: string
        link_url:
          type:
            - string
            - 'null'
        link_label:
          type:
            - string
            - 'null'
        collect:
          type: string
          enum:
            - none
            - email
            - phone
        collect_prompt:
          type:
            - string
            - 'null'
        use_ai:
          type: boolean
        enabled:
          type: boolean
        triggered_count:
          type: integer
        dms_sent:
          type: integer
        leads_captured:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AutomationList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Automation'
        next_cursor:
          $ref: '#/components/schemas/NullCursor'
    AutomationFields:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 80
        dm_message:
          type: string
          minLength: 1
          maxLength: 1000
        trigger:
          type: string
          enum:
            - comment_keyword
            - dm_keyword
        keywords:
          type: array
          maxItems: 20
          items:
            type: string
            minLength: 1
            maxLength: 100
        match_mode:
          type: string
          enum:
            - contains
            - exact
        post_external_ids:
          type:
            - array
            - 'null'
          maxItems: 50
          items:
            type: string
            minLength: 1
            maxLength: 200
        public_replies:
          type: array
          maxItems: 10
          items:
            type: string
            minLength: 1
            maxLength: 1000
        link_url:
          type:
            - string
            - 'null'
          maxLength: 2048
          pattern: ^[Hh][Tt][Tt][Pp][Ss]?://
        link_label:
          type:
            - string
            - 'null'
          maxLength: 80
        collect:
          type: string
          enum:
            - none
            - email
            - phone
        collect_prompt:
          type:
            - string
            - 'null'
          maxLength: 1000
        use_ai:
          type: boolean
        enabled:
          type: boolean
    AutomationCreate:
      allOf:
        - $ref: '#/components/schemas/AutomationFields'
        - type: object
          required:
            - account_id
            - name
            - dm_message
          properties:
            account_id:
              type: string
              format: uuid
      unevaluatedProperties: false
    AutomationUpdate:
      allOf:
        - $ref: '#/components/schemas/AutomationFields'
      minProperties: 1
      unevaluatedProperties: false
      description: At least one field. The account cannot change.
    Lead:
      type: object
      required:
        - id
        - account_id
        - automation_id
        - network
        - handle
        - name
        - email
        - phone
        - source
        - created_at
      properties:
        id:
          type: string
          format: uuid
        account_id:
          type:
            - string
            - 'null'
          format: uuid
        automation_id:
          type:
            - string
            - 'null'
          format: uuid
        network:
          oneOf:
            - $ref: '#/components/schemas/Network'
            - type: 'null'
        handle:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        source:
          type: string
          enum:
            - comment_to_dm
            - dm
            - api
        created_at:
          type: string
          format: date-time
    LeadList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Lead'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    Usage:
      type: object
      required:
        - plan
        - period_start
        - limits
        - usage
        - social_accounts
        - networks
        - platform_charges
      properties:
        plan:
          type: string
        period_start:
          type: string
          format: date
        limits:
          type: object
          additionalProperties:
            type: integer
          description: |
            Each limit by name, such as `posts_per_month`: the number that applies to your
            organisation right now, including any we have raised for it. Besides the allowances it
            carries the field sizes and ceilings a write is held to (`page_title_chars`,
            `page_bio_chars`, `link_label_chars`, `contact_note_chars`, `social_icons_per_page`,
            `links_per_page`, `webhook_endpoints`) and the hourly budgets
            (`api_pages_created_per_hour`, `api_messages_per_hour`, `thread_messages_per_hour`).
            AI is two allowances: `ai_credits_per_month`, one credit per AI draft (paid plans; 0
            on Free), spent first, then `ai_credits_per_day`, where 1 credit is 10 tokens of what
            the AI reads and writes (3,000 on Free, about 70 for a reply draft); and
            `ai_requests_per_minute` drafts in any 60 seconds.
        usage:
          type: object
          additionalProperties:
            type: integer
          description: |
            This month's use, by allowance name (`ai_credits` counts this month's AI drafts), and
            `ai_credits_today`: today's AI credits, by UTC date, against `ai_credits_per_day`.
            They reset at midnight UTC and what is left is not carried over.
        social_accounts:
          type: integer
        networks:
          type: array
          items:
            $ref: '#/components/schemas/Network'
        platform_charges:
          type:
            - object
            - 'null'
          required:
            - count
            - billed_mills
            - by_operation
          description: |
            This month's per-call charges billed on top of the plan, in thousandths of a US dollar.
            `null` unless the person who created the key may manage the organisation's billing:
            the charges are the organisation's bill, not the workspace's.
          properties:
            count:
              type: integer
            billed_mills:
              type: integer
            by_operation:
              type: object
              description: Keyed by the network operation's own name.
              additionalProperties:
                type: object
                required:
                  - count
                  - billed_mills
                properties:
                  count:
                    type: integer
                  billed_mills:
                    type: integer
    CommissionStatus:
      type: string
      enum:
        - pending
        - approved
        - payable
        - paid
        - reversed
        - fraud
    ConversionEvent:
      type: string
      enum:
        - lead
        - sale
        - refund
        - dispute
    Conversion:
      type: object
      required:
        - id
        - event
        - amount_minor
        - currency
        - occurred_at
        - attributed
        - partner
        - commission
        - commissions
        - duplicate
      properties:
        id:
          type: string
          format: uuid
        event:
          $ref: '#/components/schemas/ConversionEvent'
        amount_minor:
          type: integer
          description: Minor units. 0 for a lead; for a refund, this refund alone.
        currency:
          type:
            - string
            - 'null'
        occurred_at:
          type: string
          format: date-time
        attributed:
          type: boolean
          description: Whether a partner was credited.
        partner:
          type:
            - object
            - 'null'
          required:
            - display_name
          properties:
            display_name:
              type: string
        commission:
          type:
            - object
            - 'null'
          required:
            - amount_minor
            - currency
            - status
          properties:
            amount_minor:
              type: integer
            currency:
              type: string
            status:
              $ref: '#/components/schemas/CommissionStatus'
        commissions:
          type: array
          description: |
            Every commission the conversion created, net of reversals, each with why it is that
            amount. A sale whose items matched two reward rules earns two; `commission` is the
            first of them.
          items:
            type: object
            required:
              - amount_minor
              - currency
              - status
              - basis_text
            properties:
              amount_minor:
                type: integer
              currency:
                type: string
              status:
                $ref: '#/components/schemas/CommissionStatus'
              basis_text:
                type: string
                description: Why this amount, in one sentence.
        duplicate:
          type: boolean
          description: True when this event_id had been reported before and nothing changed.
        grants:
          type: array
          description: |
            The rewards that are not cash this conversion gave, to the promoter or to the friend,
            each in words. A friend's discount is here only when the sale shows it was used.
          items:
            type: object
            required:
              - beneficiary
              - reward_type
              - status
              - summary
            properties:
              beneficiary:
                type: string
                enum:
                  - promoter
                  - referee
              reward_type:
                $ref: '#/components/schemas/GrantType'
              status:
                $ref: '#/components/schemas/GrantStatus'
              summary:
                type: string
                description: 'What it is, in a few words: "1 month free".'
        referee_referrer:
          type:
            - object
            - 'null'
          description: |
            Invite after purchase: the friend who just made their first payment now has a referral
            link of their own, so your thank-you page can ask them to invite their friends. Null
            unless the program asks (`invite_after_purchase`), you named the customer by your own
            id, and the link exists.
          required:
            - link
            - code
          properties:
            link:
              type: string
            code:
              type: string
        referral_refused:
          type:
            - string
            - 'null'
          enum:
            - late_code
            - null
          description: |
            A referral code that was sent and not applied, and why: `late_code` when it came more
            than the program's `late_code_days` after the customer signed up. Null otherwise.
        test:
          type: boolean
          description: |
            TEST data: a test sale, `test: true`, or a test-mode or sandbox event from your billing
            system. Credited to the program's test partner only; never payable, never counted.
    ConversionCreate:
      type: object
      required:
        - program_id
        - event
        - event_id
      additionalProperties: false
      properties:
        program_id:
          type: string
          format: uuid
        event:
          $ref: '#/components/schemas/ConversionEvent'
        event_id:
          type: string
          minLength: 1
          maxLength: 255
          description: |
            YOUR id for this event, and the idempotency key. Reporting it again changes nothing, so
            a retry sends the same one; a new one is a new conversion.
        occurred_at:
          type: string
          format: date-time
        customer:
          type: object
          required:
            - id
          additionalProperties: false
          properties:
            id:
              type: string
              minLength: 1
              maxLength: 255
            email:
              type: string
              minLength: 3
              maxLength: 320
              description: Hashed on arrival and never stored.
        referral:
          type: object
          additionalProperties: false
          properties:
            code:
              type: string
              maxLength: 64
            discount_code:
              type: string
              maxLength: 64
            discount_applied:
              type: boolean
              description: |
                True when the friend's referral discount was applied to this sale in your own
                checkout. A friend's discount is recorded only on evidence it was used: this, a
                discount code, or (from Stripe) a coupon or promotion code on the payment.
            sub:
              type: string
              maxLength: 80
              description: |
                The partner's campaign tag the website tag kept (`BoltClawReferral.sub()`, from
                `/r/<code>?sub=yt`): lower-case letters, digits, `_` and `-`. Shown in the
                partner's results by tag; ignored when it is not one. From Stripe, metadata
                `boltclaw_sub`.
            click_id:
              type: string
              maxLength: 32
              description: |
                The click id the website tag kept (`BoltClawReferral.clickId()`, 16 letters and digits), so
                the time from the click to this conversion counts in the risk score. Ignored when it
                is not one. From Stripe, `client_reference_id` as `<code>--<click id>` or
                `metadata.boltclaw_click` does the same.
        amount:
          type: integer
          minimum: 0
          description: Minor units.
        currency:
          type: string
          pattern: ^[A-Za-z]{3}$
        refund_of:
          type: string
          minLength: 1
          maxLength: 255
          description: The sale's event_id.
        items:
          type: array
          maxItems: 100
          description: |
            A sale's lines, so a reward rule can pay per product, price, plan or interval. Their
            `amount`s must add up to the sale's `amount` (`400` otherwise). Leave them out and the
            sale is matched as a whole.
          items:
            $ref: '#/components/schemas/SaleItem'
        test:
          type: boolean
          description: |
            Record this as TEST data: credited to the program's test partner whoever's code it
            carries, never payable, never counted toward a limit or a total, emailing nobody, and
            sent only to webhook endpoints that receive test events. A test `event_id` and a live
            one never collide, and neither do test and live customers. Deleted with `DELETE
            /v1/referrals/programs/{id}/test-data`.
    SaleItem:
      type: object
      required:
        - amount
      additionalProperties: false
      properties:
        product:
          type: string
          maxLength: 200
          description: Your product id, or Stripe's `prod_…`.
        price:
          type: string
          maxLength: 200
          description: Your price or plan id, or Stripe's `price_…`.
        plan:
          type: string
          maxLength: 200
          description: A plan name in your own words, matched without regard to case.
        interval:
          type: string
          enum:
            - month
            - year
            - one_time
        quantity:
          type: integer
          minimum: 1
        amount:
          type: integer
          minimum: 0
          description: This line's share of the sale, minor units.
    IntegrationStatus:
      type: object
      required:
        - program_id
        - checked_at
        - overall
        - parts
        - integrations
        - test_conversions
      properties:
        program_id:
          type: string
          format: uuid
        checked_at:
          type: string
          format: date-time
        overall:
          $ref: '#/components/schemas/SetupState'
        parts:
          type: array
          items:
            $ref: '#/components/schemas/SetupPart'
        integrations:
          type: array
          items:
            $ref: '#/components/schemas/Integration'
        test_conversions:
          type: integer
          description: Test conversions the program holds now. `DELETE /v1/referrals/programs/{id}/test-data` deletes them.
    SetupState:
      type: string
      enum:
        - connected
        - warnings
        - not_connected
    SetupPart:
      type: object
      required:
        - part
        - label
        - state
        - skipped
        - summary
        - fix
        - warnings
      properties:
        part:
          type: string
          enum:
            - tag
            - billing
            - signups
            - sales
            - codes
            - panel
            - welcome
        label:
          type: string
        state:
          $ref: '#/components/schemas/SetupState'
        skipped:
          type: boolean
          description: The program's own setup does not use this part; it does not count toward `overall`.
        summary:
          type: string
          description: One sentence about what was seen
          or not.: null
        fix:
          type:
            - string
            - 'null'
          description: What to do next; null when nothing is needed.
        warnings:
          type: array
          items:
            type: object
            required:
              - code
              - message
              - fix
            properties:
              code:
                type: string
              message:
                type: string
              fix:
                type: string
    TagCheck:
      type: object
      required:
        - result
        - version
        - consent
        - payment_links
        - sentence
        - checked_at
      properties:
        result:
          type: string
          enum:
            - found_v2
            - found_v1
            - not_found
            - refused
            - unreachable
        version:
          type:
            - string
            - 'null'
          enum:
            - v1
            - v2
            - null
        consent:
          type:
            - boolean
            - 'null'
          description: Whether the tag waits for consent (`data-consent="required"`).
        payment_links:
          type:
            - string
            - 'null'
          enum:
            - none
            - ready
            - missing
            - null
          description: |
            Stripe Payment Links or a Buy Button on the page: `none`, `ready` (the version 2 tag
            has `data-payment-links="auto"`, which is the whole integration for them) or `missing`
            (there are some and the tag does not reach them; the sentence says what to add).
            Null when the page was not read.
        sentence:
          type: string
          description: A fixed sentence to show.
        checked_at:
          type: string
          format: date-time
    TestSale:
      type: object
      required:
        - run_id
        - steps
      properties:
        run_id:
          type: string
        steps:
          type: array
          items:
            $ref: '#/components/schemas/TestSaleStep'
    TestSaleStep:
      type: object
      required:
        - step
        - run_id
        - summary
        - partner
        - conversion
        - offer
        - webhooks
        - test
      properties:
        step:
          type: string
          enum:
            - partner
            - click
            - lead
            - sale
            - refund
        run_id:
          type: string
        summary:
          type: string
        partner:
          type:
            - object
            - 'null'
          required:
            - id
            - display_name
            - link
            - code
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type: string
            link:
              type: string
              description: 'The test partner''s link. Archived: no visitor can open it.'
            code:
              type: string
        conversion:
          oneOf:
            - $ref: '#/components/schemas/Conversion'
            - type: 'null'
        offer:
          oneOf:
            - $ref: '#/components/schemas/Offer'
            - type: 'null'
        webhooks:
          type: object
          required:
            - queued
            - test_endpoints
          properties:
            queued:
              type: integer
              description: Deliveries this step queued for endpoints that receive test events.
            test_endpoints:
              type: integer
        test:
          type: boolean
          enum:
            - true
    TestDataCleared:
      type: object
      required:
        - conversions
        - commissions
        - grants
        - customers
        - clicks
        - partners
      properties:
        conversions:
          type: integer
        commissions:
          type: integer
        grants:
          type: integer
        customers:
          type: integer
        clicks:
          type: integer
        partners:
          type: integer
    ReferralClickCreate:
      type: object
      required:
        - code
      additionalProperties: false
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 64
          description: The partner link's code (`?via=`).
        ip:
          type: string
          maxLength: 64
          description: The visitor's IP address
          as your server saw it. Hashed: null
          never stored.: null
        user_agent:
          type: string
          maxLength: 1024
          description: Classed as mobile
          tablet: null
          desktop or a robot; never stored.: null
        referrer:
          type: string
          maxLength: 2048
          description: The referring page. Only its host is kept.
        country:
          type: string
          maxLength: 8
          description: An ISO 3166 two-letter country code, if your edge sets one.
        landing_path:
          type: string
          maxLength: 2048
          description: The path the visitor landed on. Not stored.
    ReferralClickRecorded:
      type: object
      required:
        - recorded
        - reason
        - program_id
      properties:
        recorded:
          type: boolean
        reason:
          type:
            - string
            - 'null'
          enum:
            - bot
            - allowance
            - null
        program_id:
          type: string
          format: uuid
    FlagRule:
      type: string
      description: |
        Which rule raised a flag. `risk_review` is the risk engine asking a person (the score
        reached the program's `review_at`); `self_referral_card` is one card paying for the
        promoter's own customers; `payout_change` is a payout built inside the window after a
        partner changed where they are paid; `manual_hold` is "Hold this promoter's payouts".
        `other` is a rule this version of the API does not name yet.
      enum:
        - self_referral
        - click_velocity
        - duplicate_payout
        - risk_review
        - self_referral_card
        - payout_change
        - manual_hold
        - other
    Flag:
      type: object
      required:
        - id
        - rule
        - score
        - status
        - subject
        - created_at
        - resolved_at
        - note
        - partner
        - commission
        - conversion
        - link
      properties:
        id:
          type: string
          format: uuid
        rule:
          $ref: '#/components/schemas/FlagRule'
        score:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 100
          description: The risk score behind a `risk_review` flag; null for the other rules.
        status:
          type: string
          enum:
            - open
            - dismissed
            - confirmed
        subject:
          type: string
          enum:
            - click
            - partner
            - conversion
            - commission
        created_at:
          type: string
          format: date-time
        resolved_at:
          type:
            - string
            - 'null'
          format: date-time
        note:
          type:
            - string
            - 'null'
          description: Your note
          written when it was decided.: null
        partner:
          type:
            - object
            - 'null'
          description: The partner it is about, by their name in this program.
          required:
            - id
            - display_name
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type: string
        commission:
          type:
            - object
            - 'null'
          required:
            - id
            - status
            - amount_minor
            - currency
          properties:
            id:
              type: string
              format: uuid
            status:
              $ref: '#/components/schemas/CommissionStatus'
            amount_minor:
              type: integer
            currency:
              type: string
        conversion:
          type:
            - object
            - 'null'
          required:
            - id
            - event
            - amount_minor
            - currency
            - occurred_at
            - customer_external_id
          properties:
            id:
              type: string
              format: uuid
            event:
              $ref: '#/components/schemas/ConversionEvent'
            amount_minor:
              type: integer
            currency:
              type:
                - string
                - 'null'
            occurred_at:
              type: string
              format: date-time
            customer_external_id:
              type:
                - string
                - 'null'
              description: Your own id for the customer.
        link:
          type:
            - object
            - 'null'
          description: A velocity flag's link, by its code and type.
          required:
            - code
            - kind
            - placement
          properties:
            code:
              type: string
            kind:
              type: string
              enum:
                - primary
                - placement
            placement:
              type:
                - string
                - 'null'
              enum:
                - bio
                - post
                - automation
                - manual
                - null
    FlagList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          maxItems: 200
          items:
            $ref: '#/components/schemas/Flag'
    FlagDetail:
      type: object
      required:
        - flag
        - score
        - reasons
        - assessed_at
        - timeline
        - history
      properties:
        flag:
          $ref: '#/components/schemas/Flag'
        score:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 100
        reasons:
          type: array
          description: Every rule that counted, with its weight and what it saw, as a sentence.
          items:
            type: object
            required:
              - code
              - weight
              - sentence
            properties:
              code:
                type: string
                description: The rule, for example `fast_conversion`, `datacenter_click`, `paid_ads`, `code_leak_spike`.
              weight:
                type: integer
              sentence:
                type: string
                description: 'What was seen, in counts and times: never an address, a card or an email.'
        assessed_at:
          type:
            - string
            - 'null'
          format: date-time
        timeline:
          type: array
          description: The customer's click, signup, sale, refunds and disputes in order, with the seconds between them.
          items:
            type: object
            required:
              - kind
              - at
              - seconds_after_previous
              - amount_minor
              - currency
              - flagged
            properties:
              kind:
                type: string
                enum:
                  - click
                  - lead
                  - sale
                  - refund
                  - dispute
              at:
                type: string
                format: date-time
              seconds_after_previous:
                type:
                  - integer
                  - 'null'
              amount_minor:
                type:
                  - integer
                  - 'null'
              currency:
                type:
                  - string
                  - 'null'
              flagged:
                type: boolean
                description: The step this flag is about.
        history:
          type:
            - object
            - 'null'
          description: The partner's history in this program.
          required:
            - age_days
            - clicks30
            - customers30
            - conversion_rate_bps
            - sales60
            - refunded60
            - other_flags
          properties:
            age_days:
              type:
                - integer
                - 'null'
              description: Days since they were approved (or applied).
            clicks30:
              type: integer
            customers30:
              type: integer
            conversion_rate_bps:
              type:
                - integer
                - 'null'
              description: Customers per counted click
              in basis points.: null
            sales60:
              type: integer
            refunded60:
              type: integer
            other_flags:
              type: object
              required:
                - open
                - dismissed
                - confirmed
              properties:
                open:
                  type: integer
                dismissed:
                  type: integer
                confirmed:
                  type: integer
    FlagDecision:
      type: object
      additionalProperties: false
      properties:
        note:
          type:
            - string
            - 'null'
          maxLength: 500
          description: Kept on the flag and in the activity log.
    FlagConfirm:
      type: object
      additionalProperties: false
      properties:
        note:
          type:
            - string
            - 'null'
          maxLength: 500
        ban_partner:
          type: boolean
          description: Ban the partner from the program too (default false).
    FlagsBulk:
      type: object
      additionalProperties: false
      required:
        - flag_ids
        - note
      properties:
        flag_ids:
          type: array
          minItems: 1
          maxItems: 200
          items:
            type: string
            format: uuid
        note:
          type: string
          minLength: 1
          maxLength: 500
          description: One reason for all of them.
    FlagsBulkConfirm:
      type: object
      additionalProperties: false
      required:
        - flag_ids
        - note
      properties:
        flag_ids:
          type: array
          minItems: 1
          maxItems: 200
          items:
            type: string
            format: uuid
        note:
          type: string
          minLength: 1
          maxLength: 500
          description: One reason for all of them.
        ban_partners:
          type: boolean
          description: Ban each flag's partner too (default false).
    FlagResolution:
      type: object
      required:
        - flag
        - commissions
        - banned
      properties:
        flag:
          $ref: '#/components/schemas/Flag'
        commissions:
          type: integer
          description: Commissions this decision moved to `fraud`; 0 for a dismissal.
        banned:
          type: boolean
    FlagsResolution:
      type: object
      required:
        - decided
        - commissions
        - grants
        - banned
        - skipped
      properties:
        decided:
          type: integer
        commissions:
          type: integer
          description: Commissions moved to `fraud`.
        grants:
          type: integer
          description: Rewards that are not cash
          cancelled before they were given.: null
        banned:
          type: integer
        skipped:
          type: array
          items:
            type: object
            required:
              - flag_id
              - status
            properties:
              flag_id:
                type: string
              status:
                type: string
                enum:
                  - not_found
                  - not_open
                  - in_payout
    PayoutHold:
      type: object
      required:
        - flag
        - already
      properties:
        flag:
          $ref: '#/components/schemas/Flag'
        already:
          type: boolean
          description: True when the partner was already held.
    RiskPolicy:
      type: object
      required:
        - hold_at
        - review_at
        - extra_hold_days
        - min_seconds_lead
        - min_seconds_sale
        - min_seconds_shop
        - allow_paid_search
        - campaign_allowlist
        - hold_new_partners
      properties:
        hold_at:
          type: integer
          description: From this score, a conversion's earnings wait `extra_hold_days` longer.
        review_at:
          type: integer
          description: From this score a person is asked (`risk_review`).
        extra_hold_days:
          type: integer
        min_seconds_lead:
          type: integer
          description: Fewer seconds from click to signup than this look automated.
        min_seconds_sale:
          type: integer
        min_seconds_shop:
          type: integer
          description: The same for a shop order.
        allow_paid_search:
          type: boolean
          description: False when partners may not bid on your brand in paid search.
        campaign_allowlist:
          type: array
          items:
            type: string
          description: Campaign ids that are allowed when paid search is not.
        hold_new_partners:
          type: boolean
          description: A promoter's first days in the program (90 by default) count toward the score, and their earnings wait a little longer. On unless you switch it off.
    RiskPolicyFields:
      type: object
      additionalProperties: false
      properties:
        hold_at:
          type: integer
          minimum: 1
          maximum: 100
        review_at:
          type: integer
          minimum: 1
          maximum: 100
          description: Never below `hold_at`.
        extra_hold_days:
          type: integer
          minimum: 0
          maximum: 365
        min_seconds_lead:
          type: integer
          minimum: 0
          maximum: 3600
        min_seconds_sale:
          type: integer
          minimum: 0
          maximum: 3600
        min_seconds_shop:
          type: integer
          minimum: 0
          maximum: 3600
        allow_paid_search:
          type: boolean
        campaign_allowlist:
          type: array
          maxItems: 50
          items:
            type: string
            pattern: ^[A-Za-z0-9_.:-]{1,100}$
        hold_new_partners:
          type: boolean
    RiskPolicyView:
      type: object
      required:
        - program_id
        - policy
        - custom
        - alert_email
      properties:
        program_id:
          type: string
          format: uuid
        policy:
          $ref: '#/components/schemas/RiskPolicy'
        custom:
          $ref: '#/components/schemas/RiskPolicyFields'
        alert_email:
          type: boolean
          description: Email the program's managers when a promoter needs a look (at most once per promoter a day).
    RiskPolicyPatch:
      type: object
      additionalProperties: false
      description: Only the fields you send change. A field sent as null goes back to the platform's value.
      properties:
        hold_at:
          type:
            - integer
            - 'null'
          minimum: 1
          maximum: 100
        review_at:
          type:
            - integer
            - 'null'
          minimum: 1
          maximum: 100
          description: Never below `hold_at`.
        extra_hold_days:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 365
        min_seconds_lead:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 3600
        min_seconds_sale:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 3600
        min_seconds_shop:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 3600
        allow_paid_search:
          type:
            - boolean
            - 'null'
        campaign_allowlist:
          type:
            - array
            - 'null'
          maxItems: 50
          items:
            type: string
            pattern: ^[A-Za-z0-9_.:-]{1,100}$
        hold_new_partners:
          type:
            - boolean
            - 'null'
    RiskPolicyUpdate:
      type: object
      additionalProperties: false
      properties:
        policy:
          oneOf:
            - $ref: '#/components/schemas/RiskPolicyPatch'
            - type: 'null'
          description: |
            The fields to change: only those you send change, and one sent as null goes back to the
            platform's value. `policy: null` clears every field the program set.
        alert_email:
          type: boolean
    PartnerApplication:
      type: object
      description: |
        What the partner wrote when they applied, and what the checks saw. Counts only: never which
        other programs, which accounts, or an address.
      required:
        - questions
        - answers
        - website
        - account_age_days
        - templated
        - burst
      properties:
        questions:
          type: array
          maxItems: 3
          items:
            type: string
          description: The program's questions as the partner was asked them; empty when it asked none.
        answers:
          type: array
          maxItems: 3
          items:
            type: string
          description: In the order of `questions`.
        website:
          type:
            - string
            - 'null'
        account_age_days:
          type:
            - integer
            - 'null'
          description: Days since the partner's Bolt Claw account was made.
        templated:
          type:
            - integer
            - 'null'
          description: '"Looks templated": near-identical applications to other programs this week, or null.'
        burst:
          type:
            - integer
            - 'null'
          description: Accounts that applied from one network and device in 24 hours, or null when it was not a burst.
    Integration:
      type: object
      required:
        - id
        - provider
        - webhook_url
        - has_secret
        - customer_metadata_key
        - status
        - last_event_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        provider:
          $ref: '#/components/schemas/BillingProvider'
        webhook_url:
          type: string
        has_secret:
          type: boolean
        customer_metadata_key:
          type: string
        status:
          type: string
          enum:
            - active
            - disabled
            - disconnected
          description: '`disconnected`: a Stripe App connection that was uninstalled or disconnected.'
        last_event_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        codes_and_credits:
          type: object
          description: |
            Whether a restricted Stripe key is set, which lets Bolt Claw make codes and give credit
            and free months in your Stripe. The key itself is never returned, not even in part.
          required:
            - connected
            - mode
            - set_at
          properties:
            connected:
              type: boolean
            mode:
              type:
                - string
                - 'null'
              enum:
                - live
                - test
                - null
            set_at:
              type:
                - string
                - 'null'
              format: date-time
        payload_format:
          type:
            - string
            - 'null'
          enum:
            - snapshot
            - thin
            - null
          description: The shape of the last verified Stripe event. `thin` events carry no data; choose Snapshot in Stripe.
        auth_mode:
          type: string
          enum:
            - signing_secret
            - basic
            - header_token
            - oauth
            - api_key
            - shopify_app
          description: |
            How a delivery proves itself: a signing secret, Basic credentials Bolt Claw generated
            (Chargebee), a header token Bolt Claw generated (RevenueCat), the Stripe App (`oauth`),
            an unsigned ping confirmed through the provider's API with your token (Gumroad), or Bolt
            Claw's Shopify app (`shopify_app`), linked from the shop's own Shopify admin.
        test_mode:
          type: boolean
          description: The Sandbox switch. Every event of this integration is recorded as test data.
        credential:
          type: object
          description: Whether the credential is saved (or generated), and since when. Never the value.
          required:
            - saved
            - set_at
            - username
          properties:
            saved:
              type: boolean
            set_at:
              type:
                - string
                - 'null'
              format: date-time
            username:
              type:
                - string
                - 'null'
              description: Chargebee's Basic username. Not secret by itself.
        read_key:
          type: object
          description: Recurly's read-only key or Gumroad's token. Whether one is saved, never the value.
          required:
            - saved
            - set_at
          properties:
            saved:
              type: boolean
            set_at:
              type:
                - string
                - 'null'
              format: date-time
        provider_account:
          type:
            - string
            - 'null'
          description: Recurly's region (`us` or `eu`), or the Shopify shop that sent the first delivery.
        shopify:
          type:
            - object
            - 'null'
          description: |
            Bolt Claw's Shopify app, linked to this program from the shop's own Shopify admin. Null
            for every other kind of integration. `connected` is false once it was unlinked or the
            shop uninstalled the app.
          required:
            - shop
            - connected
            - connected_at
            - disconnected_at
          properties:
            shop:
              type:
                - string
                - 'null'
              description: The shop's myshopify.com name.
            connected:
              type: boolean
            connected_at:
              type:
                - string
                - 'null'
              format: date-time
            disconnected_at:
              type:
                - string
                - 'null'
              format: date-time
        app:
          type:
            - object
            - 'null'
          description: The Stripe App connection. Null for every other kind of integration.
          required:
            - connected
            - account_id
            - livemode
            - connected_at
            - disconnected_at
          properties:
            connected:
              type: boolean
            account_id:
              type:
                - string
                - 'null'
              description: Your own Stripe account id.
            livemode:
              type:
                - boolean
                - 'null'
            connected_at:
              type:
                - string
                - 'null'
              format: date-time
            disconnected_at:
              type:
                - string
                - 'null'
              format: date-time
        health:
          type: object
          description: What the setup status reads. Fixed words and counts; nothing from a delivery.
          required:
            - failures_since_event
            - last_failure_at
            - last_failure_reason
            - api_version
            - event_types_seen
            - last_livemode
          properties:
            failures_since_event:
              type: integer
              description: Deliveries refused since the last verified one.
            last_failure_at:
              type:
                - string
                - 'null'
              format: date-time
            last_failure_reason:
              type:
                - string
                - 'null'
              enum:
                - not_configured
                - missing_signature
                - bad_signature
                - stale
                - bad_credentials
                - bad_body
                - null
            api_version:
              type:
                - string
                - 'null'
              description: 'Stripe: the API version its events are rendered with.'
            event_types_seen:
              type: array
              items:
                type: string
              description: Up to 64 event types seen.
            last_livemode:
              type:
                - boolean
                - 'null'
    BillingProvider:
      type: string
      description: A billing system Bolt Claw connects to directly.
      enum:
        - stripe
        - paddle
        - polar
        - whop
        - revenuecat
        - woocommerce
        - fastspring
        - lemonsqueezy
        - shopify
        - chargebee
        - recurly
        - gumroad
    Program:
      type: object
      required:
        - id
        - name
        - product_url
        - currency
        - cookie_days
        - attribution_model
        - hold_days
        - min_payout_minor
        - payout_schedule
        - join_mode
        - auto_approve
        - signup_url
        - terms
        - terms_version
        - preview_title
        - preview_description
        - preview_image_url
        - customer_discount
        - discount_code_prefix
        - status
        - created_at
        - updated_at
        - pending_partners
        - integrations
        - kind
        - partner_monthly_cap_minor
        - embed
        - welcome
        - late_code_days
        - lead_grant_daily_cap
        - invite_after_purchase
        - ask_snooze_days
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        product_url:
          type: string
        currency:
          type: string
        cookie_days:
          type: integer
        attribution_model:
          type: string
          enum:
            - last_click
            - first_click
        hold_days:
          type: integer
        min_payout_minor:
          type: integer
        payout_schedule:
          type: string
          enum:
            - manual
            - monthly
        join_mode:
          type: string
          enum:
            - apply
            - invite
        auto_approve:
          type: boolean
        signup_url:
          type: string
          description: The program's public join page.
        terms:
          type:
            - string
            - 'null'
        terms_version:
          type: integer
        preview_title:
          type:
            - string
            - 'null'
        preview_description:
          type:
            - string
            - 'null'
        preview_image_url:
          type:
            - string
            - 'null'
        customer_discount:
          type:
            - string
            - 'null'
        discount_code_prefix:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - active
            - paused
            - archived
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        pending_partners:
          type: integer
        integrations:
          type: array
          items:
            $ref: '#/components/schemas/Integration'
        kind:
          $ref: '#/components/schemas/ProgramKind'
        partner_monthly_cap_minor:
          type:
            - integer
            - 'null'
          description: The most one partner earns from this program in a calendar month (UTC), bonuses included. Null for no ceiling.
        embed:
          $ref: '#/components/schemas/EmbedSettings'
        welcome:
          $ref: '#/components/schemas/WelcomeSettings'
        late_code_days:
          type: integer
          description: How many days after sign-up a referral code may still be entered; 0 for never after.
        lead_grant_daily_cap:
          type: integer
          description: The most rewards a day (UTC) that signups alone may earn, across the program.
        invite_after_purchase:
          type: boolean
          description: Whether a friend's first payment gives them a referral link of their own.
        ask_snooze_days:
          type: integer
          description: How long "Not right now" in the panel lasts before a person is asked again.
        application_questions:
          type: array
          maxItems: 3
          items:
            type: string
          description: Up to three questions a partner answers when they apply, shown on the program's join page.
        risk_alert_email:
          type: boolean
          description: Whether the program's managers are emailed when a promoter needs a look. Set with `PATCH /referrals/programs/{id}/risk-policy`.
        disclosure_text:
          type: string
          description: |
            The sentence partners add when they share a link or a code ("I earn a commission if you
            buy through my link."). Never empty. Partners are reminded of it when they copy a link,
            when they put one in a post, a bio page or an automation, after every library snippet,
            and in the in-app panel.
        leaderboard:
          description: The leaderboard promoters see; null when they see none (you always see yours).
          oneOf:
            - $ref: '#/components/schemas/LeaderboardSettings'
            - type: 'null'
        waitlist:
          description: A waitlist program's settings and its hosted page's address; null for any other program.
          oneOf:
            - $ref: '#/components/schemas/WaitlistSettings'
            - type: 'null'
        second_tier:
          description: The second tier; null while it is off. Set with `PATCH /referrals/programs/{id}/second-tier`.
          oneOf:
            - $ref: '#/components/schemas/SecondTier'
            - type: 'null'
        terms_required_version:
          type:
            - integer
            - 'null'
          description: The terms version every approved partner must accept before their next payout (a change to the second tier asks it).
        setup:
          type: object
          description: The answers to "How do you take payments?" and "How do people sign up?", which choose the setup steps.
          required:
            - payments
            - signups
            - audience_eu_uk
          properties:
            payments:
              $ref: '#/components/schemas/SetupPayments'
            signups:
              $ref: '#/components/schemas/SetupSignups'
            audience_eu_uk:
              type: boolean
              description: The program's audience includes the EU or the UK, so the setup status asks for the tag's consent mode.
    SetupPayments:
      type:
        - string
        - 'null'
      enum:
        - stripe
        - paddle
        - polar
        - whop
        - revenuecat
        - woocommerce
        - fastspring
        - lemonsqueezy
        - shopify
        - chargebee
        - recurly
        - gumroad
        - own_system
        - not_sure
        - null
    SetupSignups:
      type:
        - string
        - 'null'
      enum:
        - own_backend
        - supabase
        - firebase
        - clerk
        - authjs
        - nocode
        - checkout_only
        - null
    WelcomeSettings:
      type: object
      description: The friend's welcome, as you set it.
      required:
        - config
        - show_name
        - page
        - allowed_hosts
      properties:
        config:
          $ref: '#/components/schemas/WelcomeConfig'
        show_name:
          type: boolean
          description: Whether a referrer's name (or an affiliate's, when they agreed) is shown to the friend.
        page:
          type: boolean
          description: Whether a person opening the link first sees a one-screen welcome before your product.
        allowed_hosts:
          type: array
          items:
            type: string
          description: Hosts your website tag may read the offer from, beside the product URL's own.
    WelcomeConfig:
      type: object
      additionalProperties: false
      description: |
        The welcome's look and words. Text is shown as plain text, never markup: angle brackets,
        controls and invisible marks are refused. The call to action needs both its label and its
        address.
      properties:
        style:
          type: string
          enum:
            - bar
            - corner
            - sheet
        position:
          type: string
          enum:
            - top
            - bottom
        accent:
          type: string
          pattern: ^#[0-9a-fA-F]{6}$
          description: A colour like `#1a2b3c`.
        radius:
          type: integer
          minimum: 0
          maximum: 24
        theme:
          type: string
          enum:
            - light
            - dark
            - auto
        headline:
          type: string
          maxLength: 120
          description: Your headline. `{name}` is the inviter (or "A friend"), `{offer}` the gift.
        conditions:
          type: string
          maxLength: 160
          description: The one line of conditions.
        cta_label:
          type: string
          maxLength: 40
        cta_url:
          type: string
          maxLength: 2048
          description: An https address.
    ProgramKind:
      type: string
      description: |
        The template a program was made from. It sets the starting values and the words the
        screens use for promoters; it never switches a feature off. A `waitlist` program's
        promoters are the people on its waitlist.
      enum:
        - affiliate
        - refer_a_friend
        - creator
        - ambassador
        - reseller
        - shop
        - waitlist
    ProgramList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Program'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ProgramFields:
      type: object
      properties:
        name:
          type: string
          maxLength: 200
        product_url:
          type: string
          maxLength: 4096
          description: An https URL.
        currency:
          type: string
          maxLength: 8
        cookie_days:
          type: integer
          description: 1 to 365.
        attribution_model:
          type: string
          maxLength: 20
          description: '`last_click` or `first_click`.'
        hold_days:
          type: integer
        min_payout_minor:
          type: integer
        payout_schedule:
          type: string
          maxLength: 20
          description: '`manual` or `monthly`.'
        join_mode:
          type: string
          maxLength: 20
          description: '`apply` or `invite`.'
        auto_approve:
          type: boolean
        terms:
          type:
            - string
            - 'null'
          maxLength: 20000
        preview_title:
          type:
            - string
            - 'null'
          maxLength: 400
        preview_description:
          type:
            - string
            - 'null'
          maxLength: 1000
        customer_discount:
          type:
            - string
            - 'null'
          maxLength: 1000
        discount_code_prefix:
          type:
            - string
            - 'null'
          maxLength: 40
        partner_monthly_cap_minor:
          type:
            - integer
            - 'null'
          description: The most one partner earns in a calendar month (UTC), in minor units; null for no ceiling.
        embed_enabled:
          type: boolean
          description: Turns the in-app referral panel on or off. Off, no session can be minted and the panel does not load.
        embed_origins:
          type: array
          maxItems: 10
          items:
            type: string
            maxLength: 255
          description: |
            The sites allowed to frame the panel, like `https://app.example.com`. Replaces the list. https only
            (`http://localhost:3000` for development), no wildcard, no path, at most 10.
        embed_config:
          description: The panel look, copy and sharing, replacing the old settings whole. Unknown keys are `400`.
          allOf:
            - $ref: '#/components/schemas/EmbedConfig'
        approve_referrers:
          type: boolean
          description: 'True (default): a new referrer is approved at once. False: they are `pending` until you approve them.'
        welcome_config:
          description: The welcome's look and words, replacing the old settings whole; null for the defaults. Unknown keys are `400`.
          oneOf:
            - $ref: '#/components/schemas/WelcomeConfig'
            - type: 'null'
        welcome_show_name:
          type: boolean
          description: Show a referrer's name to the friend (default true). An affiliate's is shown only when they agreed.
        welcome_page:
          type: boolean
          description: A one-screen welcome at the link before your product (default false).
        allowed_hosts:
          type: array
          maxItems: 10
          items:
            type: string
            maxLength: 253
          description: |
            Extra hosts your website tag may read the offer from, like `www.example.com`, beside the
            product URL's own and the panel's framing origins. Replaces the list. Host names only,
            at most 10. An AI assistant's key cannot change it.
        late_code_days:
          type: integer
          minimum: 0
          maximum: 365
          description: How many days after sign-up a referral code may still be entered (default 14; 0 for never after).
        lead_grant_daily_cap:
          type: integer
          minimum: 0
          maximum: 100000
          description: The most rewards a day (UTC) signups alone may earn across the program (default 100).
        invite_after_purchase:
          type: boolean
          description: Give a friend who makes their first payment a referral link of their own (default false).
        ask_snooze_days:
          type: integer
          minimum: 1
          maximum: 365
          description: How long "Not right now" in the panel lasts (default 30).
        setup_payments:
          $ref: '#/components/schemas/SetupPayments'
        setup_signups:
          $ref: '#/components/schemas/SetupSignups'
        audience_eu_uk:
          type: boolean
          description: Your audience includes the EU or the UK (default false). The setup status then asks for the tag's consent mode.
        application_questions:
          type: array
          maxItems: 3
          items:
            type: string
            maxLength: 200
          description: |
            Up to three questions a partner answers when they apply (each 1 to 200 characters, no
            hidden characters). Answers are shown on their application; an empty list asks nothing.
        disclosure_text:
          type: string
          minLength: 10
          maxLength: 280
          description: |
            The sentence partners add when they share (plain text, 10 to 280 characters). It may be
            edited, never emptied: partners must tell people they earn from the link (FTC, ASA).
            A new program starts with one for its currency's region.
        leaderboard:
          $ref: '#/components/schemas/LeaderboardFields'
        waitlist:
          $ref: '#/components/schemas/WaitlistFields'
    ProgramCreate:
      allOf:
        - $ref: '#/components/schemas/ProgramFields'
        - type: object
          required:
            - name
            - product_url
          properties:
            kind:
              type: string
              maxLength: 40
              description: |
                Start from a template: `affiliate`, `refer_a_friend`, `creator`, `ambassador`,
                `reseller`, `shop` or `waitlist`. The program gets that kind's settings and
                reward rules (and levels, for `reseller`; the waitlist itself, for `waitlist`),
                with anything you send yourself taking precedence, in one step. Left out, the
                program starts with no rules, as before.
      unevaluatedProperties: false
    ProgramUpdate:
      allOf:
        - $ref: '#/components/schemas/ProgramFields'
      unevaluatedProperties: false
      description: Every field optional; a field left out keeps its value.
    PartnerStatus:
      type: string
      enum:
        - pending
        - approved
        - rejected
        - banned
    PartnerStats:
      type: object
      required:
        - clicks
        - leads
        - sales
        - revenue_minor
        - commission_minor
        - conversion_rate
        - epc_minor
      properties:
        clicks:
          type: integer
        leads:
          type: integer
        sales:
          type: integer
        revenue_minor:
          type: integer
        commission_minor:
          type: integer
        conversion_rate:
          type:
            - number
            - 'null'
          description: Sales per click, 0 to 1. Null with no clicks.
        epc_minor:
          type:
            - number
            - 'null'
          description: Earnings per click, in minor units. Null with no clicks.
    Partner:
      type: object
      required:
        - id
        - display_name
        - status
        - country
        - payout_method
        - share_email
        - email
        - terms_version_accepted
        - applied_at
        - approved_at
        - stats
        - group
        - group_pinned
        - application
      properties:
        id:
          type: string
          format: uuid
          description: The partner's membership in this program.
        display_name:
          type: string
        status:
          $ref: '#/components/schemas/PartnerStatus'
        country:
          type:
            - string
            - 'null'
        payout_method:
          type:
            - string
            - 'null'
          enum:
            - paypal
            - wise
            - bank
            - other
            - null
          description: The payout method type. Never the details.
        share_email:
          type: boolean
        email:
          type:
            - string
            - 'null'
          description: Only when the partner chose to share it.
        terms_version_accepted:
          type:
            - integer
            - 'null'
        applied_at:
          type: string
          format: date-time
        approved_at:
          type:
            - string
            - 'null'
          format: date-time
        stats:
          $ref: '#/components/schemas/PartnerStats'
        group:
          type:
            - object
            - 'null'
          description: The group or level the partner is in, or null.
          required:
            - id
            - name
            - level
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            level:
              type: boolean
              description: True for a level (a group with a rank).
        group_pinned:
          type: boolean
          description: True when the daily level job leaves this partner where you put them.
        application:
          $ref: '#/components/schemas/PartnerApplication'
        disclosure_acknowledged_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When they last acknowledged your disclosure reminder (shown when they copy a link, then monthly).
        muted_broadcasts:
          type: boolean
          description: Whether they muted your broadcast emails (one click in any of them). They still see your messages on their program page.
    PartnerList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Partner'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    PartnerRef:
      type:
        - object
        - 'null'
      required:
        - id
        - display_name
      properties:
        id:
          type: string
          format: uuid
        display_name:
          type: string
    Commission:
      type: object
      required:
        - id
        - status
        - amount_minor
        - gross_minor
        - reversed_minor
        - currency
        - hold_until
        - approved_at
        - reversed_at
        - created_at
        - reason
        - adjustment
        - under_review
        - partner
        - conversion
        - basis_text
        - capped_minor
        - bonus
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/CommissionStatus'
        amount_minor:
          type: integer
          description: Net of reversals. Negative for a downward adjustment.
        gross_minor:
          type: integer
        reversed_minor:
          type: integer
        currency:
          type: string
        hold_until:
          type:
            - string
            - 'null'
          format: date-time
        approved_at:
          type:
            - string
            - 'null'
          format: date-time
        reversed_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        reason:
          type:
            - string
            - 'null'
        adjustment:
          type: boolean
        under_review:
          type: boolean
        risk:
          type:
            - object
            - 'null'
          description: |
            Why the fraud engine held this commission longer (`hold`) or sent it to a person
            (`review`): the score and every reason as a sentence, from counts and times. Null when
            it held nothing. See [Fraud and risk](/docs/referral-fraud).
          required:
            - score
            - action
            - reasons
          properties:
            score:
              type: integer
              minimum: 0
              maximum: 100
            action:
              type: string
              enum:
                - hold
                - review
            reasons:
              type: array
              items:
                type: object
                required:
                  - code
                  - sentence
                properties:
                  code:
                    type: string
                  sentence:
                    type: string
        partner:
          $ref: '#/components/schemas/PartnerRef'
        conversion:
          type:
            - object
            - 'null'
          required:
            - id
            - event
            - amount_minor
            - currency
            - occurred_at
            - source
            - customer_external_id
          properties:
            id:
              type: string
              format: uuid
            event:
              $ref: '#/components/schemas/ConversionEvent'
            amount_minor:
              type: integer
            currency:
              type:
                - string
                - 'null'
            occurred_at:
              type: string
              format: date-time
            source:
              type: string
              enum:
                - stripe
                - api
                - manual
                - paddle
                - polar
                - whop
                - revenuecat
                - woocommerce
                - fastspring
                - lemonsqueezy
                - shopify
                - chargebee
                - recurly
                - gumroad
              description: Where the conversion came from. Each billing system Bolt Claw connects to is its own source.
            customer_external_id:
              type:
                - string
                - 'null'
              description: Your own id for the customer.
        basis_text:
          type: string
          description: |
            Why it is this amount: "30% of 49.00 on Yearly plans (Gold level), capped at 120.00 per
            customer". `Program rate` for a commission recorded before reward rules had reasons.
        capped_minor:
          type: integer
          description: What the program's monthly ceiling per partner held back from this commission.
        bonus:
          type:
            - string
            - 'null'
          enum:
            - milestone
            - manual
            - null
          description: |
            `milestone` for a bonus a partner earned by crossing a line, `manual` for one you paid
            by hand (which has no `conversion`), null for anything else.
        test:
          type: boolean
          description: From TEST data, earned by the program's test partner. Never payable.
        tier:
          type: integer
          enum:
            - 1
            - 2
          description: '`2` for a recruiter''s share of what a partner they recruited earned (the second tier); `1` for everything else.'
    CommissionList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Commission'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    CommissionAdjust:
      type: object
      required:
        - amount_minor
        - reason
      additionalProperties: false
      properties:
        amount_minor:
          type: integer
          description: Signed minor units; not 0.
        reason:
          type: string
          minLength: 1
          maxLength: 500
          description: Trimmed; recorded in the activity log.
    RewardConditions:
      type: object
      additionalProperties: false
      description: |
        When a rule applies. Every key you send must match; a rule with none is its audience's
        default for its event. A sale without `items` matches no rule that names `products`,
        `prices`, `plans` or `intervals`. A signup takes none; a bonus takes exactly `milestone`.
      properties:
        products:
          type: array
          maxItems: 100
          items:
            type: string
            maxLength: 200
        prices:
          type: array
          maxItems: 100
          items:
            type: string
            maxLength: 200
        plans:
          type: array
          maxItems: 100
          items:
            type: string
            maxLength: 200
          description: Matched without regard to case.
        intervals:
          type: array
          maxItems: 3
          items:
            type: string
            enum:
              - month
              - year
              - one_time
        purchase:
          type: string
          enum:
            - first
            - renewal
            - any
          description: The customer's first payment or a renewal. `any` is the same as leaving it out.
        currencies:
          type: array
          maxItems: 100
          items:
            type: string
            maxLength: 8
        min_amount_minor:
          type: integer
          minimum: 0
          description: Only sales of at least this.
        milestone:
          $ref: '#/components/schemas/Milestone'
    Milestone:
      type: object
      additionalProperties: false
      required:
        - metric
        - window
      description: |
        A bonus's line: `revenue` in `amount_minor` (the program's currency), or `sales` or
        `customers` in `count`, over the partner's `lifetime` or each `calendar_month` (UTC). Paid
        once per window, at the sale that crosses it. While it is on hold, a refund or dispute
        that takes the partner back under the line takes it back, and crossing again pays it
        again; once approved, it stays.
      properties:
        metric:
          type: string
          enum:
            - revenue
            - sales
            - customers
        amount_minor:
          type: integer
          minimum: 1
        count:
          type: integer
          minimum: 1
        window:
          type: string
          enum:
            - lifetime
            - calendar_month
    Reward:
      type: object
      required:
        - id
        - event
        - kind
        - percent_bps
        - amount_minor
        - duration
        - months
        - updated_at
        - scope
        - group_id
        - partner_id
        - label
        - conditions
        - cap_minor
        - priority
        - created_at
        - summary
      properties:
        id:
          type: string
          format: uuid
        event:
          type: string
          enum:
            - lead
            - sale
            - bonus
        kind:
          type: string
          enum:
            - percent
            - flat
        percent_bps:
          type:
            - integer
            - 'null'
          description: 'Basis points: 2000 is 20%.'
        amount_minor:
          type:
            - integer
            - 'null'
          description: Minor units of the program's currency.
        duration:
          type: string
          enum:
            - once
            - months
            - lifetime
        months:
          type:
            - integer
            - 'null'
        updated_at:
          type: string
          format: date-time
        scope:
          type: string
          enum:
            - program
            - group
            - partner
          description: Who it pays. The most specific scope with a matching rule wins.
        group_id:
          type:
            - string
            - 'null'
          format: uuid
        partner_id:
          type:
            - string
            - 'null'
          format: uuid
        label:
          type:
            - string
            - 'null'
          description: Your own words for when it applies.
        conditions:
          $ref: '#/components/schemas/RewardConditions'
        cap_minor:
          type:
            - integer
            - 'null'
          description: The most it pays for one customer
          in total.: null
        priority:
          type: integer
          description: Breaks a tie between two equally specific rules; higher wins.
        created_at:
          type: string
          format: date-time
        summary:
          type: string
          description: The rule in one sentence.
        reward_type:
          $ref: '#/components/schemas/RewardType'
        beneficiary:
          type: string
          enum:
            - promoter
            - referee
          description: Who it rewards. `referee` is the friend who arrived through the link.
        period_count:
          type:
            - integer
            - 'null'
          description: A free period's length.
        period_unit:
          type:
            - string
            - 'null'
          enum:
            - day
            - month
            - null
        perk_text:
          type:
            - string
            - 'null'
          description: A perk
          in your own words.: null
        discount_duration:
          type:
            - string
            - 'null'
          enum:
            - once
            - months
            - forever
            - null
          description: How long a friend's discount lasts.
        discount_months:
          type:
            - integer
            - 'null'
        stripe_coupon_id:
          type:
            - string
            - 'null'
          description: The coupon Bolt Claw made for this rule in your own Stripe, when it made one.
    RewardType:
      type: string
      description: |
        What a rule gives. `cash` is a commission, paid out; the rest are grants, given in your own
        system or through your Stripe. A friend (`referee`) gets a `discount`, credit, a free period,
        a perk or a gift card, never cash; a `discount` is only ever the friend's, at a sale, once
        per customer. A bonus is cash.
      enum:
        - cash
        - credit
        - free_period
        - discount
        - perk
        - gift_card
    RewardList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          maxItems: 201
          description: Every rule of the program, at most 200.
          items:
            $ref: '#/components/schemas/Reward'
    RewardCreate:
      type: object
      required:
        - event
      description: |
        `kind` (`percent` or `flat`) is required for cash, credit, a gift card and a discount; a
        free period and a perk have no amount and take none.
      additionalProperties: false
      properties:
        event:
          type: string
          enum:
            - lead
            - sale
            - bonus
        scope:
          type: string
          enum:
            - program
            - group
            - partner
          description: '`program` (everyone) unless you say otherwise.'
        group_id:
          type: string
          format: uuid
          description: With `scope` `group`.
        partner_id:
          type: string
          format: uuid
          description: 'With `scope` `partner`: one partner''s own rate.'
        kind:
          type: string
          enum:
            - percent
            - flat
        percent_bps:
          type:
            - integer
            - 'null'
          description: 1 to 10000, for `percent`.
        amount_minor:
          type:
            - integer
            - 'null'
          description: For `flat`; a bonus's must be above 0.
        duration:
          type: string
          enum:
            - once
            - months
            - lifetime
          description: Ignored for a bonus
          which is paid once per window.: null
        months:
          type:
            - integer
            - 'null'
          description: 1 to 120, for `months`.
        cap_minor:
          type:
            - integer
            - 'null'
          description: The most it pays for one customer; null for no ceiling.
        conditions:
          $ref: '#/components/schemas/RewardConditions'
        priority:
          type: integer
          description: '-1000 to 1000; 0 by default.'
        label:
          type:
            - string
            - 'null'
          maxLength: 200
          description: At most 60 characters after trimming.
        reward_type:
          allOf:
            - $ref: '#/components/schemas/RewardType'
          description: '`cash` when left out. Fixed once the rule exists.'
        beneficiary:
          type: string
          enum:
            - promoter
            - referee
          description: '`promoter` when left out. Fixed once the rule exists.'
        period_count:
          type:
            - integer
            - 'null'
          description: 'For `free_period`: 1 to 365 days, or 1 to 36 months.'
        period_unit:
          type:
            - string
            - 'null'
          enum:
            - day
            - month
            - null
          description: For `free_period`.
        perk_text:
          type:
            - string
            - 'null'
          maxLength: 400
          description: 'For `perk`: at most 120 characters after trimming.'
        discount_duration:
          type:
            - string
            - 'null'
          enum:
            - once
            - months
            - forever
            - null
          description: 'For `discount`: how long it lasts.'
        discount_months:
          type:
            - integer
            - 'null'
          description: 'For a `discount` of `months`: 1 to 36.'
    RewardUpdate:
      type: object
      additionalProperties: false
      description: |
        Every field optional. A rule's event and audience are fixed once it exists, and so are what
        it gives and to whom (`reward_type`, `beneficiary`): sending either is `400`.
      properties:
        kind:
          type: string
          enum:
            - percent
            - flat
        percent_bps:
          type:
            - integer
            - 'null'
        amount_minor:
          type:
            - integer
            - 'null'
        duration:
          type: string
          enum:
            - once
            - months
            - lifetime
        months:
          type:
            - integer
            - 'null'
        cap_minor:
          type:
            - integer
            - 'null'
        conditions:
          $ref: '#/components/schemas/RewardConditions'
        priority:
          type: integer
        label:
          type:
            - string
            - 'null'
          maxLength: 200
        period_count:
          type:
            - integer
            - 'null'
        period_unit:
          type:
            - string
            - 'null'
          enum:
            - day
            - month
            - null
        perk_text:
          type:
            - string
            - 'null'
          maxLength: 400
        discount_duration:
          type:
            - string
            - 'null'
          enum:
            - once
            - months
            - forever
            - null
        discount_months:
          type:
            - integer
            - 'null'
    LevelMinimum:
      type:
        - object
        - 'null'
      additionalProperties: false
      required:
        - metric
      description: |
        What a partner needs to reach a level: `revenue` in `amount_minor`, or `sales` or
        `customers` in `count`, over the last `window_days` (30 to 365), or all time when null.
        Counted net of refunds. Null: everyone qualifies.
      properties:
        metric:
          type: string
          enum:
            - revenue
            - sales
            - customers
        amount_minor:
          type: integer
          minimum: 0
        count:
          type: integer
          minimum: 0
        window_days:
          type:
            - integer
            - 'null'
    Group:
      type: object
      required:
        - id
        - name
        - description
        - is_default
        - rank
        - level
        - minimum
        - keep_earned
        - partners
        - summary
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        is_default:
          type: boolean
          description: A newly approved partner starts here.
        rank:
          type:
            - integer
            - 'null'
          description: A level's place on the ladder (1 is the lowest); null for a plain group.
        level:
          type: boolean
        minimum:
          $ref: '#/components/schemas/LevelMinimum'
        keep_earned:
          type: boolean
          description: A partner who reached this level is never moved down by the daily job.
        partners:
          type: integer
          description: Approved and pending partners in it now.
        summary:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    GroupList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          maxItems: 51
          description: Every group and level of the program, at most 50, levels first by rank.
          items:
            $ref: '#/components/schemas/Group'
    GroupCreate:
      type: object
      required:
        - name
      additionalProperties: false
      properties:
        name:
          type: string
          maxLength: 200
          description: 1 to 60 characters after trimming; unique in the program.
        description:
          type:
            - string
            - 'null'
          maxLength: 1000
        is_default:
          type: boolean
        rank:
          type:
            - integer
            - 'null'
          description: 1 to 100 makes it a level; null for a plain group.
        minimum:
          $ref: '#/components/schemas/LevelMinimum'
        keep_earned:
          type: boolean
    GroupUpdate:
      type: object
      additionalProperties: false
      description: Every field optional; a field left out keeps its value.
      properties:
        name:
          type: string
          maxLength: 200
        description:
          type:
            - string
            - 'null'
          maxLength: 1000
        is_default:
          type: boolean
        rank:
          type:
            - integer
            - 'null'
        minimum:
          $ref: '#/components/schemas/LevelMinimum'
        keep_earned:
          type: boolean
    PartnerUpdate:
      type: object
      additionalProperties: false
      properties:
        group_id:
          type:
            - string
            - 'null'
          format: uuid
          description: A group or level of the partner's program; null for none.
        pinned:
          type: boolean
          description: True keeps the daily level job from moving the partner.
    BonusCreate:
      type: object
      required:
        - amount_minor
        - reason
      additionalProperties: false
      properties:
        amount_minor:
          type: integer
          minimum: 1
          description: Minor units of the program's currency.
        reason:
          type: string
          minLength: 1
          maxLength: 280
          description: The partner sees it as the line's description. Recorded in the activity log.
        hold_days:
          type: integer
          minimum: 0
          maximum: 365
          description: Days before it may be paid. 0 (the default) makes it payable at once.
    ReferralDeleted:
      type: object
      required:
        - deleted
      properties:
        deleted:
          type: boolean
          const: true
    ReferrerStatus:
      type: string
      enum:
        - approved
        - pending
        - banned
      description: |
        `approved` earns rewards. `pending` is waiting for you (when the program does not approve
        new referrers at once, `embed.approve_referrers`); it can share its link and earns nothing
        yet. `banned` earns nothing; an erased referrer is also `banned`.
    ReferrerView:
      type: object
      description: |
        One of your own users who holds a referral link, as you see them. Never an email, a
        fingerprint or a Bolt Claw account: a referrer has none.
      required:
        - id
        - program_id
        - external_id
        - name
        - code
        - link_url
        - status
        - erased
        - created_at
        - clicks
        - friends
        - friends_paid
        - earned_minor
        - currency
      properties:
        id:
          type: string
          format: uuid
          description: The referrer's membership in this program. Use it with `PATCH` and `DELETE /referrals/referrers/{id}`.
        program_id:
          type: string
          format: uuid
        external_id:
          type: string
          minLength: 1
          maxLength: 200
          description: Your own id for the user.
        name:
          type:
            - string
            - 'null'
          maxLength: 60
          description: The first name or display name you sent. Null once erased.
        code:
          type:
            - string
            - 'null'
          description: The code in the referrer's link.
        link_url:
          type:
            - string
            - 'null'
          description: The link the referrer shares.
        status:
          $ref: '#/components/schemas/ReferrerStatus'
        erased:
          type: boolean
          description: 'True for an erased referrer: a row that keeps what it earned, with no name.'
        created_at:
          type: string
          format: date-time
        clicks:
          type: integer
          description: Clicks on the link
          all time.: null
        friends:
          type: integer
          description: Friends who signed up through the link.
        friends_paid:
          type: integer
          description: Friends who have paid.
        earned_minor:
          type: integer
          description: Rewards earned
          all time: null
          net of refunds: null
          in minor units of `currency`.: null
        currency:
          type: string
          description: The program's currency.
    ReferrerList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ReferrerView'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    ReferrerUpsert:
      type: object
      required:
        - external_id
      additionalProperties: false
      properties:
        external_id:
          type: string
          minLength: 1
          maxLength: 200
          description: Your own id for the user. Printable characters only, no space at either end.
        name:
          type:
            - string
            - 'null'
          maxLength: 60
          description: A first name or display name for the panel's greeting, plain text. `null` clears it.
        email:
          type:
            - string
            - 'null'
          maxLength: 400
          description: Only used to recognise the same person again (a fingerprint is kept, never the address). Never returned.
        code:
          type:
            - string
            - 'null'
          maxLength: 40
          description: 'The vanity code for their link: 3 to 40 lowercase letters and digits, single hyphens between words. Only when the referrer is created.'
    EmbedTokenCreate:
      type: object
      additionalProperties: false
      properties:
        referrer:
          type: object
          required:
            - external_id
          additionalProperties: false
          description: The user the session is for; they are made a referrer if they are not one. Leave it out only for `mode` `preview`.
          properties:
            external_id:
              type: string
              minLength: 1
              maxLength: 200
              description: Your own id for the user.
            name:
              type:
                - string
                - 'null'
              maxLength: 60
            email:
              type:
                - string
                - 'null'
              maxLength: 400
              description: Only used to recognise the same person again; never stored as an address.
            code:
              type:
                - string
                - 'null'
              maxLength: 40
              description: A vanity code
              only when the referrer is created.: null
        ttl_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          default: 3600
          description: How long the token works, in seconds.
        mode:
          type: string
          enum:
            - panel
            - preview
          default: panel
          description: '`panel` for a real referrer; `preview` shows sample data and needs no referrer.'
    EmbedTokenCreated:
      type: object
      required:
        - token
        - expires_at
        - referrer
      properties:
        token:
          type:
            - string
            - 'null'
          description: |
            `bcet_` and 43 characters. Shown once: hand it to the panel and do not store or log it.
            Null in a replay of the same `Idempotency-Key`: mint a new one.
        expires_at:
          type: string
          format: date-time
        referrer:
          oneOf:
            - $ref: '#/components/schemas/ReferrerView'
            - type: 'null'
          description: The referrer the session is for; null for a `preview` session.
    ReferrerStatusUpdate:
      type: object
      required:
        - status
      additionalProperties: false
      properties:
        status:
          type: string
          enum:
            - approved
            - banned
    ShareChannel:
      type: string
      enum:
        - native
        - whatsapp
        - x
        - linkedin
        - facebook
        - messenger
        - telegram
        - sms
        - email
      description: A share button. `native` is the device's own share sheet.
    EmbedCopy:
      type: object
      additionalProperties: false
      description: |
        Words you replace in the panel. Plain text only, never HTML. Any key left out keeps the
        panel's own English. Each value is 1 to the number of characters shown beside it.
      properties:
        headline:
          type: string
          maxLength: 120
        subline:
          type: string
          maxLength: 240
        step_1:
          type: string
          maxLength: 120
        step_2:
          type: string
          maxLength: 120
        step_3:
          type: string
          maxLength: 120
        copy_button:
          type: string
          maxLength: 30
        copied_label:
          type: string
          maxLength: 30
        share_button:
          type: string
          maxLength: 30
        email_button:
          type: string
          maxLength: 30
        email_title:
          type: string
          maxLength: 60
        terms_label:
          type: string
          maxLength: 40
        empty_title:
          type: string
          maxLength: 80
        empty_body:
          type: string
          maxLength: 240
        payout_title:
          type: string
          maxLength: 80
        payout_body:
          type: string
          maxLength: 240
        rules_title:
          type: string
          maxLength: 60
        history_title:
          type: string
          maxLength: 60
        expired_title:
          type: string
          maxLength: 80
        expired_body:
          type: string
          maxLength: 240
    EmbedMessages:
      type: object
      additionalProperties: false
      description: |
        The text each share button pre-fills, at most 280 characters, plain text. `{link}` is
        replaced by the referrer's link (a message without it gets the link on a new line) and
        `{program}` by the program's name.
      properties:
        native:
          type: string
          maxLength: 280
        whatsapp:
          type: string
          maxLength: 280
        x:
          type: string
          maxLength: 280
        linkedin:
          type: string
          maxLength: 280
        facebook:
          type: string
          maxLength: 280
        messenger:
          type: string
          maxLength: 280
        telegram:
          type: string
          maxLength: 280
        sms:
          type: string
          maxLength: 280
        email:
          type: string
          maxLength: 280
    EmbedConfig:
      type: object
      additionalProperties: false
      description: |
        The panel's look, copy and sharing. Every key is optional; a key left out keeps its
        default, and an unknown key is `400`. Sent whole: a new `embed_config` replaces the old
        one. At most 8 KB once written as JSON.
      properties:
        accent:
          type: string
          pattern: ^#[0-9a-fA-F]{6}$
          description: The accent colour, like `#2A9D8F`.
        radius:
          type: integer
          minimum: 0
          maximum: 24
          description: Corner radius in pixels.
        theme:
          type: string
          enum:
            - light
            - dark
            - auto
          description: '`auto` follows the visitor''s device.'
        link_mode:
          type: string
          enum:
            - boltclaw
            - direct
          description: |
            `boltclaw` (default): the referrer's link goes through the program's own `/r/{code}`
            redirect, which counts clicks. `direct`: your own site with `?via={code}`, which names
            no other domain; clicks are then counted only when your server reports them.
        invite_from_own_system:
          type: boolean
          description: |
            Shows a "send from my own system" field. The panel hands the address a referrer types
            to your page (the `inviteRequested` event) and nothing else: Bolt Claw never sees or
            emails it.
        channels:
          type: array
          maxItems: 9
          items:
            $ref: '#/components/schemas/ShareChannel'
          description: The share buttons to show, in order. Left out, all nine.
        copy:
          $ref: '#/components/schemas/EmbedCopy'
        messages:
          $ref: '#/components/schemas/EmbedMessages'
        email_subject:
          type: string
          maxLength: 120
        email_body:
          type: string
          maxLength: 1000
    EmbedSettings:
      type: object
      required:
        - enabled
        - key
        - panel_url
        - origins
        - generation
        - approve_referrers
        - config
      description: A program's in-app referral panel. Change it with `embed_enabled`, `embed_origins`, `embed_config` and `approve_referrers` on `PATCH /referrals/programs/{id}`.
      properties:
        enabled:
          type: boolean
          description: Whether sessions can be minted and the panel loads. Off, both fail.
        key:
          type: string
          description: The public embed key your page passes as `embedKey`. Not a secret; rotate it with `POST /referrals/programs/{id}/embed/rotate-key`.
        panel_url:
          type: string
          description: The address the panel's iframe loads.
        origins:
          type: array
          maxItems: 10
          items:
            type: string
          description: |
            The sites allowed to frame the panel, like `https://app.example.com`. https only
            (`http://localhost:3000`, with a port, is allowed for development), no
            wildcard, no path. Any other site that tries to frame it is refused by the browser.
        generation:
          type: integer
          minimum: 1
          description: Goes up by one each time you end every live session.
        approve_referrers:
          type: boolean
          description: 'True (default): a new referrer is `approved` at once. False: `pending` until you approve.'
        config:
          $ref: '#/components/schemas/EmbedConfig'
    PanelNoticeCode:
      type: string
      enum:
        - program_paused
        - referrer_not_approved
    ReasonCode:
      type: string
      description: |
        The stable code beside each history sentence. Translate by code; the English `reason` is a
        convenience. A code is never renamed or reused, and a new one only ever joins the end.

        `awaiting_payment`: the friend joined; nothing paid yet. `on_hold_until`: the friend paid;
        the reward waits out the program's hold, until `hold_until`, in case of a refund.
        `reward_ready`: the hold is over; it goes out in the next payout. `reward_paid`: it has
        been paid. `under_review`: the program is checking it first. `refunded`: the friend's
        payment was refunded, so no reward. `no_reward`: the program pays nothing for this event.
        `referrer_not_approved`: the referrer is not approved, so nothing is earned.
      enum:
        - awaiting_payment
        - on_hold_until
        - reward_ready
        - reward_paid
        - under_review
        - refunded
        - no_reward
        - referrer_not_approved
    HistoryStage:
      type: string
      enum:
        - joined
        - paid
        - rewarded
        - not_counted
      description: 'Where the friend is: `joined`, `paid`, `rewarded` (the reward is paid out) or `not_counted`.'
    PanelHistoryRow:
      type: object
      required:
        - id
        - label
        - stage
        - reason_code
        - reason
        - occurred_at
        - hold_until
        - reward
      properties:
        id:
          type: string
          description: Stable for a friend, like `f3`. Not an id of ours or yours.
        label:
          type: string
          description: '`Friend 3`: friends are numbered in the order they joined, never named.'
        stage:
          $ref: '#/components/schemas/HistoryStage'
        reason_code:
          $ref: '#/components/schemas/ReasonCode'
        reason:
          type: string
          description: One English sentence saying why the row is where it is. Translate by `reason_code`.
        occurred_at:
          type: string
          format: date-time
        hold_until:
          type:
            - string
            - 'null'
          format: date-time
          description: When the reward is ready, for `on_hold_until`; otherwise null.
        reward:
          type:
            - object
            - 'null'
          description: The reward this friend has earned so far, net of refunds; null when there is none.
          required:
            - amount_minor
            - currency
          properties:
            amount_minor:
              type: integer
            currency:
              type: string
    PayoutMethodForReferrer:
      type: string
      enum:
        - paypal
        - wise
    ReferrerPanelView:
      type: object
      description: |
        Everything the panel draws for one referrer, as data: the same View behind the hosted panel,
        the browser client and `GET /referrals/programs/{id}/referrers/{external_id}`, so your own
        screen can show exactly what the panel shows. Keys are snake_case; values are plain text.
      required:
        - mode
        - generated_at
        - referrer
        - program
        - notice
        - offer
        - stats
        - history
        - payout
        - share
        - look
        - copy
        - grants
        - ask
        - thank_you
      properties:
        mode:
          type: string
          enum:
            - panel
            - preview
          description: '`preview` is sample data.'
        generated_at:
          type: string
          format: date-time
        referrer:
          type: object
          required:
            - external_id
            - name
            - status
            - code
            - link_url
          properties:
            external_id:
              type: string
            name:
              type:
                - string
                - 'null'
            status:
              $ref: '#/components/schemas/ReferrerStatus'
            code:
              type: string
            link_url:
              type: string
              description: The link to share.
        program:
          type: object
          required:
            - name
            - currency
            - status
          properties:
            name:
              type: string
            currency:
              type: string
            status:
              type: string
              enum:
                - active
                - paused
        notice:
          type:
            - object
            - 'null'
          description: A line above everything when something stops all rewards (a paused program, an unapproved referrer); otherwise null.
          required:
            - code
            - text
          properties:
            code:
              $ref: '#/components/schemas/PanelNoticeCode'
            text:
              type: string
        offer:
          type: object
          required:
            - headline
            - subline
            - reward_text
            - friend_text
            - steps
            - rules
            - faq
            - terms
          properties:
            headline:
              type: string
            subline:
              type:
                - string
                - 'null'
            reward_text:
              type:
                - string
                - 'null'
              description: What the referrer earns, in one phrase from the rules in force.
            friend_text:
              type:
                - string
                - 'null'
              description: The friend's side as you wrote it (`customer_discount`).
            steps:
              type: array
              minItems: 3
              maxItems: 3
              items:
                type: string
            rules:
              type: array
              items:
                type: object
                required:
                  - kind
                  - text
                properties:
                  kind:
                    type: string
                    enum:
                      - counts
                      - doesnt_count
                      - timing
                  text:
                    type: string
            faq:
              type: array
              items:
                type: object
                required:
                  - question
                  - answer
                properties:
                  question:
                    type: string
                  answer:
                    type: string
            terms:
              type:
                - object
                - 'null'
              required:
                - text
                - version
              properties:
                text:
                  type: string
                version:
                  type: integer
        stats:
          type: object
          required:
            - clicks
            - friends_joined
            - friends_paid
            - rewards
          properties:
            clicks:
              type: integer
            friends_joined:
              type: integer
            friends_paid:
              type: integer
            rewards:
              type: array
              description: One entry per currency.
              items:
                type: object
                required:
                  - currency
                  - waiting_minor
                  - ready_minor
                  - paid_minor
                properties:
                  currency:
                    type: string
                  waiting_minor:
                    type: integer
                    description: Earned and still in the hold.
                  ready_minor:
                    type: integer
                    description: Past the hold
                    goes out in the next payout.: null
                  paid_minor:
                    type: integer
        history:
          type: array
          maxItems: 50
          description: One row per friend, newest first, at most 50.
          items:
            $ref: '#/components/schemas/PanelHistoryRow'
        payout:
          type: object
          required:
            - needed
            - set
            - method
            - methods
          properties:
            needed:
              type: boolean
              description: True when there is a reward to receive and no payout details are saved.
            set:
              type: boolean
            method:
              oneOf:
                - $ref: '#/components/schemas/PayoutMethodForReferrer'
                - type: 'null'
            methods:
              type: array
              items:
                $ref: '#/components/schemas/PayoutMethodForReferrer'
        share:
          type: object
          required:
            - mode
            - channels
            - messages
            - email_subject
            - email_body
            - invite_from_own_system
          properties:
            mode:
              type: string
              enum:
                - boltclaw
                - direct
            channels:
              type: array
              items:
                $ref: '#/components/schemas/ShareChannel'
            messages:
              $ref: '#/components/schemas/EmbedMessages'
            email_subject:
              type: string
            email_body:
              type: string
            invite_from_own_system:
              type: boolean
        look:
          type: object
          required:
            - accent
            - radius
            - theme
          properties:
            accent:
              type:
                - string
                - 'null'
            radius:
              type:
                - integer
                - 'null'
            theme:
              type: string
              enum:
                - light
                - dark
                - auto
        copy:
          $ref: '#/components/schemas/EmbedCopy'
        grants:
          type: array
          maxItems: 50
          description: |
            The referrer's own rewards that are not cash ("1 month free"), newest first, at most 50,
            each with where it stands in a sentence. Ids are this View's own (`g1`).
          items:
            type: object
            required:
              - id
              - text
              - status
              - status_text
              - occurred_at
            properties:
              id:
                type: string
              text:
                type: string
              status:
                $ref: '#/components/schemas/GrantStatus'
              status_text:
                type: string
              occurred_at:
                type: string
        ask:
          type: object
          description: Whether to ask the referrer to share now, or they said "Not right now" until `snoozed_until`.
          required:
            - should_ask
            - snoozed_until
          properties:
            should_ask:
              type: boolean
            snoozed_until:
              type:
                - string
                - 'null'
              format: date-time
        thank_you:
          type: object
          description: The words of the compact thank-you card, for a panel opened with `mode="thank-you"`.
          required:
            - headline
            - after_share
          properties:
            headline:
              type: string
            after_share:
              type: string
    GrantStatus:
      type: string
      description: |
        `pending` waits out the program's hold; `ready` is yours to give; `fulfilled` was given;
        `failed` could not be given yet; `cancelled` by a refund or dispute before it was given;
        `reversed` by one after.
      enum:
        - pending
        - ready
        - fulfilled
        - failed
        - cancelled
        - reversed
    GrantType:
      type: string
      enum:
        - credit
        - free_period
        - discount
        - perk
        - gift_card
    Grant:
      type: object
      required:
        - id
        - program_id
        - beneficiary
        - reward_type
        - value
        - summary
        - status
        - status_text
        - hold_until
        - ready_at
        - fulfilled_at
        - ended_at
        - fulfilment
        - reference
        - reason
        - recipient
        - partner
        - customer_external_id
        - conversion
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        beneficiary:
          type: string
          enum:
            - promoter
            - referee
        reward_type:
          $ref: '#/components/schemas/GrantType'
        value:
          type: object
          required:
            - amount_minor
            - currency
            - percent_bps
            - period_count
            - period_unit
            - perk_text
            - discount_duration
            - discount_months
          properties:
            amount_minor:
              type:
                - integer
                - 'null'
              description: 'Credit, a gift card, or an amount off: minor units of `currency`.'
            currency:
              type:
                - string
                - 'null'
            percent_bps:
              type:
                - integer
                - 'null'
              description: A percentage off, in basis points.
            period_count:
              type:
                - integer
                - 'null'
            period_unit:
              type:
                - string
                - 'null'
              enum:
                - day
                - month
                - null
            perk_text:
              type:
                - string
                - 'null'
            discount_duration:
              type:
                - string
                - 'null'
              enum:
                - once
                - months
                - forever
                - null
            discount_months:
              type:
                - integer
                - 'null'
        summary:
          type: string
          description: 'What it is, in a few words: "1 month free", "10.00 USD in credit".'
        status:
          $ref: '#/components/schemas/GrantStatus'
        status_text:
          type: string
          description: Where it stands
          in a sentence.: null
        hold_until:
          type:
            - string
            - 'null'
          format: date-time
        ready_at:
          type:
            - string
            - 'null'
          format: date-time
        fulfilled_at:
          type:
            - string
            - 'null'
          format: date-time
        ended_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When it was cancelled or reversed.
        fulfilment:
          type:
            - string
            - 'null'
          enum:
            - merchant
            - stripe
            - tremendous
            - null
          description: How it was or will be given; `merchant` is your own system.
        reference:
          type:
            - string
            - 'null'
          description: Your (or Stripe's) reference for it
          once given.: null
        reason:
          type:
            - string
            - 'null'
          description: Why it was cancelled
          could not be given: null
          or is waiting for you.: null
        recipient:
          type: object
          description: |
            Who receives it, as you know them: one of your own users with a link (`referrer`, by your
            `external_id`), an affiliate (`partner`, by their name in the program), or the friend
            (`customer`, by your own id for them).
          required:
            - kind
            - external_id
            - display_name
          properties:
            kind:
              type: string
              enum:
                - referrer
                - partner
                - customer
            external_id:
              type:
                - string
                - 'null'
            display_name:
              type:
                - string
                - 'null'
        partner:
          type:
            - object
            - 'null'
          description: The promoter the sale was credited to.
          required:
            - id
            - display_name
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type: string
        customer_external_id:
          type:
            - string
            - 'null'
          description: The friend
          by your own id for them.: null
        conversion:
          type:
            - object
            - 'null'
          required:
            - id
            - event
            - occurred_at
          properties:
            id:
              type: string
              format: uuid
            event:
              $ref: '#/components/schemas/ConversionEvent'
            occurred_at:
              type: string
              format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        test:
          type: boolean
          description: From TEST data. Never given through Stripe, never counted.
    GrantList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Grant'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    GrantFulfil:
      type: object
      required:
        - reference
      additionalProperties: false
      properties:
        reference:
          type: string
          minLength: 1
          maxLength: 200
          description: Your reference for the reward you gave, like an order or credit note number. The same one again is safe.
        note:
          type:
            - string
            - 'null'
          maxLength: 500
          description: A note for your own team.
    GrantFail:
      type: object
      required:
        - reason
      additionalProperties: false
      properties:
        reason:
          type: string
          minLength: 1
          maxLength: 500
          description: Why it cannot be given. Shown to you
          not to the person.: null
    Offer:
      type: object
      description: |
        The friend's offer in the words the welcome shows. It names nobody the person did not
        agree to show, and carries no id of ours.
      required:
        - inviter
        - headline
        - gift
        - conditions
        - disclosure
        - discount
        - stripe
        - cta
        - look
      properties:
        inviter:
          type:
            - string
            - 'null'
          description: The inviter's name
          only when they agreed to it being shown.: null
        headline:
          type: string
        gift:
          type: string
          description: 'The gift in a few words: "20% off for 3 months".'
        conditions:
          type: string
        disclosure:
          type:
            - string
            - 'null'
          description: |
            "Jane gets a reward if you buy": present whenever the program rewards the person who
            invited them, and not removable while it does.
        discount:
          type: object
          required:
            - kind
            - value
            - currency
            - unit
            - duration
            - months
            - text
            - code
          properties:
            kind:
              type: string
              enum:
                - percent
                - amount
                - free_period
                - credit
                - perk
                - gift_card
            value:
              type:
                - integer
                - 'null'
              description: Basis points for `percent`; minor units for `amount`, `credit` and `gift_card`; a count for `free_period`.
            currency:
              type:
                - string
                - 'null'
            unit:
              type:
                - string
                - 'null'
              enum:
                - day
                - month
                - null
            duration:
              type:
                - string
                - 'null'
              enum:
                - once
                - months
                - forever
                - null
            months:
              type:
                - integer
                - 'null'
            text:
              type:
                - string
                - 'null'
              description: A perk's own words.
            code:
              type:
                - string
                - 'null'
              description: The referrer's discount code, if they have one.
        stripe:
          type:
            - object
            - 'null'
          required:
            - promotion_code
          properties:
            promotion_code:
              type: string
              description: The referrer's promotion code in your Stripe (`promo_…`).
        cta:
          type:
            - object
            - 'null'
          required:
            - label
            - url
          properties:
            label:
              type: string
            url:
              type: string
        look:
          type: object
          required:
            - style
            - position
            - accent
            - radius
            - theme
          properties:
            style:
              type: string
              enum:
                - bar
                - corner
                - sheet
            position:
              type: string
              enum:
                - top
                - bottom
            accent:
              type:
                - string
                - 'null'
            radius:
              type:
                - integer
                - 'null'
            theme:
              type: string
              enum:
                - light
                - dark
                - auto
    OfferReasonCode:
      type: string
      description: |
        Why a customer may not have the offer: `unknown_code` (no live link of yours),
        `program_paused`, `self_referral` (the customer is the referrer), `already_referred`
        (another referrer has them), `not_new`.
      enum:
        - unknown_code
        - program_paused
        - self_referral
        - already_referred
        - not_new
    OfferAnswer:
      type: object
      required:
        - offer
        - eligible
        - reason_code
        - reason
        - coupon
      properties:
        offer:
          oneOf:
            - $ref: '#/components/schemas/Offer'
            - type: 'null'
        eligible:
          type: boolean
        reason_code:
          oneOf:
            - $ref: '#/components/schemas/OfferReasonCode'
            - type: 'null'
        reason:
          type:
            - string
            - 'null'
          description: One sentence to show at checkout instead of an error.
        coupon:
          type:
            - string
            - 'null'
          description: 'The friend''s coupon in your Stripe, for `discounts: [{ coupon }]`.'
    CustomerReferral:
      type: object
      required:
        - invited_by
        - invited_at
        - via
      properties:
        invited_by:
          type:
            - string
            - 'null'
          description: The inviter's name
          only when they agreed to it being shown.: null
        invited_at:
          type:
            - string
            - 'null'
          format: date-time
        via:
          type:
            - string
            - 'null'
          enum:
            - link
            - discount_code
            - null
    DiscountCode:
      type: object
      required:
        - id
        - code
        - active
        - created_at
        - partner
        - stripe_promotion_code
        - shopify_discount
        - shopify_error
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
          description: 3 to 40 letters
          digits and hyphens. Unique in the program: null
          ignoring case.: null
        active:
          type: boolean
          description: False once the code was switched off.
        created_at:
          type: string
          format: date-time
        partner:
          type:
            - object
            - 'null'
          required:
            - id
            - display_name
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type: string
        stripe_promotion_code:
          type:
            - string
            - 'null'
          description: The promotion code this code is in your own Stripe.
        shopify_discount:
          type:
            - string
            - 'null'
          description: The discount this code is in the program's linked Shopify shop.
        shopify_error:
          type:
            - string
            - 'null'
          enum:
            - no_friend_discount
            - refused
            - unavailable
            - taken
            - null
          description: Why the last Shopify sync could not make it there.
    FriendDiscount:
      type: object
      description: The discount a friend gets with a partner's code, from the program's friend's discount rule.
      required:
        - kind
        - percent_bps
        - amount_minor
        - currency
        - discount_duration
        - discount_months
      properties:
        kind:
          type: string
          enum:
            - percent
            - amount
        percent_bps:
          type:
            - integer
            - 'null'
          description: 'Basis points: 1000 is 10%.'
        amount_minor:
          type:
            - integer
            - 'null'
          description: Minor units of `currency`.
        currency:
          type: string
        discount_duration:
          type: string
          enum:
            - once
            - months
            - forever
        discount_months:
          type:
            - integer
            - 'null'
    DiscountCodeList:
      type: object
      required:
        - data
        - next_cursor
        - friend_discount
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/DiscountCode'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
        friend_discount:
          oneOf:
            - $ref: '#/components/schemas/FriendDiscount'
            - type: 'null'
    InviteCreate:
      type: object
      properties:
        max_uses:
          type: integer
          minimum: 1
          maximum: 100000
          description: How many people may join with it. 1 by default.
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
          description: Within a year.
    Invite:
      type: object
      required:
        - id
        - url
        - max_uses
        - used_count
        - expires_at
        - created_at
        - state
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          description: The link to send. Bolt Claw emails nobody.
        max_uses:
          type: integer
        used_count:
          type: integer
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        state:
          type: string
          enum:
            - active
            - expired
            - used
    ShopifyCodesResult:
      type: object
      required:
        - shop
        - created
        - matched
        - deactivated
        - failed
        - remaining
        - reason
      properties:
        shop:
          type:
            - string
            - 'null'
          description: The linked shop's myshopify.com name.
        created:
          type: integer
          description: Discounts made in the shop now.
        matched:
          type: integer
          description: Codes the shop already had with the same text
          now known.: null
        deactivated:
          type: integer
          description: Codes switched off in Bolt Claw
          now switched off in the shop.: null
        failed:
          type: integer
          description: Codes the shop refused or did not answer for. They still work as codes Bolt Claw knows.
        remaining:
          type: integer
          description: Codes waiting for the next call.
        reason:
          type:
            - string
            - 'null'
          enum:
            - not_linked
            - no_friend_discount
            - reconnect
            - null
          description: 'Why nothing could be synced: no linked shop, no friend''s discount rule, or the shop must open the app again.'
    PrivacyRequest:
      type: object
      required:
        - id
        - program_id
        - source
        - kind
        - shop
        - status
        - result
        - received_at
        - answered_at
        - due_by
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        source:
          type: string
          enum:
            - shopify
        kind:
          type: string
          enum:
            - data_request
            - customer_redact
            - shop_redact
        shop:
          type: string
          description: The shop's myshopify.com name.
        status:
          type: string
          enum:
            - open
            - answered
            - done
        result:
          type:
            - object
            - 'null'
          description: What an erasure made anonymous, as counts.
          required:
            - customers
            - conversions
            - referrers
          properties:
            customers:
              type: integer
            conversions:
              type: integer
            referrers:
              type: integer
        received_at:
          type: string
          format: date-time
        answered_at:
          type:
            - string
            - 'null'
          format: date-time
        due_by:
          type: string
          format: date-time
          description: Thirty days after Shopify asked.
    PrivacyRequestList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PrivacyRequest'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    DataSubject:
      type: object
      description: Everything Bolt Claw holds about one of your customers or in-app referrers in this workspace.
      required:
        - external_id
        - customers
        - referrers
      properties:
        external_id:
          type: string
          description: The id you looked up.
        customers:
          type: array
          description: Each program the person was referred in, as a customer.
          items:
            type: object
            required:
              - id
              - program_id
              - program_name
              - livemode
              - first_seen_at
              - attributed_at
              - partner_id
              - matched_by
              - has_processor_customer_id
              - has_email_fingerprint
              - conversions
              - grants
            properties:
              id:
                type: string
                format: uuid
              program_id:
                type: string
                format: uuid
              program_name:
                type: string
              livemode:
                type: boolean
              first_seen_at:
                type: string
                format: date-time
              attributed_at:
                type:
                  - string
                  - 'null'
                format: date-time
              partner_id:
                type:
                  - string
                  - 'null'
                format: uuid
                description: The promoter they were credited to.
              matched_by:
                type: string
                enum:
                  - external_id
                  - processor_customer_id
              has_processor_customer_id:
                type: boolean
              has_email_fingerprint:
                type: boolean
                description: Whether an email fingerprint is held. Never its value.
              conversions:
                type: array
                items:
                  type: object
                  required:
                    - id
                    - event
                    - amount_minor
                    - currency
                    - source
                    - occurred_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    event:
                      type: string
                    amount_minor:
                      type: integer
                    currency:
                      type:
                        - string
                        - 'null'
                    source:
                      type: string
                    occurred_at:
                      type: string
                      format: date-time
              grants:
                type: array
                items:
                  $ref: '#/components/schemas/DataSubjectGrant'
        referrers:
          type: array
          description: Each program the person is an in-app referrer in.
          items:
            type: object
            required:
              - id
              - program_id
              - program_name
              - display_name
              - status
              - created_at
              - terms_version_accepted
              - leaderboard_name
              - has_email_fingerprint
              - has_payout_details
              - links
              - commissions
              - grants
            properties:
              id:
                type: string
                format: uuid
              program_id:
                type: string
                format: uuid
              program_name:
                type: string
              display_name:
                type: string
              status:
                type: string
              created_at:
                type: string
                format: date-time
              terms_version_accepted:
                type:
                  - integer
                  - 'null'
              leaderboard_name:
                type:
                  - string
                  - 'null'
              has_email_fingerprint:
                type: boolean
              has_payout_details:
                type: boolean
              links:
                type: array
                items:
                  type: string
                description: The codes of their live links.
              commissions:
                type: array
                items:
                  type: object
                  required:
                    - id
                    - status
                    - amount_minor
                    - currency
                    - created_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    status:
                      type: string
                    amount_minor:
                      type: integer
                    currency:
                      type: string
                    created_at:
                      type: string
                      format: date-time
              grants:
                type: array
                items:
                  $ref: '#/components/schemas/DataSubjectGrant'
    DataSubjectGrant:
      type: object
      required:
        - id
        - reward_type
        - beneficiary
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
        reward_type:
          type: string
        beneficiary:
          type: string
          enum:
            - promoter
            - referee
        status:
          type: string
        created_at:
          type: string
          format: date-time
    DataSubjectErased:
      type: object
      description: What was erased, counted. Amounts and commissions stay as anonymous rows.
      required:
        - external_id
        - customers
        - conversions
        - referrers
        - webhook_payloads
      properties:
        external_id:
          type: string
        customers:
          type: integer
          description: Customer records turned into anonymous ones.
        conversions:
          type: integer
          description: Their conversions
          kept with their amounts and no id.: null
        referrers:
          type: integer
          description: In-app referrers erased.
        webhook_payloads:
          type: integer
          description: Stored webhook bodies whose copy of the id was scrubbed.
    TermsDraftRequest:
      type: object
      required:
        - answers
      additionalProperties: false
      properties:
        answers:
          type: object
          additionalProperties: false
          required:
            - business_name
            - contact_email
            - governing_law
            - courts
          properties:
            business_name:
              type: string
              maxLength: 200
              description: Your business's legal name
              as it signs contracts.: null
            contact_email:
              type: string
              maxLength: 254
              description: Where partners write about the program.
            who_may_join:
              type: string
              enum:
                - anyone
                - customers
                - invited
              default: anyone
            minimum_age:
              type: integer
              minimum: 16
              maximum: 21
              default: 18
            excluded_countries:
              type: string
              maxLength: 300
              description: Countries partners may not join from
              in words. Empty for none.: null
            forfeiture_months:
              type: integer
              minimum: 0
              maximum: 24
              default: 6
              description: Months money held for a missing tax form waits before it may be forfeited; 0 for never.
            governing_law:
              type: string
              maxLength: 200
              description: For example "the laws of England and Wales".
            courts:
              type: string
              maxLength: 200
              description: For example "the courts of England and Wales".
    TermsDraft:
      type: object
      required:
        - terms
        - settings
        - clauses
        - notice
      properties:
        terms:
          type: string
          description: The assembled terms. Edit them as you wish before saving.
        settings:
          type: object
          description: What was read from the program's own settings, which the terms describe.
          additionalProperties: true
        clauses:
          type: array
          items:
            type: string
          description: The template's clauses the draft holds
          by id.: null
        notice:
          type: string
          description: Based on Bolt Claw's template. Not legal advice; have it checked for the countries your partners live in.
    PrivacyRequestDetail:
      allOf:
        - $ref: '#/components/schemas/PrivacyRequest'
        - type: object
          required:
            - data
          properties:
            data:
              type:
                - object
                - 'null'
              description: For a data request still waiting, what Bolt Claw holds about the customer it named.
              required:
                - note
                - customers
              properties:
                note:
                  type: string
                customers:
                  type: array
                  items:
                    type: object
                    required:
                      - customer_id
                      - first_seen_at
                      - referred_by
                      - has_email_fingerprint
                      - conversions
                      - grants
                      - referrer
                    properties:
                      customer_id:
                        type: string
                        description: Your id for the customer (or the Shopify customer id).
                      first_seen_at:
                        type: string
                        format: date-time
                      referred_by:
                        type:
                          - string
                          - 'null'
                        description: The partner's display name.
                      has_email_fingerprint:
                        type: boolean
                      conversions:
                        type: array
                        items:
                          type: object
                          required:
                            - event
                            - amount_minor
                            - currency
                            - occurred_at
                            - order
                          properties:
                            event:
                              type: string
                            amount_minor:
                              type: integer
                            currency:
                              type:
                                - string
                                - 'null'
                            occurred_at:
                              type: string
                              format: date-time
                            order:
                              type:
                                - string
                                - 'null'
                              description: The Shopify order id.
                      grants:
                        type: array
                        items:
                          type: object
                          required:
                            - reward_type
                            - status
                            - created_at
                          properties:
                            reward_type:
                              type: string
                            status:
                              type: string
                            created_at:
                              type: string
                              format: date-time
                      referrer:
                        type:
                          - object
                          - 'null'
                        required:
                          - status
                          - link
                        properties:
                          status:
                            type: string
                          link:
                            type:
                              - string
                              - 'null'
    StripeCodesResult:
      type: object
      required:
        - coupon
        - created
        - linked
        - failed
      properties:
        coupon:
          type: string
          description: The coupon behind every code
          made from the friend's discount rule.: null
        created:
          type: integer
          description: Promotion codes made in Stripe now.
        linked:
          type: integer
          description: Codes Stripe already had with the same text
          now linked.: null
        failed:
          type: integer
          description: Codes Stripe refused. They still work as codes Bolt Claw knows.
    PayoutStatus:
      type: string
      description: |
        `unclaimed`: the provider holds it until the promoter claims it (PayPal's sign-up link, a
        reward email that bounced). `returned`: it came back before it was paid, and its commissions
        are back in Ready to pay.
      enum:
        - draft
        - processing
        - unclaimed
        - paid
        - failed
        - returned
        - cancelled
    PayoutProviderName:
      type: string
      description: |
        How a payout is paid. `manual`: a file you pay from yourself. `webhook`: your own payout
        system. The rest: your own account at that provider. BoltClaw never holds the money.
      enum:
        - manual
        - webhook
        - paypal
        - wise
        - stripe_connect
        - tremendous
        - trolley
        - tango
    PayoutFailureCode:
      type: string
      enum:
        - invalid_destination
        - recipient_not_ready
        - insufficient_funds
        - provider_refused
        - unclaimed_expired
        - returned
        - not_funded
        - cancelled_at_provider
        - limit_exceeded
        - delivery_failed
        - other
    LeftOutReason:
      type:
        - string
        - 'null'
      enum:
        - no_details
        - other_currency
        - not_paypal
        - not_wise
        - wise_currency
        - wise_usd_outside_us
        - payout_hold
        - null
    Payout:
      type: object
      required:
        - id
        - batch_id
        - status
        - provider
        - currency
        - amount_minor
        - short_reference
        - reference
        - paid_at
        - created_at
        - updated_at
        - partner
        - country
        - exports
        - delivery
      properties:
        id:
          type: string
          format: uuid
        batch_id:
          type:
            - string
            - 'null'
          format: uuid
        status:
          $ref: '#/components/schemas/PayoutStatus'
        provider:
          $ref: '#/components/schemas/PayoutProviderName'
        currency:
          type: string
        amount_minor:
          type: integer
        short_reference:
          type: string
        reference:
          type:
            - string
            - 'null'
        paid_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        partner:
          $ref: '#/components/schemas/PartnerRef'
        country:
          type:
            - string
            - 'null'
        exports:
          type:
            - object
            - 'null'
          description: Which export file an open payout would go in; null where it would, else why not.
          required:
            - method
            - generic
            - paypal
            - wise
          properties:
            method:
              type:
                - string
                - 'null'
              enum:
                - paypal
                - wise
                - bank
                - other
                - null
            generic:
              $ref: '#/components/schemas/LeftOutReason'
            paypal:
              $ref: '#/components/schemas/LeftOutReason'
            wise:
              $ref: '#/components/schemas/LeftOutReason'
        delivery:
          $ref: '#/components/schemas/PayoutDelivery'
    PayoutDelivery:
      type:
        - object
        - 'null'
      description: How a payout sent through a provider went; null until it was sent.
      required:
        - sent_at
        - provider_ref
        - awaiting_funding_at
        - unclaimed_at
        - failure
        - returned_after_paid_at
      properties:
        sent_at:
          type: string
          format: date-time
        provider_ref:
          type:
            - string
            - 'null'
          description: 'The provider''s own id for it: what to look for in your provider account.'
        awaiting_funding_at:
          type:
            - string
            - 'null'
          format: date-time
          description: Waiting for you to fund it at the provider (Wise, Trolley), since when.
        unclaimed_at:
          type:
            - string
            - 'null'
          format: date-time
        failure:
          type:
            - object
            - 'null'
          required:
            - code
            - sentence
          properties:
            code:
              $ref: '#/components/schemas/PayoutFailureCode'
            sentence:
              type: string
        returned_after_paid_at:
          type:
            - string
            - 'null'
          format: date-time
          description: It came back AFTER it was paid. Nothing was changed; it is yours to decide.
    PayoutList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Payout'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    PayoutMarkPaid:
      type: object
      required:
        - reference
      additionalProperties: false
      properties:
        reference:
          type: string
          minLength: 1
          maxLength: 200
          description: Your payment reference; trimmed.
        expected_minor:
          type: integer
          minimum: 0
          description: The amount you were shown.
    PayoutProvider:
      type: object
      description: A payout account a program pays through, by name and state. Never a credential.
      required:
        - id
        - provider
        - name
        - mode
        - status
        - problem
        - verified_at
        - last_used_at
        - label
        - needs
      properties:
        id:
          type: string
          format: uuid
        provider:
          type: string
          enum:
            - webhook
            - paypal
            - wise
            - stripe_connect
            - tremendous
            - trolley
            - tango
        name:
          type: string
        mode:
          type: string
          enum:
            - live
            - sandbox
          description: A sandbox connection never pays a batch.
        status:
          type: string
          enum:
            - active
            - error
        problem:
          type:
            - string
            - 'null'
          description: Why it is not working
          as a sentence.: null
        verified_at:
          type:
            - string
            - 'null'
          format: date-time
        last_used_at:
          type:
            - string
            - 'null'
          format: date-time
        label:
          type:
            - string
            - 'null'
          description: The campaign, profile or reward it pays from.
        needs:
          type: string
          enum:
            - email
            - email_or_iban
            - stripe_account
            - trolley_recipient
            - nothing
          description: What a promoter must give before this provider can pay them.
    PayoutProviderList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PayoutProvider'
    PayoutSettings:
      type: object
      required:
        - provider
        - providers
        - schedule
        - payout_day
        - auto_send
        - auto_send_available
        - max_payout_minor
        - available
      properties:
        provider:
          $ref: '#/components/schemas/PayoutProviderName'
        providers:
          type: array
          description: The providers a promoter may choose among; the program's own provider is always one.
          items:
            $ref: '#/components/schemas/PayoutProviderName'
        schedule:
          type: string
          enum:
            - manual
            - monthly
        payout_day:
          type: integer
          minimum: 1
          maximum: 28
          description: The day a monthly batch is made (UTC).
        auto_send:
          type: boolean
        auto_send_available:
          type: boolean
          description: False until BoltClaw allows sending on a schedule, after a legal review; until then a person sends every batch.
        max_payout_minor:
          type:
            - integer
            - 'null'
          description: A payout above this needs confirming by name.
        available:
          type: array
          description: What this program can pay with now (a file, and every live connection that works).
          items:
            $ref: '#/components/schemas/PayoutProviderName'
    PayoutSettingsUpdate:
      type: object
      additionalProperties: false
      properties:
        provider:
          $ref: '#/components/schemas/PayoutProviderName'
        providers:
          type: array
          maxItems: 8
          items:
            $ref: '#/components/schemas/PayoutProviderName'
        auto_send:
          type: boolean
        payout_day:
          type: integer
          minimum: 1
          maximum: 28
        max_payout_minor:
          type:
            - integer
            - 'null'
          minimum: 1
    FeeEstimate:
      type: object
      description: What the provider may charge YOUR account for one payout. BoltClaw charges nothing.
      required:
        - min_minor
        - max_minor
        - sentence
      properties:
        min_minor:
          type:
            - integer
            - 'null'
        max_minor:
          type:
            - integer
            - 'null'
        sentence:
          type: string
    PayoutPreview:
      type: object
      required:
        - groups
        - promoters
        - waiting
        - truncated
      properties:
        groups:
          type: array
          items:
            type: object
            required:
              - currency
              - provider
              - provider_name
              - promoters
              - total_minor
              - fee_min_minor
              - fee_max_minor
              - fee_label
              - fee_sentence
            properties:
              currency:
                type: string
              provider:
                $ref: '#/components/schemas/PayoutProviderName'
              provider_name:
                type: string
              promoters:
                type: integer
              total_minor:
                type: integer
              fee_min_minor:
                type:
                  - integer
                  - 'null'
              fee_max_minor:
                type:
                  - integer
                  - 'null'
              fee_label:
                type: string
                description: '"estimate, charged by <provider> to your account"'
              fee_sentence:
                type: string
        promoters:
          type: array
          items:
            type: object
            required:
              - partner_id
              - display_name
              - currency
              - owed_minor
              - payable_minor
              - provider
              - provider_name
              - wait
              - wait_sentence
              - waiting_since
              - fee
            properties:
              partner_id:
                type: string
                format: uuid
              display_name:
                type: string
              currency:
                type: string
              owed_minor:
                type: integer
              payable_minor:
                type: integer
              provider:
                $ref: '#/components/schemas/PayoutProviderName'
              provider_name:
                type: string
              wait:
                type:
                  - string
                  - 'null'
                description: '`tax_form`: a tax form the program asks for is missing, expired or replaced. `invoice`: the program waits for the promoter''s own invoice.'
                enum:
                  - locked
                  - details_changed
                  - flagged
                  - negative
                  - under_minimum
                  - needs_details
                  - needs_account
                  - not_connected
                  - tax_form
                  - invoice
                  - terms
                  - null
              wait_sentence:
                type:
                  - string
                  - 'null'
              waiting_since:
                type:
                  - string
                  - 'null'
                format: date-time
                description: For `tax_form`, since when their approved money has waited for it; otherwise null.
              fee:
                $ref: '#/components/schemas/FeeEstimate'
        waiting:
          type: integer
        truncated:
          type: boolean
          description: More than the 2,000 largest balances are owed.
    PayoutBatchResult:
      type: object
      required:
        - status
        - batch_id
        - payouts
        - carried
        - held
      properties:
        status:
          type: string
          enum:
            - created
            - empty
        batch_id:
          type:
            - string
            - 'null'
          format: uuid
        payouts:
          type: integer
        carried:
          type: integer
          description: Promoters whose total was below the minimum or negative.
        held:
          type: integer
          description: Approved commissions a flag or a payout-details hold kept out.
    CurrencyTotal:
      type: object
      required:
        - currency
        - amount_minor
      properties:
        currency:
          type: string
        amount_minor:
          type: integer
    PayoutBatch:
      type: object
      required:
        - id
        - program_id
        - label
        - created_at
        - sent_at
        - totals
        - sendable
        - lines
        - waiting_for_funding
        - auto_send
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        label:
          type: string
        created_at:
          type: string
          format: date-time
        sent_at:
          type:
            - string
            - 'null'
          format: date-time
        totals:
          type: array
          description: What the batch held when it was made.
          items:
            $ref: '#/components/schemas/CurrencyTotal'
        sendable:
          type: object
          required:
            - payout_ids
            - totals
            - providers
            - large
          properties:
            payout_ids:
              type: array
              items:
                type: string
                format: uuid
            totals:
              type: array
              description: Send these back as `expected_totals`.
              items:
                $ref: '#/components/schemas/CurrencyTotal'
            providers:
              type: array
              items:
                $ref: '#/components/schemas/PayoutProviderName'
            large:
              type: array
              description: Payouts to name in `confirm_large`.
              items:
                type: string
                format: uuid
        lines:
          type: array
          items:
            type: object
            required:
              - id
              - partner_id
              - display_name
              - provider
              - provider_name
              - status
              - amount_minor
              - currency
              - ready
              - wait
              - wait_sentence
              - large
              - average_minor
              - fee
              - delivery
            properties:
              id:
                type: string
                format: uuid
              partner_id:
                type: string
                format: uuid
              display_name:
                type: string
              provider:
                $ref: '#/components/schemas/PayoutProviderName'
              provider_name:
                type: string
              status:
                $ref: '#/components/schemas/PayoutStatus'
              amount_minor:
                type: integer
              currency:
                type: string
              ready:
                type: boolean
              wait:
                type:
                  - string
                  - 'null'
                enum:
                  - manual
                  - not_connected
                  - changed
                  - zero
                  - held
                  - flagged
                  - needs_details
                  - needs_account
                  - tax_form
                  - invoice
                  - terms
                  - null
              wait_sentence:
                type:
                  - string
                  - 'null'
              large:
                type: boolean
              average_minor:
                type:
                  - integer
                  - 'null'
              fee:
                $ref: '#/components/schemas/FeeEstimate'
              delivery:
                $ref: '#/components/schemas/PayoutDelivery'
        waiting_for_funding:
          type: array
          items:
            type: object
            required:
              - provider
              - provider_name
              - payouts
              - link
            properties:
              provider:
                $ref: '#/components/schemas/PayoutProviderName'
              provider_name:
                type: string
              payouts:
                type: integer
              link:
                type: string
        auto_send:
          type: object
          required:
            - requested_at
            - send_at
            - cancelled_at
            - sent_at
          properties:
            requested_at:
              type:
                - string
                - 'null'
              format: date-time
            send_at:
              type:
                - string
                - 'null'
              format: date-time
            cancelled_at:
              type:
                - string
                - 'null'
              format: date-time
            sent_at:
              type:
                - string
                - 'null'
              format: date-time
    PayoutSend:
      type: object
      required:
        - expected_totals
      additionalProperties: false
      properties:
        payout_ids:
          type: array
          maxItems: 5000
          description: Send only these; every payout in `sendable` when absent.
          items:
            type: string
            format: uuid
        expected_totals:
          type: object
          description: 'What you expect to send, per currency, in minor units: `{"USD": 12000}`. Must equal `sendable.totals`.'
          additionalProperties:
            type: integer
            minimum: 1
        confirm_large:
          type: array
          maxItems: 5000
          items:
            type: string
            format: uuid
    PayoutSendResult:
      type: object
      required:
        - batch_id
        - claimed
        - results
      properties:
        batch_id:
          type: string
          format: uuid
        claimed:
          type: integer
        results:
          type: array
          items:
            type: object
            required:
              - payout_id
              - status
              - failure
            properties:
              payout_id:
                type: string
                format: uuid
              status:
                type: string
                enum:
                  - paid
                  - sent
                  - awaiting_funding
                  - unclaimed
                  - requested
                  - failed
                  - unknown
                description: '`unknown`: the provider did not answer in time; BoltClaw asks it before anything is sent again.'
              failure:
                type:
                  - string
                  - 'null'
    AutoSendCancelled:
      type: object
      required:
        - cancelled
      properties:
        cancelled:
          type: boolean
    PayoutDetails:
      type: object
      required:
        - payout_id
        - amount_minor
        - currency
        - reference
        - method
        - details
        - external_id
      properties:
        payout_id:
          type: string
          format: uuid
        amount_minor:
          type: integer
        currency:
          type: string
        reference:
          type: string
        method:
          type:
            - string
            - 'null'
          enum:
            - paypal
            - wise
            - bank
            - other
            - null
        details:
          type:
            - object
            - 'null'
          description: The promoter's own payout details for this method (an email, or bank fields), in snake_case.
          additionalProperties:
            type: string
        external_id:
          type:
            - string
            - 'null'
          description: For one of your own users (a referrer), your own id for them.
    PayoutResult:
      type: object
      required:
        - status
      additionalProperties: false
      properties:
        status:
          type: string
          enum:
            - paid
            - failed
            - returned
        reference:
          type: string
          maxLength: 200
          description: Your own payment reference (paid only).
        reason:
          type: string
          enum:
            - invalid_destination
            - recipient_not_ready
            - insufficient_funds
            - returned
            - other
          description: Why it failed or came back (required unless paid).
    TaxRequirement:
      type: string
      description: |
        Whether a program's payouts wait for a tax form. `off`: never. `us_only`: a US person needs
        a W-9, and anyone else signs a short statement that they are not a US person. `all`:
        everyone needs a W-9, W-8BEN or W-8BEN-E.
      enum:
        - 'off'
        - us_only
        - all
    TaxProfileStatus:
      type: object
      description: |
        Whether your own tax details are complete. Never the details themselves: your EIN, legal
        name and addresses are entered and read in the dashboard only.
      required:
        - country
        - domestic_vat_bps
        - payer_complete
        - invoice_complete
        - updated_at
      properties:
        country:
          type:
            - string
            - 'null'
          description: Your business's country, two letters.
        domestic_vat_bps:
          type:
            - integer
            - 'null'
          description: Your own VAT rate in basis points (2000 is 20%), used for a promoter in your own country.
        payer_complete:
          type: boolean
          description: Your 1099 payer details (legal name, EIN, US address, telephone) are entered. The 1099 files need them.
        invoice_complete:
          type: boolean
          description: Your invoicing details (legal name, address, country, VAT number) are entered. Self-billing needs them.
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    NecProvider:
      type: object
      description: One payout provider, and whether what it paid counts toward your 1099-NEC, with why.
      required:
        - provider
        - label
        - included
        - default_included
        - note
        - reason
        - source
      properties:
        provider:
          $ref: '#/components/schemas/PayoutProviderName'
        label:
          type: string
        included:
          type: boolean
          description: Counted toward your 1099-NEC, as you set it.
        default_included:
          type: boolean
        note:
          type:
            - string
            - 'null'
          description: Your own note, where you changed the row.
        reason:
          type: string
          description: Why it is included or left out by default.
        source:
          type: object
          required:
            - title
            - url
          properties:
            title:
              type: string
            url:
              type: string
              format: uri
    TaxSettings:
      type: object
      required:
        - program_id
        - tax_forms
        - invoice_required
        - self_billing
        - profile
        - providers
        - sources_note
        - disclaimer
      properties:
        program_id:
          type: string
          format: uuid
        tax_forms:
          $ref: '#/components/schemas/TaxRequirement'
        invoice_required:
          type: boolean
          description: Each payout waits for the promoter's own invoice. A promoter who self-bills never waits for one.
        self_billing:
          type: boolean
          description: You issue self-billed invoices for the promoters who accept your self-billing agreement.
        profile:
          $ref: '#/components/schemas/TaxProfileStatus'
        providers:
          type: array
          description: Which payout providers count toward your 1099-NEC.
          items:
            $ref: '#/components/schemas/NecProvider'
        sources_note:
          type: string
        disclaimer:
          type: string
          description: BoltClaw prepares these forms; it does not give tax advice or file anything for you.
    TaxSettingsUpdate:
      type: object
      additionalProperties: false
      description: Send only the fields you want to change. An empty object is `400`.
      properties:
        tax_forms:
          $ref: '#/components/schemas/TaxRequirement'
        invoice_required:
          type: boolean
        self_billing:
          type: boolean
          description: Turning it on needs your invoicing details first (`409` until then).
    TaxFormType:
      type: string
      description: |
        `w9`: Form W-9, from a US person. `w8ben`: Form W-8BEN, from a person outside the US.
        `w8bene`: Form W-8BEN-E, from an organisation outside the US. `foreign_statement`: a short
        statement that they are not a US person, which a program set to `us_only` accepts instead
        of a W-8.
      enum:
        - w9
        - w8ben
        - w8bene
        - foreign_statement
    TaxFormSummary:
      type:
        - object
        - 'null'
      description: A promoter's latest form for your business, by type and status; null when they have none. Never its contents.
      required:
        - id
        - type
        - name
        - revision
        - status
        - signed_at
        - valid_until
      properties:
        id:
          type: string
          format: uuid
        type:
          $ref: '#/components/schemas/TaxFormType'
        name:
          type: string
          description: The form's name, such as Form W-9.
        revision:
          type: string
          description: The IRS revision it was built from.
        status:
          type: string
          enum:
            - signed
            - superseded
            - expired
            - invalid
          description: |
            `superseded`: replaced after a change of name, tax number or address, and not signed
            again yet. `expired`: a W-8 past its last day. `invalid`: it no longer matches what was
            signed; ask them to sign again.
        signed_at:
          type: string
          format: date-time
        valid_until:
          type:
            - string
            - 'null'
          format: date-time
          description: A W-8's last day (31 December of the third year after it was signed). Null for a W-9 or the statement.
    TaxFormStatus:
      type: object
      required:
        - partner_id
        - display_name
        - origin
        - form
        - requirement_met
        - waiting_since
        - forfeitable_minor
      properties:
        partner_id:
          type: string
          format: uuid
        display_name:
          type: string
        origin:
          type: string
          enum:
            - account
            - managed
          description: '`managed`: one of your own users (a referrer).'
        form:
          $ref: '#/components/schemas/TaxFormSummary'
        requirement_met:
          type: boolean
          description: False while their payouts wait for a form.
        waiting_since:
          type:
            - string
            - 'null'
          format: date-time
          description: Since when their approved money has waited for a form; null when nothing waits.
        forfeitable_minor:
          type: integer
          description: The approved money waiting for a form, in minor units, which a person may forfeit.
    TaxFormStatusList:
      type: object
      required:
        - program_id
        - tax_forms
        - data
        - next_cursor
        - disclaimer
      properties:
        program_id:
          type: string
          format: uuid
        tax_forms:
          $ref: '#/components/schemas/TaxRequirement'
        data:
          type: array
          items:
            $ref: '#/components/schemas/TaxFormStatus'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
        disclaimer:
          type: string
    ForfeitRequest:
      type: object
      required:
        - reason
      additionalProperties: false
      properties:
        reason:
          type: string
          minLength: 3
          maxLength: 500
          description: Why, recorded in the activity log.
    ForfeitResult:
      type: object
      required:
        - partner_id
        - forfeited
        - totals
      properties:
        partner_id:
          type: string
          format: uuid
        forfeited:
          type: integer
          description: How many commissions were given up.
        totals:
          type: array
          description: What was given up, per currency.
          items:
            $ref: '#/components/schemas/CurrencyTotal'
    NecReason:
      type: string
      description: Why a person is on your 1099-NEC file or not. `reason_text` says it in a sentence.
      enum:
        - reportable
        - below_threshold
        - backup_withholding
        - not_us_person
        - corporation
        - no_form
        - nothing_reportable
        - threshold_unset
    NecWarning:
      type: string
      description: Something to fix or check before you file. `text` says it in a sentence.
      enum:
        - no_form
        - no_tin
        - backup_withholding
        - form_superseded
        - w8_expired
        - other_currency
        - network_only
    TaxYearRecipient:
      type: object
      description: |
        One person your programs paid in the year (every membership of one account together), and
        whether they belong on your 1099-NEC. No tax number and no legal name.
      required:
        - reference
        - partner_ids
        - display_name
        - origin
        - form
        - included
        - listed_for_backup_withholding
        - reason
        - reason_text
        - warnings
        - reportable_minor
        - excluded_minor
        - other_currencies
        - by_provider
        - e_delivery
      properties:
        reference:
          type: string
          description: Their reference in the files (`P-` and ten characters). Never an account id.
        partner_ids:
          type: array
          description: Their memberships in your programs.
          items:
            type: string
            format: uuid
        display_name:
          type: string
        origin:
          type: string
          enum:
            - account
            - managed
        form:
          $ref: '#/components/schemas/TaxFormSummary'
        included:
          type: boolean
          description: On your 1099-NEC file.
        listed_for_backup_withholding:
          type: boolean
          description: |
            No W-9 or no tax number, and something reportable was paid: listed for your accountant
            with a backup withholding mark (24%). BoltClaw does not withhold.
        reason:
          $ref: '#/components/schemas/NecReason'
        reason_text:
          type: string
        warnings:
          type: array
          items:
            type: object
            required:
              - code
              - text
            properties:
              code:
                $ref: '#/components/schemas/NecWarning'
              text:
                type: string
        reportable_minor:
          type: integer
          description: US cents paid through the providers you report.
        excluded_minor:
          type: integer
          description: US cents paid through the providers you leave out, such as PayPal.
        other_currencies:
          type: array
          description: Paid in other currencies through the providers you report. Never counted; listed for you to convert.
          items:
            $ref: '#/components/schemas/CurrencyTotal'
        by_provider:
          type: array
          description: How they were paid in the year.
          items:
            type: object
            required:
              - provider
              - currency
              - amount_minor
              - payouts
            properties:
              provider:
                $ref: '#/components/schemas/PayoutProviderName'
              currency:
                type: string
              amount_minor:
                type: integer
              payouts:
                type: integer
        e_delivery:
          type: boolean
          description: They agreed to receive their Copy B electronically. Otherwise, send them paper.
    TaxYear:
      type: object
      required:
        - year
        - threshold_minor
        - payer_complete
        - recipients
        - counts
        - providers
        - sources_note
        - disclaimer
      properties:
        year:
          type: integer
        threshold_minor:
          type:
            - integer
            - 'null'
          description: |
            The year's 1099-NEC threshold in US cents (200000 for 2026). Null until it is set for
            the year, and until then no IRS file can be made for it.
        payer_complete:
          type: boolean
          description: Your 1099 payer details are entered. The files need them.
        recipients:
          type: array
          items:
            $ref: '#/components/schemas/TaxYearRecipient'
        counts:
          type: object
          required:
            - recipients
            - included
            - warnings
          properties:
            recipients:
              type: integer
            included:
              type: integer
            warnings:
              type: integer
              description: People with at least one warning.
        providers:
          type: array
          items:
            $ref: '#/components/schemas/NecProvider'
        sources_note:
          type: string
        disclaimer:
          type: string
    Invoice:
      type: object
      description: |
        An invoice for one payout: one you issued in the promoter's name (`self_billed`), or their
        own (`uploaded`). Read its PDF through `POST /referrals/invoices/{id}/link`.
      required:
        - id
        - payout_id
        - partner_id
        - kind
        - status
        - number
        - currency
        - gross_minor
        - net_minor
        - vat_minor
        - vat_bps
        - vat_treatment
        - issued_at
        - accept_by
        - accepted_at
        - disputed_at
        - dispute_reason
        - dispute_note
        - replaced_by
      properties:
        id:
          type: string
          format: uuid
        payout_id:
          type: string
          format: uuid
        partner_id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - self_billed
            - uploaded
        status:
          type: string
          enum:
            - issued
            - accepted
            - disputed
            - void
            - received
          description: |
            `issued`: accepted unless the promoter disputes it by `accept_by` (14 days).
            `accepted`. `disputed`: paused until you issue a corrected one. `void`: replaced (see
            `replaced_by`). `received`: the promoter's own uploaded invoice.
        number:
          type:
            - string
            - 'null'
          description: A self-billed invoice's number, in the promoter's own series with you, without gaps (`SB1A2B3C4D-00042`). Null for an uploaded one.
        currency:
          type: string
        gross_minor:
          type: integer
          description: The payout's amount.
        net_minor:
          type: integer
        vat_minor:
          type: integer
        vat_bps:
          type:
            - integer
            - 'null'
          description: The VAT rate in basis points, for a domestic supply.
        vat_treatment:
          type: string
          enum:
            - domestic
            - reverse_charge
            - outside_scope
            - none
          description: |
            `domestic`: the promoter is in your own country and charges your VAT rate.
            `reverse_charge`: cross-border business to business, so you account for the VAT (the
            invoice says "Reverse charge"). `outside_scope`: you are outside the EU and the UK.
            `none`: an uploaded invoice.
        issued_at:
          type: string
          format: date-time
        accept_by:
          type:
            - string
            - 'null'
          format: date-time
        accepted_at:
          type:
            - string
            - 'null'
          format: date-time
        disputed_at:
          type:
            - string
            - 'null'
          format: date-time
        dispute_reason:
          type:
            - string
            - 'null'
          enum:
            - details_wrong
            - amount_wrong
            - vat_wrong
            - not_mine
            - other
            - null
        dispute_note:
          type:
            - string
            - 'null'
          description: What the promoter wrote, up to 500 characters.
        replaced_by:
          type:
            - string
            - 'null'
          format: uuid
    InvoiceList:
      type: object
      required:
        - payout_id
        - data
      properties:
        payout_id:
          type: string
          format: uuid
        data:
          type: array
          items:
            $ref: '#/components/schemas/Invoice'
    FileLink:
      type: object
      required:
        - url
        - expires_at
      properties:
        url:
          type:
            - string
            - 'null'
          format: uri
          description: |
            Downloads the PDF once, within ten minutes. Treat it as a secret. Null in a replay of
            the same `Idempotency-Key`: ask for a new one.
        expires_at:
          type: string
          format: date-time
    BrandEffective:
      type: object
      description: The brand partners of this scope actually see, the program's override over the workspace's.
      required:
        - name
        - logo_light_url
        - logo_dark_url
        - favicon_url
        - accent
        - background
        - radius
        - show_powered_by
        - sender_name
        - reply_to
      properties:
        name:
          type:
            - string
            - 'null'
        logo_light_url:
          type:
            - string
            - 'null'
          format: uri
        logo_dark_url:
          type:
            - string
            - 'null'
          format: uri
        favicon_url:
          type:
            - string
            - 'null'
          format: uri
        accent:
          type:
            - string
            - 'null'
          description: '`#rrggbb`.'
        background:
          type:
            - string
            - 'null'
          description: '`#rrggbb`.'
        radius:
          type:
            - integer
            - 'null'
          description: Corner radius in pixels, 0 to 24.
        show_powered_by:
          type: boolean
          description: Whether "Powered by BoltClaw" shows. Off unless you turn it on.
        sender_name:
          type:
            - string
            - 'null'
          description: The name partners' emails come from; null for the default sender.
        reply_to:
          type:
            - string
            - 'null'
          format: email
          description: The confirmed support address partners' replies go to; null until one is confirmed.
    Brand:
      type: object
      required:
        - scope
        - program_id
        - name
        - accent
        - background
        - radius
        - sender_name
        - show_powered_by
        - support_email
        - support_email_verified
        - logo_light_url
        - logo_dark_url
        - favicon_url
        - effective
        - updated_at
      properties:
        scope:
          type: string
          enum:
            - workspace
            - program
          description: '`workspace`: the brand of every program. `program`: one program''s override.'
        program_id:
          type:
            - string
            - 'null'
          format: uuid
        name:
          type:
            - string
            - 'null'
          description: What this brand sets; null follows the workspace's brand (or the program's own name).
        accent:
          type:
            - string
            - 'null'
          description: '`#rrggbb`, used for buttons and links.'
        background:
          type:
            - string
            - 'null'
          description: '`#rrggbb`, the portal''s page colour.'
        radius:
          type:
            - integer
            - 'null'
          description: Corner radius in pixels, 0 to 24.
        sender_name:
          type:
            - string
            - 'null'
          description: The name partners' emails come from. The address stays BoltClaw's.
        show_powered_by:
          type:
            - boolean
            - 'null'
        support_email:
          type:
            - string
            - 'null'
          format: email
        support_email_verified:
          type: boolean
          description: Confirmed with a code; only then is it the reply-to of partners' emails.
        logo_light_url:
          type:
            - string
            - 'null'
          format: uri
        logo_dark_url:
          type:
            - string
            - 'null'
          format: uri
        favicon_url:
          type:
            - string
            - 'null'
          format: uri
        effective:
          $ref: '#/components/schemas/BrandEffective'
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    BrandUpdate:
      type: object
      additionalProperties: false
      description: Send only the fields you want to change; `null` clears one. An empty object is `400`.
      properties:
        program_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Change this program's override instead of the workspace's brand.
        name:
          type:
            - string
            - 'null'
          maxLength: 80
          description: No control characters and no angle brackets.
        accent:
          type:
            - string
            - 'null'
          pattern: ^#[0-9a-fA-F]{6}$
        background:
          type:
            - string
            - 'null'
          pattern: ^#[0-9a-fA-F]{6}$
        radius:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 24
        support_email:
          type:
            - string
            - 'null'
          format: email
          description: Unconfirmed until its code comes back.
        show_powered_by:
          type:
            - boolean
            - 'null'
        sender_name:
          type:
            - string
            - 'null'
          maxLength: 60
          description: 'No control characters and none of these: " < > @ , ; : \'
        logo_light_path:
          type:
            - string
            - 'null'
          description: The `path` an upload answered, or null to remove the logo.
        logo_dark_path:
          type:
            - string
            - 'null'
          description: The `path` an upload answered, or null to remove the logo for dark backgrounds.
        favicon_path:
          type:
            - string
            - 'null'
          description: The `path` an upload answered, or null to remove the icon.
    BrandFileSlot:
      type: string
      description: '`logo_light`: the logo on light backgrounds. `logo_dark`: on dark ones. `favicon`: the browser tab''s icon.'
      enum:
        - logo_light
        - logo_dark
        - favicon
    BrandUploadRequest:
      type: object
      required:
        - slot
        - content_type
      additionalProperties: false
      properties:
        program_id:
          type:
            - string
            - 'null'
          format: uuid
          description: For a program's override.
        slot:
          $ref: '#/components/schemas/BrandFileSlot'
        content_type:
          type: string
          enum:
            - image/png
            - image/jpeg
            - image/webp
    BrandUpload:
      type: object
      required:
        - upload_url
        - path
        - slot
        - expires_at
      properties:
        upload_url:
          type:
            - string
            - 'null'
          format: uri
          description: |
            PUT the file's bytes here, with its content type. One file; treat it as a secret. Null
            in a replay of the same `Idempotency-Key`: ask for a new one.
        path:
          type: string
          description: Send it to PATCH /referrals/brand as the slot's field once the file is uploaded.
        slot:
          $ref: '#/components/schemas/BrandFileSlot'
        expires_at:
          type: string
          format: date-time
    BrandScope:
      type: object
      additionalProperties: false
      description: An empty object for the workspace's brand.
      properties:
        program_id:
          type:
            - string
            - 'null'
          format: uuid
          description: A program's override.
    BrandEmailCodeSent:
      type: object
      required:
        - sent
        - to
      properties:
        sent:
          type: boolean
          enum:
            - true
        to:
          type: string
          format: email
          description: 'The support address the code went to: your own.'
    BrandEmailCodeConfirm:
      type: object
      required:
        - code
      additionalProperties: false
      properties:
        program_id:
          type:
            - string
            - 'null'
          format: uuid
        code:
          type: string
          pattern: ^[0-9]{6}$
          description: The six digits from the email.
    DomainKind:
      type: string
      description: '`links`: your referral links answer on it (`https://go.example.com/<code>`). `portal`: your partner portal and join pages, and links too.'
      enum:
        - links
        - portal
    DomainStatus:
      type: string
      description: |
        `pending_ownership`: waiting for the TXT record that proves it is yours. `pending_dns`:
        waiting for the CNAME record. `pending_certificate`: the certificate is being issued.
        `active`: answering. `detached`: its records stopped pointing at us, so it stopped
        answering; put them back to reconnect it, or it is removed after 7 days (`remove_after`).
      enum:
        - pending_ownership
        - pending_dns
        - pending_certificate
        - active
        - detached
    DnsRecord:
      type: object
      required:
        - type
        - name
        - value
        - purpose
        - done
      properties:
        type:
          type: string
          enum:
            - TXT
            - CNAME
        name:
          type: string
          description: The record's full name, as your DNS host may ask for it.
        value:
          type:
            - string
            - 'null'
          description: What it must hold; null for the CNAME until the TXT record is seen, when the target for this domain is known.
        purpose:
          type: string
          enum:
            - ownership
            - routing
            - provider
          description: '`ownership`: proves the domain is yours. `routing`: points it at us. `provider`: a second TXT record our hosting provider asks for.'
        done:
          type: boolean
          description: In place, as last checked.
    Domain:
      type: object
      required:
        - id
        - host
        - kind
        - status
        - status_text
        - all_programs
        - program_ids
        - records
        - problem
        - caa_warning
        - url
        - checked_at
        - activated_at
        - detached_at
        - remove_after
        - created_at
      properties:
        id:
          type: string
          format: uuid
        host:
          type: string
          description: Lower case, international names in their punycode form.
        kind:
          $ref: '#/components/schemas/DomainKind'
        status:
          $ref: '#/components/schemas/DomainStatus'
        status_text:
          type: string
          description: The status, in a few words.
        all_programs:
          type: boolean
          description: It serves every program, new ones included.
        program_ids:
          type: array
          description: The programs it serves when `all_programs` is false; empty otherwise.
          items:
            type: string
            format: uuid
        records:
          type: array
          description: Every DNS record it needs, in the order to set them.
          items:
            $ref: '#/components/schemas/DnsRecord'
        problem:
          type:
            - string
            - 'null'
          description: What is in the way, as a sentence; null when nothing is.
        caa_warning:
          type: boolean
          description: A CAA record on your domain does not allow letsencrypt.org, which issues the certificate.
        url:
          type:
            - string
            - 'null'
          format: uri
          description: '`https://<host>` once active.'
        checked_at:
          type:
            - string
            - 'null'
          format: date-time
        activated_at:
          type:
            - string
            - 'null'
          format: date-time
        detached_at:
          type:
            - string
            - 'null'
          format: date-time
        remove_after:
          type:
            - string
            - 'null'
          format: date-time
          description: When a detached domain is removed.
        created_at:
          type: string
          format: date-time
    DomainList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Domain'
    DomainCreate:
      type: object
      required:
        - host
        - kind
      additionalProperties: false
      properties:
        host:
          type: string
          maxLength: 300
          description: A subdomain of yours, such as go.example.com. Never a domain's root, a wildcard or an IP address.
        kind:
          $ref: '#/components/schemas/DomainKind'
        all_programs:
          type:
            - boolean
            - 'null'
          description: Serve every program, new ones included. True when left out.
        program_ids:
          type:
            - array
            - 'null'
          maxItems: 50
          description: The programs it serves, when `all_programs` is false (1 to 50).
          items:
            type: string
            format: uuid
    DomainRemoved:
      type: object
      required:
        - removed
        - host
      properties:
        removed:
          type: boolean
          enum:
            - true
        host:
          type: string
    DirectoryCategory:
      type: string
      enum:
        - software
        - ai_tools
        - education
        - finance
        - health
        - ecommerce
        - creator_tools
        - agencies
        - other
    DirectoryCard:
      type: object
      description: |
        A program as the partner directory shows it to signed-in partners. Nothing about who
        promotes it or how many do, and no id but the listing's own.
      required:
        - listing_id
        - name
        - category
        - category_label
        - pitch
        - logo_url
        - rewards_summary
        - cookie_days
        - payout_schedule
        - payout_day
        - min_payout
        - pays_through
        - countries
        - languages
        - listed_since
        - relation
      properties:
        listing_id:
          type: string
          format: uuid
          description: 'The listing''s own id: never the program''s.'
        name:
          type: string
        category:
          $ref: '#/components/schemas/DirectoryCategory'
        category_label:
          type: string
        pitch:
          type:
            - string
            - 'null'
        logo_url:
          type:
            - string
            - 'null'
        rewards_summary:
          type: array
          items:
            type: string
          description: Generated from the program's own reward rules, never typed.
        cookie_days:
          type: integer
        payout_schedule:
          type: string
          enum:
            - manual
            - monthly
        payout_day:
          type:
            - integer
            - 'null'
        min_payout:
          type: object
          required:
            - amount_minor
            - currency
          properties:
            amount_minor:
              type: integer
            currency:
              type: string
        pays_through:
          type: array
          items:
            type: string
            enum:
              - manual
              - webhook
              - paypal
              - wise
              - stripe_connect
              - tremendous
              - trolley
              - tango
        countries:
          type: array
          items:
            type: string
        languages:
          type: array
          items:
            type: string
        listed_since:
          type: string
          description: YYYY-MM-DD.
        relation:
          type: string
          enum:
            - own
            - member
            - none
          description: About the reader only.
    ListingSettings:
      type: object
      required:
        - program_id
        - listing_id
        - listed
        - category
        - pitch
        - logo_url
        - countries
        - languages
        - listed_since
        - state
        - missing
        - preview
      properties:
        program_id:
          type: string
          format: uuid
        listing_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Null until the listing is first saved.
        listed:
          type: boolean
        category:
          $ref: '#/components/schemas/DirectoryCategory'
        pitch:
          type:
            - string
            - 'null'
        logo_url:
          type:
            - string
            - 'null'
        countries:
          type: array
          items:
            type: string
        languages:
          type: array
          items:
            type: string
        listed_since:
          type:
            - string
            - 'null'
        state:
          type: string
          enum:
            - unlisted
            - listed
            - held
            - delisted
        missing:
          type: array
          description: Every condition not met yet. Listing is refused while any is.
          items:
            type: object
            required:
              - code
              - sentence
            properties:
              code:
                type: string
                enum:
                  - program_inactive
                  - invite_only
                  - no_terms
                  - no_reward
                  - org_too_new
                  - org_suspended
                  - no_sale
                  - delisted
              sentence:
                type: string
        preview:
          oneOf:
            - $ref: '#/components/schemas/DirectoryCard'
            - type: 'null'
    ListingUpdate:
      type: object
      additionalProperties: false
      properties:
        listed:
          type: boolean
        category:
          $ref: '#/components/schemas/DirectoryCategory'
        pitch:
          type:
            - string
            - 'null'
          maxLength: 280
          description: 'Plain text: no HTML.'
        countries:
          type:
            - array
            - 'null'
          maxItems: 60
          items:
            type: string
            pattern: ^[A-Za-z]{2}$
          description: Two-letter country codes the program welcomes partners from.
        languages:
          type:
            - array
            - 'null'
          maxItems: 20
          items:
            type: string
            pattern: ^[A-Za-z]{2}$
          description: Two-letter language codes, like en or de.
        logo_path:
          type:
            - string
            - 'null'
          maxLength: 200
          description: The `path` from the logo upload, or null to remove it.
    ImageUploadCreate:
      type: object
      required:
        - content_type
      additionalProperties: false
      properties:
        content_type:
          type: string
          enum:
            - image/png
            - image/jpeg
            - image/webp
    ImageUpload:
      type: object
      required:
        - upload_url
        - path
        - expires_at
      properties:
        upload_url:
          type:
            - string
            - 'null'
          description: PUT the file here. Null in a replay of the same Idempotency-Key.
        path:
          type: string
        expires_at:
          type: string
          format: date-time
    BroadcastAudience:
      type: object
      required:
        - kind
      additionalProperties: false
      properties:
        kind:
          type: string
          enum:
            - all
            - group
            - partners
        group_id:
          type: string
          format: uuid
          description: 'With `group`: the group or level.'
        partner_ids:
          type: array
          maxItems: 500
          items:
            type: string
            format: uuid
          description: 'With `partners`: the promoters you picked.'
    BroadcastCreate:
      type: object
      required:
        - subject
        - body
      additionalProperties: false
      properties:
        subject:
          type: string
          maxLength: 120
        body:
          type: string
          maxLength: 5000
          description: |
            Markdown subset: paragraphs, **bold**, *italic*, [links](https://...) and lists. No HTML,
            no images, https links only. `{name}`, `{link}` and `{code}` are filled per promoter.
        audience:
          $ref: '#/components/schemas/BroadcastAudience'
    BroadcastUpdate:
      type: object
      additionalProperties: false
      properties:
        subject:
          type: string
          maxLength: 120
        body:
          type: string
          maxLength: 5000
        audience:
          $ref: '#/components/schemas/BroadcastAudience'
    Broadcast:
      type: object
      required:
        - id
        - program_id
        - subject
        - body
        - audience
        - status
        - via
        - link_hosts
        - preview
        - test_sends
        - tested_at
        - tested_current
        - sent_at
        - finished_at
        - counts
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        subject:
          type: string
        body:
          type: string
        audience:
          $ref: '#/components/schemas/BroadcastAudience'
        status:
          type: string
          enum:
            - draft
            - sending
            - sent
        via:
          type: string
          enum:
            - dashboard
            - api
        link_hosts:
          type: array
          items:
            type: string
          description: Every host the message links to.
        preview:
          type: string
          description: The first line, with sample values in the merge fields.
        test_sends:
          type: integer
        tested_at:
          type:
            - string
            - 'null'
          format: date-time
        tested_current:
          type: boolean
          description: Whether the current text was test-sent.
        sent_at:
          type:
            - string
            - 'null'
          format: date-time
        finished_at:
          type:
            - string
            - 'null'
          format: date-time
        counts:
          type: object
          required:
            - recipients
            - emailed
            - failed
            - muted
            - referrers
            - skipped
          properties:
            recipients:
              type: integer
            emailed:
              type: integer
            failed:
              type: integer
            muted:
              type: integer
              description: Muted the program's emails; they still see it on their program page.
            referrers:
              type: integer
              description: 'Your own users in the in-app panel: never emailed by BoltClaw.'
            skipped:
              type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    BroadcastList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Broadcast'
    BroadcastTested:
      allOf:
        - $ref: '#/components/schemas/Broadcast'
        - type: object
          required:
            - sent_to
          properties:
            sent_to:
              type: string
              description: The key creator's own address, where the test went.
    BroadcastReferrer:
      type: object
      required:
        - referrer_id
        - external_id
        - display_name
        - link
        - code
      properties:
        referrer_id:
          type: string
          format: uuid
        external_id:
          type: string
          description: Your own id for the user.
        display_name:
          type: string
        link:
          type:
            - string
            - 'null'
        code:
          type:
            - string
            - 'null'
    BroadcastReferrerList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/BroadcastReferrer'
        next_cursor:
          type:
            - string
            - 'null'
    ImportRow:
      type: object
      required:
        - name
        - email
      additionalProperties: false
      properties:
        line:
          type: integer
          minimum: 1
          description: The row's line in your file, echoed in errors.
        name:
          type: string
          maxLength: 80
        email:
          type: string
          maxLength: 254
          description: Hashed on arrival, never stored, never emailed.
        code:
          type: string
          maxLength: 40
          description: A vanity code held for this person.
        group:
          type: string
          maxLength: 80
          description: A group or level of the program, by name.
        rate_bps:
          type: integer
          minimum: 1
          maximum: 10000
          description: A custom rate for this person, in basis points.
        balance_minor:
          type: integer
          minimum: 1
          description: An opening balance owed, paid as a bonus when they claim.
        currency:
          type: string
          description: 'The balance''s currency: the program''s own.'
    ImportRequest:
      type: object
      additionalProperties: false
      description: Either `rows` or `csv`, not both.
      properties:
        rows:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/ImportRow'
        csv:
          type: string
          maxLength: 65000
          description: A CSV with a header row; at most 500 rows a call.
    ImportCommit:
      allOf:
        - $ref: '#/components/schemas/ImportRequest'
        - type: object
          properties:
            import_id:
              type: string
              format: uuid
              description: Continue an import begun by an earlier part of the file.
      unevaluatedProperties: false
    ImportRowError:
      type: object
      required:
        - line
        - field
        - message
      properties:
        line:
          type: integer
        field:
          type: string
          enum:
            - name
            - email
            - code
            - group
            - rate
            - balance
            - currency
            - row
        message:
          type: string
    ImportPreview:
      type: object
      required:
        - rows
        - valid
        - errors
        - with_balance
      properties:
        rows:
          type: integer
        valid:
          type: integer
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ImportRowError'
        with_balance:
          type: integer
    ImportInvite:
      type: object
      required:
        - id
        - line
        - name
        - code
        - group_id
        - rate_bps
        - balance
        - link
        - state
        - claimed_at
      properties:
        id:
          type: string
        line:
          type:
            - integer
            - 'null'
        name:
          type: string
        code:
          type:
            - string
            - 'null'
        group_id:
          type:
            - string
            - 'null'
        rate_bps:
          type:
            - integer
            - 'null'
        balance:
          type:
            - object
            - 'null'
          required:
            - amount_minor
            - currency
          properties:
            amount_minor:
              type: integer
            currency:
              type: string
        link:
          type: string
          description: This person's personal link. Send it from your own email.
        state:
          type: string
          enum:
            - open
            - waiting
            - claimed
            - revoked
        claimed_at:
          type:
            - string
            - 'null'
          format: date-time
    ImportCommitted:
      type: object
      required:
        - import_id
        - invites
      properties:
        import_id:
          type: string
          format: uuid
        invites:
          type: array
          items:
            $ref: '#/components/schemas/ImportInvite'
    ImportInviteList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ImportInvite'
        next_cursor:
          type:
            - string
            - 'null'
    Import:
      type: object
      required:
        - id
        - program_id
        - invites
        - with_balance
        - claimed
        - created_at
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        invites:
          type: integer
        with_balance:
          type: integer
        claimed:
          type: integer
        created_at:
          type: string
          format: date-time
    ImportList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Import'
    LibraryItem:
      type: object
      required:
        - id
        - program_id
        - kind
        - title
        - body
        - url
        - host
        - image_url
        - ready
        - group_id
        - in_panel
        - position
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - image
            - snippet
            - link
        title:
          type: string
        body:
          type:
            - string
            - 'null'
          description: A snippet's text, with `{link}` and `{code}` as written.
        url:
          type:
            - string
            - 'null'
        host:
          type:
            - string
            - 'null'
        image_url:
          type:
            - string
            - 'null'
        ready:
          type: boolean
          description: False for an image item whose file is not in yet.
        group_id:
          type:
            - string
            - 'null'
        in_panel:
          type: boolean
        position:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    LibraryItemList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/LibraryItem'
    LibraryItemCreate:
      type: object
      required:
        - kind
        - title
      additionalProperties: false
      properties:
        kind:
          type: string
          enum:
            - image
            - snippet
            - link
        title:
          type: string
          maxLength: 120
        body:
          type: string
          maxLength: 2000
          description: A snippet's text.
        url:
          type: string
          maxLength: 2048
          description: A link's https address.
        group_id:
          type:
            - string
            - 'null'
          format: uuid
        in_panel:
          type: boolean
          description: 'A snippet only: shown to your own users in the in-app panel too.'
        position:
          type: integer
          minimum: 0
          maximum: 10000
    LibraryItemUpdate:
      type: object
      additionalProperties: false
      properties:
        title:
          type: string
          maxLength: 120
        body:
          type: string
          maxLength: 2000
        url:
          type: string
          maxLength: 2048
        group_id:
          type:
            - string
            - 'null'
          format: uuid
        in_panel:
          type: boolean
        position:
          type: integer
          minimum: 0
          maximum: 10000
        image_path:
          type: string
          maxLength: 200
          description: An image item's file, from its upload.
    LibraryItemDeleted:
      type: object
      required:
        - deleted
        - id
      properties:
        deleted:
          type: boolean
          enum:
            - true
        id:
          type: string
          format: uuid
    LinkHost:
      type: object
      required:
        - host
        - source
        - verified
        - record
        - checked_at
        - problem
      properties:
        host:
          type: string
        source:
          type: string
          enum:
            - product
            - allowed
          description: '`product`: the product URL''s own host, needing no proof.'
        verified:
          type: boolean
        record:
          type:
            - object
            - 'null'
          required:
            - type
            - name
            - value
          properties:
            type:
              type: string
              enum:
                - TXT
            name:
              type: string
            value:
              type: string
        checked_at:
          type:
            - string
            - 'null'
          format: date-time
        problem:
          type:
            - string
            - 'null'
    LinkHostList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/LinkHost'
    BoardMetric:
      type: string
      enum:
        - customers
        - sales
        - revenue
        - friends
      description: '`customers`: paying customers brought; `sales`: payments; `revenue`: their amount, in minor units; `friends`: on a waitlist, friends who joined and confirmed.'
    BoardPeriod:
      type: string
      enum:
        - month
        - 30d
        - quarter
        - all
      description: '`month` and `quarter` are calendar ones (UTC); `30d` the last 30 days; `all` since the program began.'
    BoardNameChoice:
      type: string
      enum:
        - hidden
        - initials
        - display
      description: How a promoter chose to appear to the program's other promoters. `hidden` is "A promoter", the default for a partner; one of your own users starts `initials`.
    LeaderboardSettings:
      type: object
      required:
        - metric
        - period
        - show
        - size
        - panel
      properties:
        metric:
          $ref: '#/components/schemas/BoardMetric'
        period:
          $ref: '#/components/schemas/BoardPeriod'
        show:
          type: string
          enum:
            - counts
            - amounts
          description: '`counts`: promoters see places and counts, never money; `amounts`: revenue too.'
        size:
          type: integer
          minimum: 10
          maximum: 100
          description: How many places promoters and the panel see.
        panel:
          type: boolean
          description: Whether the in-app panel shows a "Top referrers" card.
    LeaderboardFields:
      type:
        - object
        - 'null'
      additionalProperties: false
      description: The leaderboard promoters see, merged onto the current setting; null switches it off. You always see your own.
      properties:
        metric:
          $ref: '#/components/schemas/BoardMetric'
        period:
          $ref: '#/components/schemas/BoardPeriod'
        show:
          type: string
          enum:
            - counts
            - amounts
        size:
          type: integer
          minimum: 10
          maximum: 100
        panel:
          type: boolean
    LeaderboardRow:
      type: object
      required:
        - rank
        - value
        - partner_id
        - kind
        - display_name
        - external_id
        - appears_as
        - under_review
        - last_activity_at
      properties:
        rank:
          type: integer
          minimum: 1
        value:
          type: integer
          description: The count
          or revenue in minor units.: null
        partner_id:
          type: string
          format: uuid
          description: The membership id.
        kind:
          type: string
          enum:
            - partner
            - referrer
          description: '`referrer`: one of your own users.'
        display_name:
          type: string
        external_id:
          type:
            - string
            - 'null'
          description: Your own id
          for one of your users.: null
        appears_as:
          $ref: '#/components/schemas/BoardNameChoice'
        under_review:
          type: boolean
          description: An open fraud flag of theirs in the program.
        last_activity_at:
          type:
            - string
            - 'null'
          format: date-time
    Leaderboard:
      type: object
      required:
        - program_id
        - settings
        - metric
        - period
        - title
        - since
        - until
        - ranked
        - computed_at
        - rows
      properties:
        program_id:
          type: string
          format: uuid
        settings:
          description: The program's setting; null when promoters see no leaderboard.
          oneOf:
            - $ref: '#/components/schemas/LeaderboardSettings'
            - type: 'null'
        metric:
          $ref: '#/components/schemas/BoardMetric'
        period:
          $ref: '#/components/schemas/BoardPeriod'
        title:
          type: string
          description: '"paying customers, this month".'
        since:
          type:
            - string
            - 'null'
          format: date-time
        until:
          type:
            - string
            - 'null'
          format: date-time
        ranked:
          type: integer
          description: How many promoters have something counted.
        computed_at:
          type:
            - string
            - 'null'
          format: date-time
        rows:
          type: array
          items:
            $ref: '#/components/schemas/LeaderboardRow'
    ContestStatus:
      type: string
      enum:
        - scheduled
        - running
        - ended
        - awarded
        - cancelled
    ContestPrizeInput:
      type: object
      required:
        - rank
        - kind
      additionalProperties: false
      properties:
        rank:
          type: integer
          minimum: 1
          maximum: 100
        through_rank:
          type: integer
          minimum: 1
          maximum: 100
          description: The same prize for every place from `rank` to this one.
        kind:
          type: string
          enum:
            - cash
            - grant
        amount_minor:
          type: integer
          minimum: 1
          description: Cash, credit or a gift card, in the program's currency's minor units.
        reward_type:
          type: string
          enum:
            - credit
            - gift_card
            - free_period
            - perk
          description: A `grant`'s type.
        perk_text:
          type: string
          maxLength: 120
          description: 'A perk''s words: "A hoodie".'
        period_count:
          type: integer
          minimum: 1
          maximum: 365
          description: 'A free period''s length: up to 365 days or 36 months.'
        period_unit:
          type: string
          enum:
            - day
            - month
    ContestPrize:
      allOf:
        - $ref: '#/components/schemas/ContestPrizeInput'
        - type: object
          required:
            - place
            - text
          properties:
            place:
              type: string
              description: '"1st", "2nd to 5th".'
            text:
              type: string
              description: '"500.00 USD", "A hoodie", "1 month free".'
    Contest:
      type: object
      required:
        - id
        - program_id
        - name
        - description
        - starts_at
        - ends_at
        - metric
        - group_id
        - prizes
        - minimum
        - status
        - status_text
        - ended_at
        - awarded_at
        - cancelled_at
        - cancel_reason
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        program_id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        metric:
          $ref: '#/components/schemas/BoardMetric'
        group_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Only one group or level may enter; null for every promoter.
        prizes:
          type: array
          items:
            $ref: '#/components/schemas/ContestPrize'
        minimum:
          type: integer
          description: What a promoter must reach to win a prize.
        status:
          $ref: '#/components/schemas/ContestStatus'
        status_text:
          type: string
        ended_at:
          type:
            - string
            - 'null'
          format: date-time
        awarded_at:
          type:
            - string
            - 'null'
          format: date-time
        cancelled_at:
          type:
            - string
            - 'null'
          format: date-time
        cancel_reason:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ContestList:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Contest'
    ContestStanding:
      type: object
      required:
        - rank
        - place
        - qualified
        - value
        - partner_id
        - kind
        - display_name
        - external_id
        - appears_as
        - under_review
        - prize
      properties:
        rank:
          type: integer
        place:
          type:
            - integer
            - 'null'
          description: Place among those who reached the minimum (what a prize goes by).
        qualified:
          type: boolean
        value:
          type: integer
        partner_id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - partner
            - referrer
        display_name:
          type: string
        external_id:
          type:
            - string
            - 'null'
        appears_as:
          $ref: '#/components/schemas/BoardNameChoice'
        under_review:
          type: boolean
        prize:
          type:
            - string
            - 'null'
          description: The prize this place wins as things stand.
    ContestDetail:
      allOf:
        - $ref: '#/components/schemas/Contest'
        - type: object
          required:
            - standings
            - computed_at
            - award
          properties:
            standings:
              type: array
              items:
                $ref: '#/components/schemas/ContestStanding'
            computed_at:
              type:
                - string
                - 'null'
              format: date-time
            award:
              type: object
              required:
                - possible
                - reason
                - awardable_at
                - winners
                - under_review
              properties:
                possible:
                  type: boolean
                  description: Whether the prizes may be awarded now.
                reason:
                  type:
                    - string
                    - 'null'
                  description: Why not
                  when not.: null
                awardable_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
                  description: When the last hold on what the winners counted passes.
                winners:
                  type: integer
                under_review:
                  type: integer
    ContestFields:
      type: object
      properties:
        description:
          type:
            - string
            - 'null'
          maxLength: 1000
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
          description: At most 366 days after the start.
        metric:
          $ref: '#/components/schemas/BoardMetric'
        group_id:
          type:
            - string
            - 'null'
          format: uuid
        prizes:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: '#/components/schemas/ContestPrizeInput'
        minimum:
          type: integer
          minimum: 0
    ContestCreate:
      allOf:
        - $ref: '#/components/schemas/ContestFields'
        - type: object
          required:
            - name
            - starts_at
            - ends_at
            - prizes
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 80
      unevaluatedProperties: false
    ContestUpdate:
      allOf:
        - $ref: '#/components/schemas/ContestFields'
        - type: object
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 80
      unevaluatedProperties: false
    ContestAward:
      type: object
      required:
        - contest
        - awarded
      properties:
        contest:
          $ref: '#/components/schemas/Contest'
        awarded:
          type: array
          items:
            type: object
            required:
              - partner_id
              - place
              - commission_id
              - grant_id
            properties:
              partner_id:
                type: string
                format: uuid
              place:
                type: integer
              commission_id:
                type:
                  - string
                  - 'null'
                format: uuid
                description: A cash prize's bonus.
              grant_id:
                type:
                  - string
                  - 'null'
                format: uuid
                description: Any other prize's reward.
    ReasonBody:
      type: object
      required:
        - reason
      additionalProperties: false
      properties:
        reason:
          type: string
          minLength: 3
          maxLength: 500
    ReportLink:
      type: object
      required:
        - type
        - url
        - expires_at
        - filename
      properties:
        type:
          type: string
          enum:
            - commissions
            - conversions
            - partners
            - payouts
            - grants
            - contests
        url:
          type: string
          description: Open it once, within ten minutes, to download the CSV. Only its fingerprint is kept.
        expires_at:
          type: string
          format: date-time
        filename:
          type: string
          description: What the file is called when it downloads, like `acme-partners-commissions-2026-10-27.csv`.
    ReferralLink:
      type: object
      required:
        - id
        - code
        - url
        - kind
        - placement
        - archived_at
        - created_at
        - partner
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
        url:
          type: string
          description: '`https://boltclaw.ai/r/<code>`, or on your own link domain.'
        kind:
          type: string
          enum:
            - primary
            - placement
        placement:
          type:
            - string
            - 'null'
          enum:
            - bio
            - post
            - automation
            - manual
            - null
          description: Where BoltClaw placed it, by type only (never the partner's own note).
        archived_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        partner:
          type:
            - object
            - 'null'
          required:
            - id
            - display_name
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type: string
    ReferralLinkList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ReferralLink'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    InviteSummary:
      type: object
      description: An invite without its link (shown once, when the invite is made).
      required:
        - id
        - max_uses
        - used_count
        - expires_at
        - created_at
        - state
      properties:
        id:
          type: string
          format: uuid
        max_uses:
          type: integer
        used_count:
          type: integer
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        state:
          type: string
          enum:
            - active
            - expired
            - used
    InviteSummaryList:
      type: object
      required:
        - data
        - next_cursor
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/InviteSummary'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
    CodeCreate:
      type: object
      additionalProperties: false
      properties:
        code:
          type:
            - string
            - 'null'
          maxLength: 64
          description: 3 to 40 letters, digits and hyphens. Left out, the program's suggestion.
    CodeUpdate:
      type: object
      required:
        - stripe_promotion_code
      additionalProperties: false
      properties:
        stripe_promotion_code:
          type:
            - string
            - 'null'
          maxLength: 255
          description: '`promo_…` in your own Stripe, or null to forget it.'
    CommissionApproveRequest:
      type: object
      required:
        - reason
      additionalProperties: false
      properties:
        reason:
          type: string
          maxLength: 500
          description: Why it is paid before its hold ends. Kept in the activity log.
    PayoutCancel:
      type: object
      additionalProperties: false
      properties:
        status:
          type: string
          enum:
            - cancelled
            - failed
          description: '`cancelled` (the default), or `failed` when you tried to pay it and the money did not arrive.'
    LevelRecalculation:
      type: object
      required:
        - moved
      properties:
        moved:
          type: integer
          description: Partners moved to another level.
    WaitlistSettings:
      type: object
      required:
        - bump
        - page
        - pitch
        - closed
        - page_url
        - ended_at
      properties:
        bump:
          type: integer
          description: Places a confirmed friend moves their referrer up.
        page:
          type: boolean
          description: Whether the hosted waitlist page is on.
        pitch:
          type:
            - string
            - 'null'
          description: The hosted page's few lines about the product.
        closed:
          type: boolean
          description: No new entries; everybody keeps their place.
        page_url:
          type:
            - string
            - 'null'
          description: The hosted page's address. It is unguessable and noindex; POST /referrals/programs/{id}/waitlist/rotate-page makes a new one.
        ended_at:
          type:
            - string
            - 'null'
          format: date-time
    WaitlistFields:
      type:
        - object
        - 'null'
      additionalProperties: false
      description: A waitlist program's settings, merged onto the current ones.
      properties:
        bump:
          type: integer
          minimum: 1
          maximum: 1000
          description: Places a confirmed friend moves their referrer up (default 5).
        page:
          type: boolean
          description: Switch the hosted page at `/r/w/<token>` on (default false).
        pitch:
          type:
            - string
            - 'null'
          maxLength: 500
        closed:
          type: boolean
    WaitlistEntryStatus:
      type: string
      enum:
        - waiting
        - invited
        - joined
    WaitlistEntry:
      type: object
      required:
        - id
        - external_id
        - name
        - code
        - link_url
        - source
        - status
        - position
        - friends
        - verified
        - disqualified
        - disqualify_reason
        - flags
        - joined_at
        - invited_at
        - referred_by
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type: string
          description: Your own id for the person; the hosted page makes one, `wl_…`.
        name:
          type:
            - string
            - 'null'
        code:
          type:
            - string
            - 'null'
        link_url:
          type:
            - string
            - 'null'
          description: Their link to invite friends.
        source:
          type: string
          enum:
            - api
            - hosted
        status:
          $ref: '#/components/schemas/WaitlistEntryStatus'
        position:
          type:
            - integer
            - 'null'
          description: Their place in line; null until they confirm.
        friends:
          type: integer
          description: Confirmed friends who count.
        verified:
          type: boolean
        disqualified:
          type: boolean
        disqualify_reason:
          type:
            - string
            - 'null'
        flags:
          type: array
          items:
            type: string
            enum:
              - velocity
              - self
          description: '`velocity`: one of a flood from one address; `self`: the referrer themselves. Either moves nobody.'
        joined_at:
          type: string
          format: date-time
        invited_at:
          type:
            - string
            - 'null'
          format: date-time
        referred_by:
          type:
            - object
            - 'null'
          properties:
            external_id:
              type:
                - string
                - 'null'
            name:
              type:
                - string
                - 'null'
    WaitlistState:
      type: object
      required:
        - verified
        - status
        - disqualified
        - position
        - position_text
        - total
        - friends
        - pending_friends
        - bump
        - move_up_text
        - code
        - link_url
        - milestones
      properties:
        verified:
          type: boolean
        status:
          $ref: '#/components/schemas/WaitlistEntryStatus'
        disqualified:
          type: boolean
        position:
          type:
            - integer
            - 'null'
        position_text:
          type:
            - string
            - 'null'
          description: '"You are number 1,204".'
        total:
          type: integer
        friends:
          type: integer
        pending_friends:
          type: integer
          description: Friends who joined and have not confirmed yet.
        bump:
          type: integer
        move_up_text:
          type: string
        code:
          type:
            - string
            - 'null'
        link_url:
          type:
            - string
            - 'null'
        milestones:
          type: array
          items:
            type: object
            required:
              - target
              - current
              - reached
              - reward
              - text
            properties:
              target:
                type: integer
              current:
                type: integer
              reached:
                type: boolean
              reward:
                type: string
              text:
                type: string
                description: '"2 of 3 friends joined: one more for early access".'
    WaitlistEntryDetail:
      allOf:
        - $ref: '#/components/schemas/WaitlistEntry'
        - type: object
          required:
            - state
          properties:
            state:
              oneOf:
                - $ref: '#/components/schemas/WaitlistState'
                - type: 'null'
    WaitlistEntryList:
      type: object
      required:
        - data
        - next_cursor
        - total
        - line
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WaitlistEntry'
        next_cursor:
          $ref: '#/components/schemas/NextCursor'
        total:
          type: integer
          description: Entries matching the filter.
        line:
          type: integer
          description: Confirmed entries
          the line's length.: null
    WaitlistEntryUpsert:
      type: object
      required:
        - external_id
      additionalProperties: false
      properties:
        external_id:
          type: string
          minLength: 1
          maxLength: 200
        name:
          type:
            - string
            - 'null'
          maxLength: 200
        email:
          type: string
          maxLength: 320
          description: Kept only as a keyed fingerprint for the self-referral check.
        code:
          type: string
          maxLength: 40
          description: A vanity code for their own link, on creation only.
        referral_code:
          type: string
          maxLength: 40
          description: The code of the link they arrived with.
        verified:
          type: boolean
        status:
          $ref: '#/components/schemas/WaitlistEntryStatus'
    WaitlistEntryUpdate:
      type: object
      additionalProperties: false
      properties:
        verified:
          type: boolean
        status:
          $ref: '#/components/schemas/WaitlistEntryStatus'
    WaitlistInvite:
      type: object
      required:
        - entry_ids
        - subject
        - message
      additionalProperties: false
      properties:
        entry_ids:
          type: array
          minItems: 1
          maxItems: 500
          items:
            type: string
            format: uuid
        subject:
          type: string
          minLength: 1
          maxLength: 140
          description: One line.
        message:
          type: string
          maxLength: 6000
          description: 'The broadcasts'' Markdown subset: paragraphs, **bold**, *italic*, lists and https links.'
    WaitlistInvited:
      type: object
      required:
        - invited
        - emailed
        - entry_ids
      properties:
        invited:
          type: integer
        emailed:
          type: integer
          description: Entries from the hosted page
          emailed by BoltClaw.: null
        entry_ids:
          type: array
          items:
            type: string
            format: uuid
    WaitlistEnded:
      type: object
      required:
        - forgotten
        - ended_at
      properties:
        forgotten:
          type: integer
          description: How many entries' addresses and links were deleted by this call.
        ended_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the waitlist ended (the first end
          if it had ended before).: null
    WaitlistPageAddress:
      type: object
      required:
        - page_url
      properties:
        page_url:
          type: string
          description: The hosted page's new address. It is unguessable and noindex.
    SecondTier:
      type: object
      required:
        - percent_bps
        - months
        - scope
      properties:
        percent_bps:
          type: integer
          minimum: 1
          maximum: 2000
          description: The recruiter's share of a recruit's own commission, in basis points (2000 is 20%).
        months:
          type: integer
          minimum: 1
          maximum: 24
          description: How many months after the recruit joined.
        scope:
          type: string
          enum:
            - cash
          description: Cash commissions on sales only.
    SecondTierSettings:
      type: object
      required:
        - program_id
        - second_tier
        - rule
        - terms_version
        - terms_required_version
        - clause
        - fence
      properties:
        program_id:
          type: string
          format: uuid
        second_tier:
          description: Null while it is off.
          oneOf:
            - $ref: '#/components/schemas/SecondTier'
            - type: 'null'
        rule:
          type:
            - string
            - 'null'
          description: The rule in one sentence
          as partners read it.: null
        terms_version:
          type: integer
        terms_required_version:
          type:
            - integer
            - 'null'
          description: The terms version partners must accept before their next payout.
        clause:
          type: string
          description: The paragraph the terms carry while it is on.
        fence:
          type: string
          description: Why it is limited the way it is.
    SecondTierUpdate:
      type: object
      required:
        - enabled
      additionalProperties: false
      properties:
        enabled:
          type: boolean
        percent_bps:
          type: integer
          minimum: 1
          maximum: 2000
        months:
          type:
            - integer
            - 'null'
          minimum: 1
          maximum: 24
    StatsBucket:
      type: object
      required:
        - start
        - clicks
        - untracked_clicks
        - leads
        - sales
        - revenue_minor
        - commission_minor
        - conversion_rate
        - epc_minor
      properties:
        start:
          type: string
          format: date
          description: The bucket's first day (UTC; weeks start on Monday).
        clicks:
          type: integer
        untracked_clicks:
          type:
            - integer
            - 'null'
        leads:
          type: integer
        sales:
          type: integer
        revenue_minor:
          type: integer
        commission_minor:
          type: integer
        conversion_rate:
          type:
            - number
            - 'null'
        epc_minor:
          type:
            - number
            - 'null'
    ProgramStats:
      type: object
      required:
        - currency
        - from
        - to
        - bucket
        - totals
        - untracked_clicks
        - untracked_by_month
        - series
        - partners
      properties:
        currency:
          type: string
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        bucket:
          type:
            - string
            - 'null'
          enum:
            - day
            - week
            - month
            - null
        totals:
          allOf:
            - $ref: '#/components/schemas/PartnerStats'
            - type: object
              required:
                - refunded_minor
                - other_currency_sales
              properties:
                refunded_minor:
                  type: integer
                other_currency_sales:
                  type: integer
        untracked_clicks:
          type: integer
        untracked_by_month:
          type: array
          items:
            type: object
            required:
              - month
              - clicks
            properties:
              month:
                type: string
              clicks:
                type: integer
        series:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/StatsBucket'
        partners:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/PartnerStats'
              - type: object
                required:
                  - id
                  - display_name
                properties:
                  id:
                    type: string
                    format: uuid
                  display_name:
                    type: string
  responses:
    BadRequest:
      description: |
        `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in
        `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: '`UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    PlanLimit:
      description: '`PLAN_LIMIT`: the organisation''s plan does not allow more of this. The message names the allowance.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: |
        `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`:
        the key's organisation is suspended. Never about another workspace's object, which is 404.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    ApprovalRequired:
      description: |
        `APPROVAL_REQUIRED` on the first try, `APPROVAL_PENDING` on a retry before a person has
        decided. NOTHING HAS HAPPENED either way. The body carries `approval` with a `url`: show
        it to the person, and once they approve, send the identical request again with the
        approval id in `BoltClaw-Approval`. Only an AI assistant's request is ever answered this
        way.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: |
        `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately
        the same answer and never 403. If the id is right, check you are using the right key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Conflict:
      description: |
        `CONFLICT`: the request clashes with the object's current state (the message says how).
        `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT`: the `Idempotency-Key` was used
        with another request, or its first request is still running.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    PayloadTooLarge:
      description: '`PAYLOAD_TOO_LARGE`: the body is over 64 KB.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unprocessable:
      description: |
        `VALIDATION_ERROR`: the request is well formed but cannot be done as asked, such as a
        network that does not support it. Or `UPSTREAM_ERROR`: the network refused it.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RateLimited:
      description: '`RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds.'
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    InternalError:
      description: '`INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Upstream:
      description: '`UPSTREAM_ERROR`: the social network failed the call. The message says what happened.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    UpstreamUnavailable:
      description: '`UPSTREAM_ERROR`: the social network is temporarily unavailable; retrying later may work.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
