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

# Provision a property

> Provision an own brand with competitors.

Creates the own brand (`is_own: true`, `external_ref`), then links each competitor in turn — an existing competitor row in the organisation whose stored domain normalises to the same host is reused (its stored name kept), otherwise one is created. `markets` are validated against the market list and the organisation's country-cap headroom and echoed back; they are NOT persisted (countries attach to prompts). Checks run before any write: 409 `brand_exists` (an own brand whose stored domain normalises to the same host — a dashboard-created `https://www.Example.com/` matches `example.com`) / `external_ref_exists` (both with `details.existing_id`) and 422 `country_cap_exceeded` leave nothing behind, as does 422 `brand_cap_exceeded`. NOT ATOMIC past the brand insert: a competitor refused by the per-brand cap stops the loop and the response is 422 `competitor_cap_exceeded` whose `details` (`PropertyPartialFailure`) carry the CREATED brand and a per-competitor result — retry the failed ones through `POST …/brands/{id}/competitors`; do not re-provision. Each competitor is itself ONE atomic call, so a refused competitor leaves nothing behind — no row and no link.



## OpenAPI

````yaml /api-reference/openapi.json post /orgs/{orgId}/properties
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}/properties:
    post:
      tags:
        - brands
      summary: Provision a property
      description: >-
        Provision an own brand with competitors.


        Creates the own brand (`is_own: true`, `external_ref`), then links each
        competitor in turn — an existing competitor row in the organisation
        whose stored domain normalises to the same host is reused (its stored
        name kept), otherwise one is created. `markets` are validated against
        the market list and the organisation's country-cap headroom and echoed
        back; they are NOT persisted (countries attach to prompts). Checks run
        before any write: 409 `brand_exists` (an own brand whose stored domain
        normalises to the same host — a dashboard-created
        `https://www.Example.com/` matches `example.com`) /
        `external_ref_exists` (both with `details.existing_id`) and 422
        `country_cap_exceeded` leave nothing behind, as does 422
        `brand_cap_exceeded`. NOT ATOMIC past the brand insert: a competitor
        refused by the per-brand cap stops the loop and the response is 422
        `competitor_cap_exceeded` whose `details` (`PropertyPartialFailure`)
        carry the CREATED brand and a per-competitor result — retry the failed
        ones through `POST …/brands/{id}/competitors`; do not re-provision. Each
        competitor is itself ONE atomic call, so a refused competitor leaves
        nothing behind — no row and no link.
      operationId: createProperty
      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: 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/PropertyCreate'
      responses:
        '201':
          description: The created property (the brand detail) plus the markets echo.
          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/PropertyCreated'
                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_exists` / `external_ref_exists` — `details` is an
            `ExistingResourceConflict`, nothing created; `conflict` (or another
            code a competitor's own refusal carried) — a competitor could not be
            linked while provisioning: the brand WAS created and `details` is a
            `PropertyPartialFailure`; or `idempotency_key_reuse` /
            `idempotency_key_in_flight`, with no `details`.
          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:
                        anyOf:
                          - $ref: '#/components/schemas/PropertyPartialFailure'
                          - $ref: '#/components/schemas/ExistingResourceConflict'
                    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'
        '422':
          description: >-
            `brand_cap_exceeded` — nothing created, `details` is a `CapRefusal`;
            `country_cap_exceeded` — nothing created, `details` is a
            `CountryCapRefusal` (the route checks this one itself, so it names
            both sides); `competitor_cap_exceeded` — the brand WAS created and
            `details` is a `PropertyPartialFailure`.
          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:
                        anyOf:
                          - $ref: '#/components/schemas/PropertyPartialFailure'
                          - $ref: '#/components/schemas/CountryCapRefusal'
                          - $ref: '#/components/schemas/CapRefusal'
                    required:
                      - code
                      - message
                  request_id:
                    type: string
                    description: Echoed in the `X-Request-Id` header.
                required:
                  - error
                  - request_id
        '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:
    PropertyCreate:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        domain:
          description: >-
            Registrable host name. Normalised before storage and comparison:
            lower-cased, scheme / `www.` / path / port removed (`example.com`,
            `https://www.Example.com/x` and `EXAMPLE.COM` are the same domain).
          type: string
          minLength: 1
          maxLength: 2048
        geography:
          description: >-
            Primary-market hint (alpha-2 when set by the UI). Not validated
            against the market list.
          anyOf:
            - type: string
              minLength: 1
              maxLength: 100
            - type: 'null'
        external_ref:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 255
              description: >-
                Your own identifier. Unique per organisation,
                case-insensitively.
            - type: 'null'
        competitors:
          description: >-
            Competitors to track for the property. An existing competitor row in
            the organisation with the same domain is reused (its stored name is
            kept); otherwise one is created. Duplicate domains within the
            request are collapsed (first wins). A competitor with the property's
            own domain is 400 `validation_error` (`competitor_is_self`).
          maxItems: 100
          type: array
          items:
            $ref: '#/components/schemas/CompetitorInput'
        markets:
          description: >-
            Markets you intend to run prompts in. Validated against the market
            list AND the organisation's country-cap headroom (422
            `country_cap_exceeded` before anything is created); NOT persisted
            here — countries attach to prompts (`POST …/prompts`, `PUT
            …/brands/{id}/prompts`). Echoed in `markets`.
          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}$
      required:
        - name
        - domain
    PropertyCreated:
      type: object
      properties:
        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: UUID.
        name:
          type: string
        domain:
          type: string
        is_own:
          type: boolean
          description: true = a property you track; false = a competitor row.
        external_ref:
          anyOf:
            - type: string
            - type: 'null'
          description: Your own id for the property (brands.external_ref).
        geography:
          anyOf:
            - type: string
            - type: 'null'
          description: Primary-market hint (free text, alpha-2 when set by the UI).
        sunset_at:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Set when the brand has been archived; archived brands are omitted
            from lists.
        created_at:
          type: string
          description: RFC 3339 timestamp.
          format: date-time
        updated_at:
          type: string
          description: RFC 3339 timestamp.
          format: date-time
        competitors:
          type: array
          items:
            $ref: '#/components/schemas/CompetitorRef'
          description: Empty for competitor rows.
        last_scan_at:
          anyOf:
            - type: string
              description: RFC 3339 timestamp.
              format: date-time
            - type: 'null'
          description: >-
            When the latest COMPLETED, non-preview run for this brand finalised.
            A preview run, or a run later demoted from completed, is never this
            value. Null until the first such run finalises.
        last_scan_changed_at:
          anyOf:
            - type: string
              description: RFC 3339 timestamp.
              format: date-time
            - type: 'null'
          description: >-
            A change token that moves on finalisation, completion flips,
            deletions, budget aborts, any update of the brand row itself, a
            linked competitor's name or domain change, and a change to this
            org's scheduling inputs. Poll this for 'anything changed' among the
            STORED fields, not last_scan_at. It does NOT cover
            next_scheduled_scan, which is derived from the clock as well — see
            that field.
        next_scheduled_scan:
          anyOf:
            - type: string
              pattern: ^\d{4}-\d{2}-\d{2}$
              description: Calendar date, `YYYY-MM-DD` (UTC).
              format: date
            - type: 'null'
          description: >-
            UTC date the next scheduled scan is due for this organisation's plan
            (Mon+Thu for paid tiers below Enterprise, daily for Enterprise).
            Null for tiers with no cadence, on-demand orgs, non-customer orgs,
            competitor rows, archived brands — and for an org that has not yet
            completed its first (activation) scan, which is not scheduled.
            COMPUTED AT REQUEST TIME and NOT covered by last_scan_changed_at: it
            is derived from the org's scheduling inputs AND the clock, so on a
            cadence day it rolls to the next date as the dispatch hour passes,
            with no write anywhere for a token to version. Read it as an as-of
            value with the response rather than caching it until the token
            moves, and treat it as a plan — a cadence scan can be delayed or
            fail.
        markets:
          $ref: '#/components/schemas/MarketsEcho'
      required:
        - id
        - name
        - domain
        - is_own
        - external_ref
        - geography
        - sunset_at
        - created_at
        - updated_at
        - competitors
        - last_scan_at
        - last_scan_changed_at
        - next_scheduled_scan
        - markets
    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
    PropertyPartialFailure:
      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 property WAS created; retry the failed competitors through `POST
            …/brands/{id}/competitors` once there is headroom.
        brand:
          $ref: '#/components/schemas/PropertyCreated'
        competitors:
          type: array
          items:
            $ref: '#/components/schemas/CompetitorApplyResult'
      required:
        - brand_id
        - brand
        - competitors
    ExistingResourceConflict:
      type: object
      properties:
        existing_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 row that already holds this domain, external_ref or prompt text.
      required:
        - existing_id
    CountryCapRefusal:
      type: object
      properties:
        cap:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        attempted:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: The market count the request would leave the organisation holding.
        currently_used:
          type: array
          items:
            type: string
          description: Markets already held across the organisation's active prompts.
        requested:
          type: array
          items:
            type: string
      required:
        - cap
        - attempted
        - currently_used
        - requested
    CapRefusal:
      type: object
      properties:
        tier:
          type: string
        cap:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        attempted:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
    CompetitorInput:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        domain:
          description: >-
            Registrable host name. Normalised before storage and comparison:
            lower-cased, scheme / `www.` / path / port removed (`example.com`,
            `https://www.Example.com/x` and `EXAMPLE.COM` are the same domain).
          type: string
          minLength: 1
          maxLength: 2048
      required:
        - name
        - domain
    CompetitorRef:
      type: object
      properties:
        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: UUID.
        name:
          type: string
        domain:
          type: string
      required:
        - id
        - name
        - domain
    MarketsEcho:
      type: object
      properties:
        accepted:
          type: array
          items:
            type: string
          description: >-
            The requested markets, upper-cased and de-duplicated — every one
            fits the headroom.
        currently_used:
          type: array
          items:
            type: string
          description: >-
            Distinct markets already held by the organisation's ACTIVE prompts
            (the country cap's grain) at the time of the request.
        cap:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: 'null'
          description: >-
            Effective country cap; null when caps are not enforced for this
            organisation.
      required:
        - accepted
        - currently_used
        - cap
    CompetitorApplyResult:
      type: object
      properties:
        name:
          type: string
          description: >-
            As sent — this list reports the request's rows; the linked rows as
            stored are in `brand.competitors`.
        domain:
          type: string
          description: As sent, normalised — correlate on this.
        status:
          type: string
          enum:
            - applied
            - failed
            - not_attempted
          description: >-
            `not_attempted`: a preceding competitor hit the cap, so this one was
            never tried.
        competitor_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: UUID.
        reused:
          description: >-
            true when an existing competitor row in the organisation was linked
            instead of a new one being created.
          type: boolean
        error:
          $ref: '#/components/schemas/WriteError'
      required:
        - name
        - domain
        - status
    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.

````