SurfacedBySurfacedBy Docs

Webhook Events

The event types SurfacedBy delivers via outbound webhooks, with their payload fields.

Ask an AI:Open in ChatGPTOpen in Claude

Every webhook delivery is a JSON object with the same envelope. The envelope carries the event id, the event type, when the event was created, whether it is a real or test event, and the event-specific fields under data. Every data object also carries workspace_id and workspace_slug, so you can route the event to the right workspace without a follow-up API call.

Envelope

{
  "created_at": "2026-04-16T12:34:56.123456+00:00",
  "data": {
    "...": "event-specific fields",
    "workspace_id": "0b6f3c1e-8a2d-4c57-9f0e-2d4b7a1c9e53",
    "workspace_slug": "acme-corp"
  },
  "id": "5f1d2c3b-7e8a-4b9c-a0d1-e2f3a4b5c6d7",
  "livemode": true,
  "type": "scan.completed"
}
FieldMeaning
idEvent id (a UUID). The same event keeps the same id across retries and replays; deduplicate on it.
typeEvent type. Switch on it in your handler.
created_atWhen the event was created, as an ISO 8601 timestamp with a UTC offset.
livemodetrue for real events, false for events sent with the Send test event button.
data.workspace_idThe workspace the event belongs to. Stable; route on it.
data.workspace_slugThe workspace's current URL slug, for human-readable destinations such as channel names or filenames. It changes if the workspace is renamed, and can be null if it could not be resolved.

All ids in payloads (scan_id, domain_id, alert_id, and so on) are UUIDs. Keys in the delivered body are sorted alphabetically; parse the JSON rather than relying on key order, and verify the signature on the raw bytes.

Event types

EventDescription
scan.completedA check finished and its results are stored.
scan.failedA check stopped before it finished. It is final for that check.
scan.progressProgress while a check runs, at most one update every five seconds per check.
alert.triggeredAn alert fired for a domain, for example a Visibility Score drop or a competitor passing you.
citation.changedThe total number of citations changed between two completed checks of a domain.
domain.addedA domain was added to the workspace, from the dashboard or the Data API.
competitor.discoveredA check found a new competitor in the AI answers.
report.readyA report finished generating.

Every event about a check carries measurement_scope and measurement_label, which say what kind of check it was: daily_monitoring / Daily Check or full_benchmark / Full Check. See Measurement scope.

Which endpoints receive an event

An event about a domain goes only to endpoints on the same side as the domain. Events about a domain tracked in the dashboard go to the workspace's dashboard endpoints, and only while the workspace is on a paid plan (see Registering an endpoint). Events about a domain registered through the Console go only to that Console account's endpoints; see Console webhooks.

scan.completed

Fires once when a check finishes and its results are stored.

FieldTypeMeaning
scan_idstringThe check's id.
domain_idstringDomain the check ran for.
domainstringDomain name.
measurement_scopestringdaily_monitoring for a Daily Check, full_benchmark for a Full Check.
measurement_labelstringDaily Check or Full Check.
visibility_scoreintegerVisibility Score (0 to 100) computed from this check's own results when it finished. It can differ slightly from the headline score the dashboard shows; read the API if you need the served value.
is_first_checkbooleantrue when this is the domain's first completed Daily Check or Full Check, the check that brings its first results.
started_atstringWhen the check started (ISO 8601).
completed_atstringWhen the check finished (ISO 8601).
results_summary.brand_coverage_pctnumberBrand Coverage, as a percentage. On a Full Check it reflects that check's own answers. On a Daily Check it reflects the current state across every tracked prompt, not only the prompts this check re-tested.
results_summary.total_citationsintegerTotal citations across all AI platforms, on the same basis as brand_coverage_pct.
results_summary.duration_secondsnumberHow long the check took.
results_urlstringConsole checks only: the absolute URL of the check's results, https://api.surfacedby.com/api/v1/console/scans/{scan_id}/results. Fetch it with your Console API key. Other checks do not carry this field.
{
  "created_at": "2026-04-16T12:34:56.123456+00:00",
  "data": {
    "completed_at": "2026-04-16T12:34:55.482113+00:00",
    "domain": "example.com",
    "domain_id": "9c2e4a71-3b5d-4f08-8e6a-1d7c2b9f4e30",
    "is_first_check": false,
    "measurement_label": "Full Check",
    "measurement_scope": "full_benchmark",
    "results_summary": {
      "brand_coverage_pct": 38.4,
      "duration_seconds": 295.0,
      "total_citations": 121
    },
    "scan_id": "4a8f1e2d-6c3b-4d5a-9e7f-0b1c2d3e4f5a",
    "started_at": "2026-04-16T12:30:00.210944+00:00",
    "visibility_score": 72,
    "workspace_id": "0b6f3c1e-8a2d-4c57-9f0e-2d4b7a1c9e53",
    "workspace_slug": "acme-corp"
  },
  "id": "5f1d2c3b-7e8a-4b9c-a0d1-e2f3a4b5c6d7",
  "livemode": true,
  "type": "scan.completed"
}

To fetch the full analytics for the check, call the Data API's analytics endpoint with the scan_id.

scan.failed

Fires when a check stops on an error. A failed check is final: no scan.completed follows for the same scan_id. Scheduled checks continue on their normal cadence.

FieldTypeMeaning
scan_idstringThe check's id.
domain_idstringDomain the check ran for.
domainstringDomain name.
measurement_scopestringdaily_monitoring or full_benchmark.
measurement_labelstringDaily Check or Full Check.
error_summarystringWhy the check stopped, as one of the fixed messages below.

error_summary is always one of these messages, where example.com stands for the domain name:

  • We could not finish this check. (any failure without a more specific message)
  • This check was cancelled.
  • This check stopped because the domain was removed.
  • This check stopped because the account was deleted.
  • This check stopped because the plan no longer includes checks.
  • This check waited in the queue too long and was stopped.
  • We could not open example.com just now. The site is limiting how often it can be checked. Wait a few minutes, then try again.
  • We could not read enough from example.com to run the check. Make sure the site loads in your browser, then try again.

The last two name the domain, or say "that website" in its place. Match on the first words rather than the whole message if you branch on them.

scan.progress

Progress updates while a check runs, at most one per check every five seconds.

FieldTypeMeaning
scan_idstringThe check's id.
domain_idstringDomain the check ran for.
domainstringDomain name.
measurement_scopestringdaily_monitoring or full_benchmark.
measurement_labelstringDaily Check or Full Check.
progressobjectThe check's progress (fields below).

progress carries:

FieldTypeMeaning
phasestring or nullThe current step: preparing, generating_prompts, asking_platforms, reading_citations, scoring, writing_brief, or complete once the check has finished. Null when no named step is running, for example before the first one starts.
percentageintegerProgress from 0 to 100.
phasesarrayEvery step this check runs, in order, each { "key", "status" } with status pending, running, done, or failed. A Full Check lists preparing, generating_prompts, asking_platforms, reading_citations, scoring, writing_brief; a Daily Check lists asking_platforms and reading_citations. A step made of several parts that run together is done when every part is done, and otherwise failed, running, or pending, in that order of precedence.
platformsarray or nullOne row per AI platform, { "name", "status", "completed", "total" }, once the platform set is known. name is the platform key (for example chatgpt); completed counts answers already stored for that platform.
prompts_totalinteger or nullPrompts the check asks each platform.
platforms_totalinteger or nullAI platforms the check asks.
answers_completedinteger or nullAnswers stored so far across all platforms.
answers_totalinteger or nullTotal answers the check will collect.
google_aiostring or nullState of the Google AI Overviews check (pending, running, done, failed), or null when the check does not include it.

alert.triggered

Fires when an alert is generated. See Alerts for what each alert means.

FieldTypeMeaning
alert_idstringAlert id.
alert_typestringOne of first_scan_complete, visibility_drop, brand_coverage_change, platform_drop, prompt_set_changed, competitor_surpass, competitor_gap_widened, competitor_faded, competitor_passed, new_competitor, cited_in_ai, site_health_issue, site_health_resolved, theme_emerged, theme_decayed.
severitystringinfo, warning, or critical.
titlestringAlert title, as shown in the dashboard.
summarystringShort description of what changed.
domain_idstringDomain the alert is about.
scan_idstring or nullThe check that produced the alert, when there is one.
detailsobjectType-specific context, such as previous and current values, the platform key, or the competitor. Coverage values are brand_coverage_pct and previous_brand_coverage_pct; score values are current_score and previous_score. Alerts that compare two checks carry measurement_scope and measurement_label. cited_in_ai carries the prompt and the platforms. Theme alerts (theme_emerged, theme_decayed) include theme_slug, the Brand Theme's identifier, and theme_name, its display name. They also include baseline_coverage_pct, coverage_pct, compared_responses (shared answer cells per check), and the supporting baseline_scan_ids and confirmation_scan_ids. Fading compares the lowest of three baselines with the highest of two confirmations; emergence compares the highest baseline with the lowest confirmation, so every check supports the change. Site health alerts carry check_key, check_label, finding_key (the same value on the issue and on its resolution) and the evidence for the finding.
{
  "created_at": "2026-04-16T12:35:10.004521+00:00",
  "data": {
    "alert_id": "e7d6c5b4-a392-4817-8f6e-5d4c3b2a1f09",
    "alert_type": "platform_drop",
    "details": {
      "answers_compared": 45,
      "brand_coverage_pct": 0.0,
      "measurement_label": "Full Check",
      "measurement_scope": "full_benchmark",
      "platform": "perplexity",
      "platform_answers_compared": 15,
      "previous_brand_coverage_pct": 40.0,
      "previous_mentions": 6
    },
    "domain_id": "9c2e4a71-3b5d-4f08-8e6a-1d7c2b9f4e30",
    "scan_id": "4a8f1e2d-6c3b-4d5a-9e7f-0b1c2d3e4f5a",
    "severity": "critical",
    "summary": "Your brand coverage on Perplexity dropped from 40.0% to 0%. You are no longer being mentioned on this platform.",
    "title": "Dropped on Perplexity",
    "workspace_id": "0b6f3c1e-8a2d-4c57-9f0e-2d4b7a1c9e53",
    "workspace_slug": "acme-corp"
  },
  "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "livemode": true,
  "type": "alert.triggered"
}

citation.changed

Fires when a check completes and the domain's total citation count differs from the previous completed check (Daily or Full). It does not fire for a domain's first check or when the count is unchanged.

FieldTypeMeaning
scan_idstringThe check that just completed.
previous_scan_idstringThe previous completed check, used for the comparison.
domain_idstringDomain id.
domainstringDomain name.
previous_countintegerCitation count on the previous check.
new_countintegerCitation count on this check.
deltaintegernew_count minus previous_count.
measurement_scopestringdaily_monitoring or full_benchmark, for this check.
measurement_labelstringDaily Check or Full Check.
previous_measurement_scopestringScope of the previous check.
comparison_scopestringsame_scope when both checks have the same scope, mixed_scope when one is a Daily Check and the other a Full Check. Treat mixed_scope deltas as directional only.

domain.added

Fires when a domain is added to a workspace.

FieldTypeMeaning
domain_idstringThe new domain's id.
domainstringDomain name.
sourcestringdashboard when added in the dashboard, api when added through the Data API.

competitor.discovered

Fires once for each competitor newly found for a domain at the end of a check.

FieldTypeMeaning
competitor_idstringCompetitor id.
competitor_domainstringThe competitor's domain.
domain_idstringThe tracked domain the competitor was found for.
domainstringTracked domain name.
discovery_sourcestringdetected when SurfacedBy found the competitor in AI answers, added_by_you when someone in your workspace entered it.

report.ready

Fires when a report has been generated and is ready to fetch.

FieldTypeMeaning
report_idstringReport id.
domain_idstringDomain the report covers.
titlestringReport title.
audience_presetstringThe audience preset the report was written for, for example executive, growth, or client.
generated_bystringmanual when someone generated the report, scheduled when a report schedule produced it.

Order

Events are not guaranteed to arrive in order, and retries can deliver an older event after a newer one. Use created_at when order matters to your handler.

On this page