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

# List scan history

> Paginated scan history for the authenticated partner, newest first. Defaults to the last 30 days — pass an explicit ?from to reach further back, or ?month for a calendar-month shorthand. Uses cursor pagination within the window: pass the previous page's next_cursor as ?cursor to fetch older entries. Rate limited separately from scan creation (600 reads/hour).



## OpenAPI

````yaml https://www.getsurfaced.ai/api/partner/openapi.json get /api/partner/scans
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/scans:
    get:
      tags:
        - History
      summary: List scan history
      description: >-
        Paginated scan history for the authenticated partner, newest first.
        Defaults to the last 30 days — pass an explicit ?from to reach further
        back, or ?month for a calendar-month shorthand. Uses cursor pagination
        within the window: pass the previous page's next_cursor as ?cursor to
        fetch older entries. Rate limited separately from scan creation (600
        reads/hour).
      operationId: listScans
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 50
            maximum: 200
          description: Page size. Values above 200 are clamped to 200.
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: >-
            Opaque cursor from the previous page's next_cursor (a created_at
            timestamp). Returns scans created strictly before it.
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: >-
            Window start (inclusive, ISO-8601 date or datetime). Defaults to 30
            days ago. Pass an older date to list historical scans. Mutually
            exclusive with month (400 if combined).
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: >-
            Window end (exclusive, ISO-8601 date or datetime). Defaults to now.
            Must be after from (400 otherwise). Mutually exclusive with month
            (400 if combined).
        - name: month
          in: query
          required: false
          schema:
            type: string
            pattern: ^\d{4}-(0[1-9]|1[0-2])$
          description: >-
            Calendar-month shorthand: expands to that UTC calendar month
            ([first-of-month, first-of-next-month)), overriding the default
            rolling 30-day window. Mutually exclusive with from/to — 400 if
            combined with either.
      responses:
        '200':
          description: One page of scan history.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerScanList'
              example:
                scans:
                  - scan_id: 6f1d2c3b-4a5e-4f60-9b7c-8d9e0f1a2b3c
                    created_at: '2026-07-14T10:31:00.000Z'
                    url: https://example.nl
                    tier: basic
                    status: completed
                    billable_cents: 250
                next_cursor: null
        '400':
          description: Invalid limit/month parameter, or month combined with from/to.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidLimit:
                  value:
                    error: Invalid limit — must be a positive integer
                    code: invalid_limit
                invalidMonth:
                  value:
                    error: Invalid month — must be YYYY-MM
                    code: invalid_month
                monthWithRange:
                  value:
                    error: month is mutually exclusive with from/to
                    code: month_from_to_conflict
        '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:
    PartnerScanList:
      type: object
      required:
        - scans
        - next_cursor
      properties:
        scans:
          type: array
          items:
            $ref: '#/components/schemas/PartnerScanListItem'
          description: Scan history, newest first.
        next_cursor:
          type:
            - string
            - 'null'
          description: >-
            created_at of the last item in this page; pass as ?cursor to fetch
            the next (older) page. Null when this is the last page.
    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".
    PartnerScanListItem:
      type: object
      required:
        - scan_id
        - created_at
        - url
        - tier
        - status
      properties:
        scan_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        url:
          type: string
          description: The scanned company URL.
        tier:
          type:
            - string
            - 'null'
        status:
          type: string
        billable_cents:
          type:
            - integer
            - 'null'
          description: >-
            Rate-card value of this scan in cents (the per-scan overage rate for
            its surface package). Not necessarily an amount charged: under a
            plan with included scans, only scans beyond the monthly allowance
            bill individually — see GET /api/partner/usage's billing block for
            the authoritative period total. Present once a scan result row
            exists. Completed scans carry their rate-card value; FAILED scans
            carry 0 — an all-failed scan is never billed, but it does get an
            evidence row for audit (EXPX-1687), so this key appears for those
            too.
  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.