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

# Sync prompts

> Declaratively sync an own brand's active prompt set.

Makes the brand's ACTIVE prompts equal `prompts` (at most 500 rows). Matching is on the text, case- and surrounding-whitespace-insensitive, within the brand (coarser than the lower(text) uniqueness index, so several ACTIVE rows can share a key: the oldest is the match and the rest count as rows the request did not name). Per desired row: an active match with identical countries (and platforms / tags where sent) is `unchanged`; an active match that differs is `updated`; a text that exists only INACTIVE **reactivates the newest inactive row** (`reactivated`, fields updated) — never a duplicate insert; anything else is `created` (`source: "api"`). With `deactivate_missing` (default true) every active prompt not in the request is `deactivated`. Each desired row is asserted ACTIVE in the same statement that writes it (a row planned as `unchanged` is asserted conditionally, so a clean sync still writes nothing), which is what makes a downgrade landing mid-run refuse the row instead of editing a prompt it has already switched off: expect `failed` / `brand_archived` for those rows, and `reactivated` where a row planned as `unchanged` had been switched off on a still-live brand. NOT ATOMIC: rows are applied one by one, deactivations FIRST (so the slots they free are available to the creates and reactivations that follow), then the desired rows in request order; a row refused by a cap (422 codes), by the archived-brand rule (`brand_archived`) or by the active-text uniqueness rule is reported as `failed` with its code and the rest of the request still runs. The LAST statement of a `deactivate_missing` run re-reads the brand's active set under a brand-grain lock every writer of a live prompt takes: it switches off anything outside the requested set, and RE-ASSERTS the DESIRED STATE of the rows this call applied — their countries / platforms / tags (each restored row reported `reasserted`) and the two claims underneath those, that the row still exists and is still ACTIVE. The sync's per-row writes and that reconciliation are separate transactions, so a concurrent `PATCH`, `DELETE` or deactivation landing between them would otherwise leave the 200 describing a state that had been overwritten. A prompt switched off in that window is put back on with its fields, reported `reinstated`; a prompt DELETED in it is reported `failed` / `not_found` and is not re-created (its text was resolved to an id that no longer names anything — re-run the sync and it is created properly). Re-activation is cap-checked like a create, so one the database refuses is one more `failed` row and the rest of the reconciliation still lands. A refused row is never re-asserted (nothing was applied to it), and a row whose text another writer changed after it was written is reported `failed` / `prompt_changed` and deactivated rather than rewritten. So when this endpoint answers 200, every prompt it says it applied is present, active and carrying the requested fields, or named in `results` as `failed`. The response is 200 with a per-row `results` list (request order, then the deactivated, reasserted and reinstated rows), a `summary` and the organisation's `usage` afterwards. `dry_run` returns the same shape with the planned actions and writes nothing. Duplicate texts within the request are 400 `duplicate_prompt_text`. Own brands only (400 `validation_error` with `details[].code = "own_brand_only"`); an archived brand is 409 `brand_archived`, an empty sync included.



## OpenAPI

````yaml /api-reference/openapi.json put /orgs/{orgId}/brands/{brandId}/prompts
openapi: 3.1.0
info:
  title: Surfais API
  version: 1.0.0
  description: >-
    The Surfais API gives programmatic access to the AI-visibility data Surfais
    measures for your brands: organisations, brands and competitors, tracked
    prompts, per-run results and mentions, scores, share of voice and cited
    sources. It can also provision brands, manage prompts and competitors,
    request on-demand scans, and deliver signed webhooks when a scan completes
    or a score or sentiment threshold is crossed.


    Base URL: `https://api.surfais.com/v1`. Send your API key as a Bearer token
    (`Authorization: Bearer sfs_live_…`). Test keys (`sfs_test_…`) are rejected
    by the production API. Requests and responses are JSON: a success is `{
    "data": … }`, an error is `{ "error": { "code", "message" }, "request_id"
    }`.


    Guides for authentication, pagination, errors, rate limits, idempotency and
    webhooks are at https://docs.surfais.com/api/introduction.
  contact:
    name: Surfais support
    email: support@surfais.com
    url: https://docs.surfais.com/api/support
servers:
  - url: https://api.surfais.com/v1
    description: >-
      Production (the `/v1` prefix is part of the server URL; paths below are
      relative to it).
security:
  - bearerKey: []
tags:
  - name: orgs
    description: Organisations reachable by the key, their usage, and the partner link.
  - name: brands
    description: Own brands (properties) and competitor rows.
  - name: prompts
    description: 'Tracked prompts: reads, CRUD and the declarative sync.'
  - name: results
    description: Finalised run-level data.
  - name: scores
    description: Persisted scores, per-market history and share of voice.
  - name: sources
    description: Cited domains.
  - name: scans
    description: On-demand scan requests.
  - name: webhooks
    description: >-
      Outbound event delivery: endpoints, subscriptions, deliveries (partner
      keys).
externalDocs:
  description: Guides, concepts and webhooks
  url: https://docs.surfais.com/api/introduction
paths:
  /orgs/{orgId}/brands/{brandId}/prompts:
    put:
      tags:
        - prompts
      summary: Sync prompts
      description: >-
        Declaratively sync an own brand's active prompt set.


        Makes the brand's ACTIVE prompts equal `prompts` (at most 500 rows).
        Matching is on the text, case- and surrounding-whitespace-insensitive,
        within the brand (coarser than the lower(text) uniqueness index, so
        several ACTIVE rows can share a key: the oldest is the match and the
        rest count as rows the request did not name). Per desired row: an active
        match with identical countries (and platforms / tags where sent) is
        `unchanged`; an active match that differs is `updated`; a text that
        exists only INACTIVE **reactivates the newest inactive row**
        (`reactivated`, fields updated) — never a duplicate insert; anything
        else is `created` (`source: "api"`). With `deactivate_missing` (default
        true) every active prompt not in the request is `deactivated`. Each
        desired row is asserted ACTIVE in the same statement that writes it (a
        row planned as `unchanged` is asserted conditionally, so a clean sync
        still writes nothing), which is what makes a downgrade landing mid-run
        refuse the row instead of editing a prompt it has already switched off:
        expect `failed` / `brand_archived` for those rows, and `reactivated`
        where a row planned as `unchanged` had been switched off on a still-live
        brand. NOT ATOMIC: rows are applied one by one, deactivations FIRST (so
        the slots they free are available to the creates and reactivations that
        follow), then the desired rows in request order; a row refused by a cap
        (422 codes), by the archived-brand rule (`brand_archived`) or by the
        active-text uniqueness rule is reported as `failed` with its code and
        the rest of the request still runs. The LAST statement of a
        `deactivate_missing` run re-reads the brand's active set under a
        brand-grain lock every writer of a live prompt takes: it switches off
        anything outside the requested set, and RE-ASSERTS the DESIRED STATE of
        the rows this call applied — their countries / platforms / tags (each
        restored row reported `reasserted`) and the two claims underneath those,
        that the row still exists and is still ACTIVE. The sync's per-row writes
        and that reconciliation are separate transactions, so a concurrent
        `PATCH`, `DELETE` or deactivation landing between them would otherwise
        leave the 200 describing a state that had been overwritten. A prompt
        switched off in that window is put back on with its fields, reported
        `reinstated`; a prompt DELETED in it is reported `failed` / `not_found`
        and is not re-created (its text was resolved to an id that no longer
        names anything — re-run the sync and it is created properly).
        Re-activation is cap-checked like a create, so one the database refuses
        is one more `failed` row and the rest of the reconciliation still lands.
        A refused row is never re-asserted (nothing was applied to it), and a
        row whose text another writer changed after it was written is reported
        `failed` / `prompt_changed` and deactivated rather than rewritten. So
        when this endpoint answers 200, every prompt it says it applied is
        present, active and carrying the requested fields, or named in `results`
        as `failed`. The response is 200 with a per-row `results` list (request
        order, then the deactivated, reasserted and reinstated rows), a
        `summary` and the organisation's `usage` afterwards. `dry_run` returns
        the same shape with the planned actions and writes nothing. Duplicate
        texts within the request are 400 `duplicate_prompt_text`. Own brands
        only (400 `validation_error` with `details[].code = "own_brand_only"`);
        an archived brand is 409 `brand_archived`, an empty sync included.
      operationId: syncBrandPrompts
      parameters:
        - in: path
          name: orgId
          schema:
            type: string
            format: uuid
            pattern: >-
              ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            description: >-
              Organisation id. Org keys: the key's own org. Partner keys: any
              org with an active link. Anything else is 404.
          required: true
          description: >-
            Organisation id. Org keys: the key's own org. Partner keys: any org
            with an active link. Anything else is 404.
        - in: path
          name: brandId
          schema:
            type: string
            format: uuid
            pattern: >-
              ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            description: >-
              Brand id within the org (own brand or competitor row); 404 when
              not in the org. Writes that add tracking need an own (400
              `validation_error` with `details[].code = "own_brand_only"`),
              non-archived (409 `brand_archived`) brand.
          required: true
          description: >-
            Brand id within the org (own brand or competitor row); 404 when not
            in the org. Writes that add tracking need an own (400
            `validation_error` with `details[].code = "own_brand_only"`),
            non-archived (409 `brand_archived`) brand.
        - in: header
          name: Idempotency-Key
          schema:
            description: >-
              Optional. 1–255 characters, unique per intended write. Same key +
              same request → the stored response is replayed with
              `Idempotent-Replayed: true`; same key + different request → 409
              `idempotency_key_reuse`; still running → 409
              `idempotency_key_in_flight`. Outside that range → 400
              `validation_error` (`invalid_header`).
            type: string
            minLength: 1
            maxLength: 255
          description: >-
            Optional. 1–255 characters, unique per intended write. Same key +
            same request → the stored response is replayed with
            `Idempotent-Replayed: true`; same key + different request → 409
            `idempotency_key_reuse`; still running → 409
            `idempotency_key_in_flight`. Outside that range → 400
            `validation_error` (`invalid_header`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PromptSync'
      responses:
        '200':
          description: >-
            Multi-status result: what was created / updated / reactivated /
            unchanged / reasserted / reinstated / deactivated / failed.
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            X-RateLimit-Limit:
              required: true
              description: >-
                Requests allowed per minute for this key. On a 429
                `rate_limited` from the failed-authentication gate: failed
                attempts allowed per minute for the client address.
              schema:
                type: string
                description: >-
                  Requests allowed per minute for this key. On a 429
                  `rate_limited` from the failed-authentication gate: failed
                  attempts allowed per minute for the client address.
            X-RateLimit-Remaining:
              required: true
              description: >-
                Requests left in the current minute window (0 on the
                failed-authentication 429).
              schema:
                type: string
                description: >-
                  Requests left in the current minute window (0 on the
                  failed-authentication 429).
            X-RateLimit-Reset:
              required: true
              description: Unix seconds at which the current minute window ends.
              schema:
                type: string
                description: Unix seconds at which the current minute window ends.
            Idempotent-Replayed:
              description: >-
                Present, as `true`, when this response was replayed from the
                Idempotency-Key store rather than executed.
              schema:
                description: >-
                  Present, as `true`, when this response was replayed from the
                  Idempotency-Key store rather than executed.
                type: string
                const: 'true'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PromptSyncResult'
                required:
                  - data
        '400':
          description: >-
            `validation_error` (with `details`), `invalid_json`, or the
            operation's own 400 (`text_immutable`, `duplicate_prompt_text`). An
            `Idempotency-Key` outside its length bounds is `validation_error`
            with `details[].path = "Idempotency-Key"`.
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            X-RateLimit-Limit:
              required: true
              description: >-
                Requests allowed per minute for this key. On a 429
                `rate_limited` from the failed-authentication gate: failed
                attempts allowed per minute for the client address.
              schema:
                type: string
                description: >-
                  Requests allowed per minute for this key. On a 429
                  `rate_limited` from the failed-authentication gate: failed
                  attempts allowed per minute for the client address.
            X-RateLimit-Remaining:
              required: true
              description: >-
                Requests left in the current minute window (0 on the
                failed-authentication 429).
              schema:
                type: string
                description: >-
                  Requests left in the current minute window (0 on the
                  failed-authentication 429).
            X-RateLimit-Reset:
              required: true
              description: Unix seconds at which the current minute window ends.
              schema:
                type: string
                description: Unix seconds at which the current minute window ends.
            Idempotent-Replayed:
              description: >-
                Present, as `true`, when this response was replayed from the
                Idempotency-Key store rather than executed.
              schema:
                description: >-
                  Present, as `true`, when this response was replayed from the
                  Idempotency-Key store rather than executed.
                type: string
                const: 'true'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: '`invalid_api_key` — uniform for every authentication failure.'
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: >-
            `write_scope_required` (the key has no `write` scope),
            `link_scope_insufficient` (partner key on a `read_only` link),
            `partner_only` (`PATCH …/link` with an org key), or
            `tier_not_entitled`. Every one of them is raised after the rate
            limiter has charged the key's minute allowance, so it carries the
            `X-RateLimit-*` trio (an exhausted allowance answers 429 first).
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            X-RateLimit-Limit:
              required: true
              description: >-
                Requests allowed per minute for this key. On a 429
                `rate_limited` from the failed-authentication gate: failed
                attempts allowed per minute for the client address.
              schema:
                type: string
                description: >-
                  Requests allowed per minute for this key. On a 429
                  `rate_limited` from the failed-authentication gate: failed
                  attempts allowed per minute for the client address.
            X-RateLimit-Remaining:
              required: true
              description: >-
                Requests left in the current minute window (0 on the
                failed-authentication 429).
              schema:
                type: string
                description: >-
                  Requests left in the current minute window (0 on the
                  failed-authentication 429).
            X-RateLimit-Reset:
              required: true
              description: Unix seconds at which the current minute window ends.
              schema:
                type: string
                description: Unix seconds at which the current minute window ends.
            Idempotent-Replayed:
              description: >-
                Present, as `true`, when this response was replayed from the
                Idempotency-Key store rather than executed.
              schema:
                description: >-
                  Present, as `true`, when this response was replayed from the
                  Idempotency-Key store rather than executed.
                type: string
                const: 'true'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: >-
            `not_found` — the org is not reachable by this key or a nested id is
            not in it. Raised after authentication and the minute charge, so it
            carries the `X-RateLimit-*` trio and is metered.
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            X-RateLimit-Limit:
              required: true
              description: >-
                Requests allowed per minute for this key. On a 429
                `rate_limited` from the failed-authentication gate: failed
                attempts allowed per minute for the client address.
              schema:
                type: string
                description: >-
                  Requests allowed per minute for this key. On a 429
                  `rate_limited` from the failed-authentication gate: failed
                  attempts allowed per minute for the client address.
            X-RateLimit-Remaining:
              required: true
              description: >-
                Requests left in the current minute window (0 on the
                failed-authentication 429).
              schema:
                type: string
                description: >-
                  Requests left in the current minute window (0 on the
                  failed-authentication 429).
            X-RateLimit-Reset:
              required: true
              description: Unix seconds at which the current minute window ends.
              schema:
                type: string
                description: Unix seconds at which the current minute window ends.
            Idempotent-Replayed:
              description: >-
                Present, as `true`, when this response was replayed from the
                Idempotency-Key store rather than executed.
              schema:
                description: >-
                  Present, as `true`, when this response was replayed from the
                  Idempotency-Key store rather than executed.
                type: string
                const: 'true'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: >-
            `brand_archived` (`BrandStateConflict`); or `idempotency_key_reuse`
            / `idempotency_key_in_flight`, with no `details`. Per-row uniqueness
            refusals are `failed` rows in the 200, never a 409.
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            X-RateLimit-Limit:
              required: true
              description: >-
                Requests allowed per minute for this key. On a 429
                `rate_limited` from the failed-authentication gate: failed
                attempts allowed per minute for the client address.
              schema:
                type: string
                description: >-
                  Requests allowed per minute for this key. On a 429
                  `rate_limited` from the failed-authentication gate: failed
                  attempts allowed per minute for the client address.
            X-RateLimit-Remaining:
              required: true
              description: >-
                Requests left in the current minute window (0 on the
                failed-authentication 429).
              schema:
                type: string
                description: >-
                  Requests left in the current minute window (0 on the
                  failed-authentication 429).
            X-RateLimit-Reset:
              required: true
              description: Unix seconds at which the current minute window ends.
              schema:
                type: string
                description: Unix seconds at which the current minute window ends.
            Idempotent-Replayed:
              description: >-
                Present, as `true`, when this response was replayed from the
                Idempotency-Key store rather than executed.
              schema:
                description: >-
                  Present, as `true`, when this response was replayed from the
                  Idempotency-Key store rather than executed.
                type: string
                const: 'true'
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        description: >-
                          Stable machine code — see the error list in the
                          description.
                      message:
                        type: string
                      details:
                        $ref: '#/components/schemas/BrandStateConflict'
                    required:
                      - code
                      - message
                  request_id:
                    type: string
                    description: Echoed in the `X-Request-Id` header.
                required:
                  - error
                  - request_id
        '413':
          description: '`payload_too_large` — the body exceeds 1 MB.'
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            X-RateLimit-Limit:
              required: true
              description: >-
                Requests allowed per minute for this key. On a 429
                `rate_limited` from the failed-authentication gate: failed
                attempts allowed per minute for the client address.
              schema:
                type: string
                description: >-
                  Requests allowed per minute for this key. On a 429
                  `rate_limited` from the failed-authentication gate: failed
                  attempts allowed per minute for the client address.
            X-RateLimit-Remaining:
              required: true
              description: >-
                Requests left in the current minute window (0 on the
                failed-authentication 429).
              schema:
                type: string
                description: >-
                  Requests left in the current minute window (0 on the
                  failed-authentication 429).
            X-RateLimit-Reset:
              required: true
              description: Unix seconds at which the current minute window ends.
              schema:
                type: string
                description: Unix seconds at which the current minute window ends.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: >-
            `rate_limited` (per-minute limit, or too many failed authentications
            from this address) or `quota_exceeded`.
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            Retry-After:
              required: true
              description: Seconds to wait before retrying — present on every 429 and 503.
              schema:
                type: string
                description: >-
                  Seconds to wait before retrying — present on every 429 and
                  503.
            X-RateLimit-Limit:
              required: true
              description: >-
                Requests allowed per minute for this key. On a 429
                `rate_limited` from the failed-authentication gate: failed
                attempts allowed per minute for the client address.
              schema:
                type: string
                description: >-
                  Requests allowed per minute for this key. On a 429
                  `rate_limited` from the failed-authentication gate: failed
                  attempts allowed per minute for the client address.
            X-RateLimit-Remaining:
              required: true
              description: >-
                Requests left in the current minute window (0 on the
                failed-authentication 429).
              schema:
                type: string
                description: >-
                  Requests left in the current minute window (0 on the
                  failed-authentication 429).
            X-RateLimit-Reset:
              required: true
              description: Unix seconds at which the current minute window ends.
              schema:
                type: string
                description: Unix seconds at which the current minute window ends.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: >-
            `internal_error` — quote `request_id`. A failure inside a route
            answers after the minute charge and carries the `X-RateLimit-*`
            trio; a failure before the limiter carries only `X-Request-Id`.
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            X-RateLimit-Limit:
              description: >-
                Present when the failure occurred after the minute charge
                (inside a route); absent on a failure before the limiter.
                Requests allowed per minute for this key.
              schema:
                description: >-
                  Present when the failure occurred after the minute charge
                  (inside a route); absent on a failure before the limiter.
                  Requests allowed per minute for this key.
                type: string
            X-RateLimit-Remaining:
              description: >-
                Present when the failure occurred after the minute charge.
                Requests left in the current minute window.
              schema:
                description: >-
                  Present when the failure occurred after the minute charge.
                  Requests left in the current minute window.
                type: string
            X-RateLimit-Reset:
              description: >-
                Present when the failure occurred after the minute charge. Unix
                seconds at which the current minute window ends.
              schema:
                description: >-
                  Present when the failure occurred after the minute charge.
                  Unix seconds at which the current minute window ends.
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: >-
            `api_unavailable` (switched off; `Retry-After` is 60),
            `rate_limit_unavailable` (fail-closed limiter — its store, or a
            saturated failed-authentication gate; before the minute charge, so
            no `X-RateLimit-*` trio), or `store_unavailable` (a tenant-store
            call behind an authenticated request exceeded 15000 ms — raised
            after the minute charge, so it is counted and carries the trio).
          headers:
            X-Request-Id:
              required: true
              description: >-
                Correlation id for this request; equals `request_id` in an error
                envelope.
              schema:
                type: string
                description: >-
                  Correlation id for this request; equals `request_id` in an
                  error envelope.
            Retry-After:
              required: true
              description: Seconds to wait before retrying — present on every 429 and 503.
              schema:
                type: string
                description: >-
                  Seconds to wait before retrying — present on every 429 and
                  503.
            X-RateLimit-Limit:
              description: >-
                Present when the failure occurred after the minute charge
                (inside a route); absent on a failure before the limiter.
                Requests allowed per minute for this key.
              schema:
                description: >-
                  Present when the failure occurred after the minute charge
                  (inside a route); absent on a failure before the limiter.
                  Requests allowed per minute for this key.
                type: string
            X-RateLimit-Remaining:
              description: >-
                Present when the failure occurred after the minute charge.
                Requests left in the current minute window.
              schema:
                description: >-
                  Present when the failure occurred after the minute charge.
                  Requests left in the current minute window.
                type: string
            X-RateLimit-Reset:
              description: >-
                Present when the failure occurred after the minute charge. Unix
                seconds at which the current minute window ends.
              schema:
                description: >-
                  Present when the failure occurred after the minute charge.
                  Unix seconds at which the current minute window ends.
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
components:
  schemas:
    PromptSync:
      type: object
      properties:
        prompts:
          maxItems: 500
          type: array
          items:
            $ref: '#/components/schemas/SyncPromptInput'
          description: >-
            The desired ACTIVE prompt set for the brand, at most 500 rows. Two
            rows with the same text (case- and whitespace-insensitive) are 400
            `duplicate_prompt_text`. An empty list with `deactivate_missing:
            true` deactivates every active prompt of the brand.
        deactivate_missing:
          default: true
          description: >-
            Deactivate active prompts of the brand that are not in `prompts`.
            Default true.
          type: boolean
        dry_run:
          default: false
          description: >-
            Return the plan (`results[].action` per row) without writing
            anything.
          type: boolean
      required:
        - prompts
    PromptSyncResult:
      type: object
      properties:
        summary:
          $ref: '#/components/schemas/PromptSyncSummary'
        results:
          type: array
          items:
            $ref: '#/components/schemas/PromptSyncResultRow'
          description: >-
            One row per input prompt, in request order, then one `deactivated`
            row per prompt switched off.
        usage:
          $ref: '#/components/schemas/PromptSyncUsage'
      required:
        - summary
        - results
        - usage
    ErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Stable machine code — see the error list in the description.
            message:
              type: string
            details: {}
          required:
            - code
            - message
        request_id:
          type: string
          description: Echoed in the `X-Request-Id` header.
      required:
        - error
        - request_id
    BrandStateConflict:
      type: object
      properties:
        brand_id:
          type: string
          format: uuid
          pattern: >-
            ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The own brand whose state refused the write.
      required:
        - brand_id
    SyncPromptInput:
      type: object
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 500
          description: >-
            The prompt as an end user would type it. At most 500 characters;
            leading/trailing whitespace is trimmed.
        countries:
          description: Markets the prompt runs in. Duplicates are collapsed.
          minItems: 1
          maxItems: 29
          type: array
          items:
            description: >-
              ISO 3166-1 alpha-2 market (case-insensitive). Must be one Surfais
              scans; otherwise 400 `validation_error` with `details[].code =
              "unsupported_country"`.
            type: string
            pattern: ^[A-Za-z]{2}$
        platforms:
          description: >-
            Omit on a NEW prompt for the default; omit on an existing one to
            leave its platforms as they are.
          minItems: 1
          maxItems: 5
          type: array
          items:
            type: string
            enum:
              - perplexity
              - chatgpt
              - gemini
              - claude
              - ai_overviews
            description: Scanned AI platform id.
        tags:
          description: >-
            Omit on a NEW prompt for none; omit on an existing one to leave its
            tags as they are.
          maxItems: 50
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 100
      required:
        - text
        - countries
    PromptSyncSummary:
      type: object
      properties:
        created:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        updated:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        reactivated:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        unchanged:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        reasserted:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: >-
            Rows whose requested fields another writer changed after this call
            had applied them, and the final reconciliation put back. Counted
            separately from the action the row was applied as, so `created +
            updated + reactivated + unchanged` still totals the request.
        reinstated:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: >-
            Rows another writer DEACTIVATED after this call had applied them,
            and the final reconciliation switched back on (restoring the
            requested fields in the same statement). Counted separately, for the
            same reason as `reasserted`. A prompt the same window DELETED cannot
            be reinstated: it is reported `failed` / `not_found` instead, and
            re-running the sync re-creates it.
        deactivated:
          type: integer
          minimum: 0
          maximum: 9007199254740991
        failed:
          type: integer
          minimum: 0
          maximum: 9007199254740991
      required:
        - created
        - updated
        - reactivated
        - unchanged
        - reasserted
        - reinstated
        - deactivated
        - failed
    PromptSyncResultRow:
      type: object
      properties:
        text:
          type: string
          description: >-
            The text as sent (for `deactivated`, `reasserted` and `reinstated`
            rows: as stored).
        action:
          type: string
          enum:
            - created
            - updated
            - reactivated
            - unchanged
            - reasserted
            - reinstated
            - deactivated
            - failed
          description: >-
            One per input prompt, plus a `deactivated` row per prompt switched
            off, and — from the final reconciliation — a `reasserted` row per
            prompt whose fields a concurrent writer had moved and a `reinstated`
            row per prompt a concurrent writer had switched off or
            deleted-then-restored. A repaired prompt also carries its own
            earlier row for the action it was applied as.
        prompt_id:
          description: >-
            Present for every row that maps to an existing prompt, and for
            created rows after a real run (absent on `dry_run` creates).
          type: string
          format: uuid
          pattern: >-
            ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        error:
          $ref: '#/components/schemas/WriteError'
          description: >-
            Present on `failed` rows: what refused this one — a cap, the
            active-text uniqueness rule, `brand_archived`, `not_found` (the
            prompt was deleted between the plan and the write, or between the
            write and the final reconciliation) or `prompt_changed` (another
            writer renamed it, or moved it to another brand, in that window —
            nothing was applied to it; re-run the sync). The rest of the request
            was still applied.
      required:
        - text
        - action
    PromptSyncUsage:
      type: object
      properties:
        prompts:
          type: object
          properties:
            used:
              type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
              description: >-
                ACTIVE prompts in the organisation after the run (before it, on
                `dry_run`). On the Agency plan, prompt-country slots — the unit
                its prompt cap counts.
            cap:
              anyOf:
                - type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                - type: 'null'
          required:
            - used
            - cap
        countries:
          type: object
          properties:
            used:
              type: array
              items:
                type: string
              description: Distinct markets held by the organisation's active prompts.
            cap:
              anyOf:
                - type: integer
                  minimum: -9007199254740991
                  maximum: 9007199254740991
                - type: 'null'
          required:
            - used
            - cap
      required:
        - prompts
        - countries
    WriteError:
      type: object
      properties:
        code:
          type: string
          description: >-
            A stable code from the error table (`prompt_cap_exceeded`,
            `country_cap_exceeded`, `competitor_cap_exceeded`, `conflict`, …).
        message:
          type: string
      required:
        - code
        - message
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      bearerFormat: sfs_live_… / sfs_test_…
      description: >-
        A Surfais API key — an organisation key or a partner key. Issued by
        Surfais; shown once. Rotate by creating a new key, then revoking the old
        one.

````