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

# Get monthly usage, quota, and billing

> Usage rollup for one UTC calendar month: scan counts (total/completed/failed, by tier), remaining quota headroom, this partner's rate limit, and the amount billed for the period. `scans.total` — and therefore `quota.used` — excludes failed scans: failed scans are not billed and do not count toward the quota. The monthly quota itself is soft (informational only, never blocks requests) unless the partner also has `quota.overage_hard_cap_scans` configured, in which case requests are hard-rejected once usage reaches the quota plus that cap. Defaults to the current in-progress month when ?month is omitted.



## OpenAPI

````yaml https://www.getsurfaced.ai/api/partner/openapi.json get /api/partner/usage
openapi: 3.1.0
info:
  title: Surfaced Partner API
  version: 1.0.0
  description: >-
    Competitor Scan API for Surfaced partners. Start a scan of a company's
    AI-search visibility, poll for the result, and (optionally) receive signed
    webhooks via progressive delivery.


    ## Progressive delivery


    A scan can produce up to two payload generations, distinguished by
    `revision`:


    - **Preliminary** (`status: "preliminary"`, `revision: 1`) — a best-effort
    mid-run snapshot with a live-computed `geo_score`/`band` from whatever
    observations exist so far. Returned inline on the 202 POST response (and 200
    GET polls) once at least one provider has reported, and pushed once to your
    `callback_url` as `scan.preliminary`. There is no retry backstop for the
    preliminary push — it is opportunistic.

    - **Final** (`status: "completed"`) — guaranteed for every scan that
    produces a result. Delivered as `scan.completed` with `revision: 2` when a
    preliminary was delivered first, otherwise `revision: 1`.


    Deduplicate on `(scan_id, revision)`.


    ## Failed scans


    A scan that produces no result ends as `status: "failed"`. It is terminal:
    nothing about it will change. `error.code` says which way it failed —
    `all_providers_failed` (we queried every provider you asked for and every
    call errored) or `analysis_failed` (the scan died before any provider was
    queried).


    Every scan that reaches an end state gets exactly ONE terminal signal, so
    you never have to wait on a timeout to find out which happened:


    - **`scan.completed`** — a result exists. `data` is a `PartnerScanOutput`.

    - **`scan.failed`** — no result exists. `data` is a
    `PartnerScanFailedOutput`, and it is a DIFFERENT shape: `geo_score`, `band`,
    `competitors` and `score_confidence` are ABSENT (not null), and a required
    `error` block carries a stable `code` (e.g. `all_providers_failed`), a human
    `message`, and `providers_failed`.


    That omission is deliberate and worth designing around. A failed scan means
    *we could not reach the models*, which is a different statement from *this
    company has no AI visibility* — and a `null` score is far too easily
    coalesced into `0` and rendered as the second. If your integration turns a
    scan into copy, branch on the event name (or on the presence of
    `data.error`) before reading any result field.


    **Every channel carries the same failure payload.** If you poll instead of
    using webhooks, `GET /api/partner/competitor-scan/{id}` returns **200** with
    that identical `PartnerScanFailedOutput` — `error` block included — and so
    does the create endpoint when a scan fails inside its sync-wait window. You
    never need a `callback_url` to find out why a scan failed.


    So HTTP 200 on either endpoint means *a terminal payload is enclosed*, not
    *the scan succeeded*. Check `status` (or the presence of `error`) before
    reading any score field. 202 now means the scan is still running, and
    carries `poll_after_ms`. Keep the legacy rule too: an absent `poll_after_ms`
    means stop, which now only occurs on a degraded path where we know a scan is
    terminal but could not assemble its payload.


    A failed scan is not billed and does not consume your monthly allowance.


    ## Score confidence


    `score_confidence` reflects provider coverage behind `geo_score`: the full
    basic trio (chatgpt + gemini + perplexity) reporting = `high`; partial
    coverage degrades it to `reduced` or `low`.


    ## Sandbox


    Test keys (`gsk_test_...`) exercise the full request validation, then
    short-circuit to deterministic mock responses: no scan runs, nothing is
    persisted, no rate limit is consumed, and responses carry `sandbox: true`.
    Same input always yields the same output, so integration tests can assert
    exact values.
servers:
  - url: https://www.getsurfaced.ai
    description: Production
security:
  - partnerApiKey: []
tags:
  - name: Scans
    description: Start and poll competitor scans.
  - name: History
    description: Scan history and reconciliation.
  - name: Usage
    description: Monthly usage rollups, quota headroom, and billing.
paths:
  /api/partner/usage:
    get:
      tags:
        - Usage
      summary: Get monthly usage, quota, and billing
      description: >-
        Usage rollup for one UTC calendar month: scan counts
        (total/completed/failed, by tier), remaining quota headroom, this
        partner's rate limit, and the amount billed for the period.
        `scans.total` — and therefore `quota.used` — excludes failed scans:
        failed scans are not billed and do not count toward the quota. The
        monthly quota itself is soft (informational only, never blocks requests)
        unless the partner also has `quota.overage_hard_cap_scans` configured,
        in which case requests are hard-rejected once usage reaches the quota
        plus that cap. Defaults to the current in-progress month when ?month is
        omitted.
      operationId: getUsage
      parameters:
        - name: month
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-(0[1-9]|1[0-2])$
          description: >-
            UTC calendar month to report on, YYYY-MM. Defaults to the current
            (in-progress) month.
      responses:
        '200':
          description: Usage summary for the requested (or current) month.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerUsage'
              example:
                period:
                  month: 2026-07
                  from: '2026-07-01T00:00:00.000Z'
                  to: '2026-08-01T00:00:00.000Z'
                scans:
                  total: 42
                  completed: 39
                  failed: 3
                  by_tier:
                    basic: 30
                    full: 12
                quota:
                  monthly_scan_quota: 500
                  effective_quota: 500
                  credits: 0
                  included: 500
                  used: 42
                  remaining: 458
                  overage_hard_cap_scans: 100
                  prorated: false
                prorated: false
                rate_limit_per_hour: 200
                billed_cents: 8400
                billing:
                  base_fee_cents: 8400
                  included_scans: 42
                  overage_scans: 0
                  overage_cents: 0
                  billed_cents: 8400
        '400':
          description: Invalid month parameter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Invalid month — must be YYYY-MM
                code: invalid_month
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Suspended'
        '429':
          description: Read rate limit exceeded (600/hour).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Rate limited
                code: rate_limited
        '503':
          $ref: '#/components/responses/NotEnabled'
components:
  schemas:
    PartnerUsage:
      type: object
      description: >-
        Per-partner usage rollup for one UTC calendar month — scan counts, quota
        headroom, and the amount billed for the period. `scans.total` excludes
        failed scans (failed scans are not billed and do not count toward the
        quota) and is the same figure used to evaluate the monthly quota, so
        `quota.used` always equals `scans.total`. Billing is
        base-fee-plus-overage: a partner with a configured monthly allowance
        (`quota.monthly_scan_quota`) is charged a monthly base fee plus a
        metered rate for scans beyond the allowance, counted chronologically by
        scan creation time within the period; a partner with no allowance
        configured is billed flat per scan at the same per-surface-package rate,
        with no base fee. See `billing` for the full split.


        **Only scans you requested through this API are counted.** If you also
        send traffic to Surfaced through a referral link, the signups attributed
        to you are NOT partner-API scans: they do not appear in `GET
        /api/partner/scans`, they do not consume your monthly allowance, and
        they are never billed to you. Referral attribution and API usage are
        separate ledgers on purpose — one is credit for traffic you sent, the
        other is what you asked us to run.
      required:
        - period
        - scans
        - quota
        - rate_limit_per_hour
        - billed_cents
        - billing
        - prorated
      properties:
        period:
          type: object
          required:
            - month
            - from
            - to
          properties:
            month:
              type: string
              pattern: ^\d{4}-(0[1-9]|1[0-2])$
              examples:
                - 2026-07
            from:
              type: string
              format: date-time
              description: Inclusive UTC start of the month.
            to:
              type: string
              format: date-time
              description: Exclusive UTC start of the next month.
        scans:
          type: object
          required:
            - total
            - completed
            - failed
            - by_tier
          properties:
            total:
              type: integer
              description: >-
                Scans in the period, excluding failed scans — failed scans are
                not billed and do not count toward the quota. This is the figure
                the monthly quota is evaluated against.
            completed:
              type: integer
              description: Scans that finished successfully.
            failed:
              type: integer
              description: Scans that errored out.
            by_tier:
              type: object
              required:
                - basic
                - full
              properties:
                basic:
                  type: integer
                full:
                  type: integer
                  description: Includes scans requesting both packages (tier "both").
        quota:
          type:
            - object
            - 'null'
          description: >-
            Null when no monthly_scan_quota is configured for this partner
            (unlimited).
          required:
            - monthly_scan_quota
            - credits
            - included
            - used
            - remaining
            - overage_hard_cap_scans
            - prorated
          properties:
            monthly_scan_quota:
              type: integer
            credits:
              type:
                - integer
                - 'null'
              description: >-
                Scans credited to this account for THIS period only, on top of
                the plan allowance (for example to make good a service
                incident). 0 in an uncredited period, which is the normal case.
                A credit never carries over into another month. `null` means the
                credit total could not be read for this request — a transient
                condition; `included` and `remaining` are then null too, rather
                than reporting the uncredited figures as if they were
                authoritative. Retry.
            included:
              type:
                - integer
                - 'null'
              description: >-
                The allowance actually in force this period: the effective quota
                plus `credits`. `remaining` is this minus `used`, and the hard
                cap described under `overage_hard_cap_scans` is layered on top
                of this figure. Equal to `monthly_scan_quota` in an ordinary
                uncredited, unprorated period. Null whenever `credits` is null.
            used:
              type: integer
              description: Equal to scans.total (failed scans excluded — see scans.total).
            remaining:
              type:
                - integer
                - 'null'
              description: >-
                `included` minus `used`. Equal to monthly_scan_quota minus used
                every period after the first that carries no credit — but see
                `prorated` below for a partner's first (activation) period, and
                `credits` above. Can go negative once the allowance in force is
                exceeded. Null whenever `credits` is null (the credit total
                could not be read, so the headroom is unknown).
            overage_hard_cap_scans:
              type:
                - integer
                - 'null'
              description: >-
                Maximum overage scans permitted beyond the included monthly
                allowance. Null means no ceiling. When set, POST
                /api/partner/competitor-scan starts rejecting new scans with a
                429 once used reaches the allowance in force for the period plus
                this value — that is `included`, which equals
                `monthly_scan_quota` except during a prorated first period (see
                `prorated`) or a period carrying a credit (see `credits`). The
                cap itself is never prorated and is never credited.
            prorated:
              type: boolean
              description: >-
                True when the CURRENT queried period is this partner's
                first-activation period and that scaled the effective allowance
                down from the flat monthly_scan_quota shown above (proportional
                to the days remaining in the month from activation).
                monthly_scan_quota always shows the flat configured plan number;
                `remaining` reflects the scaled-down effective allowance when
                this is true. Always false for every period after the first.
        prorated:
          type: boolean
          description: >-
            True when this period is the partner's first (activation) period, in
            which case the included allowance and the base fee are scaled by the
            fraction of the month remaining from activation. Mirrored inside
            `quota` for convenience, but authoritative here: `quota` is null for
            a partner with no configured allowance, and such a partner can still
            have a prorated `billing.base_fee_cents`.
        rate_limit_per_hour:
          type: integer
          description: This partner's configured hourly scan-creation rate limit.
        billed_cents:
          type:
            - integer
            - 'null'
          deprecated: true
          description: >-
            Deprecated — kept for one release for back-compat, always equal to
            `billing.billed_cents`. Prefer `billing.billed_cents`. Null when the
            partner has no pricing configured at all (distinct from a partner
            who owes exactly $0).
        billing:
          type: object
          description: >-
            Base-fee-plus-overage billing breakdown for the period.
            `billed_cents` is null when the partner has no pricing configured at
            all — distinct from a partner who is correctly billed $0 (e.g. a
            base fee of 0 and no overage).
          required:
            - base_fee_cents
            - included_scans
            - overage_scans
            - overage_cents
            - billed_cents
          properties:
            base_fee_cents:
              type: integer
              description: Fixed monthly charge, in cents. 0 for partners with no base fee.
            included_scans:
              type: integer
              description: >-
                Scans this period covered by the allowance in force
                (`quota.included`), counted chronologically from the start of
                the period. 0 when no allowance is configured. During a prorated
                first period this is capped at the reduced allowance, so overage
                can begin while `used` is still below `monthly_scan_quota`.
            overage_scans:
              type: integer
              description: >-
                Scans this period beyond the allowance — every scan is overage
                when no allowance is configured.
            overage_cents:
              type: integer
              description: >-
                Metered charge for overage_scans, in cents, at each scan's own
                surface-package rate.
            billed_cents:
              type:
                - integer
                - 'null'
              description: >-
                base_fee_cents + overage_cents. Null when the partner has no
                pricing configured at all.
    Error:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: >-
            Human-readable message. Free text that may interpolate per-request
            values (a domain, a tier label) — do not branch on it; branch on
            `code`.
        detail:
          type: string
          description: >-
            Optional human-readable elaboration (present on tier_not_allowed and
            monthly_scan_limit_reached).
        code:
          type: string
          enum:
            - unauthorized
            - partner_suspended
            - not_enabled
            - invalid_json
            - invalid_request_body
            - url_contains_userinfo
            - tier_not_allowed
            - invalid_llm_set
            - unscannable_domain
            - rate_limited
            - monthly_scan_limit_reached
            - invalid_limit
            - month_from_to_conflict
            - invalid_month
            - invalid_date
            - invalid_date_range
            - not_found
            - scan_failed
            - temporarily_unavailable
          description: >-
            Machine-readable failure slug, present on EVERY non-2xx response.
            Closed set; branch on this, never on `error`.
        domain:
          type: string
          description: >-
            The registrable domain that triggered an unscannable_domain
            rejection. Only present alongside code: "unscannable_domain".
  responses:
    Unauthorized:
      description: Missing, malformed, or revoked API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Unauthorized
            code: unauthorized
    Suspended:
      description: The partner account is suspended.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: partner_suspended
            code: partner_suspended
    NotEnabled:
      description: The partner scan API is not enabled in this environment.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: not_enabled
            code: not_enabled
  securitySchemes:
    partnerApiKey:
      type: http
      scheme: bearer
      description: >-
        Partner API key as a Bearer token: `Authorization: Bearer gsk_live_...`.
        Live keys (`gsk_live_`) run real scans; test keys (`gsk_test_`) run
        request validation then return deterministic sandbox mocks (`sandbox:
        true`, nothing persisted, no rate-limit consumption). Keys are issued
        per partner and are revokable.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.