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

# Preliminary scan snapshot best effort revision 1

> Fired at most once per scan, only when a callback_url was supplied AND the scan outlived the create endpoint's sync-wait window with at least one provider reported. Best-effort: there is deliberately no retry backstop — a scan may skip straight to scan.completed. The payload is a full PartnerScanOutput with status "preliminary" and revision 1. Verify the signature before trusting the payload (see the signature note on scan.completed — both events use the identical scheme). Deduplicate on (scan_id, revision).



## OpenAPI

````yaml https://www.getsurfaced.ai/api/partner/openapi.json webhook scan.preliminary
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: {}
components:
  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.