SurfacedBySurfacedBy Docs

Webhook Events

The event types SurfacedBy delivers via outbound webhooks.

Ask an AI:Open in ChatGPTOpen in Claude

Every webhook delivery is a JSON object with a stable envelope. The envelope includes the event id, the event type, the timestamp, and the event-specific payload under data. Every payload also carries workspace_id and workspace_slug under data so you can route the event to the right tenant without a follow-up API call.

Envelope

{
  "id": "evt_01HR7A2B3C4D5E6F7G8H9J0K1L",
  "type": "scan.completed",
  "created_at": "2026-04-16T12:34:56Z",
  "livemode": true,
  "data": {
    "workspace_id": "wsp_01HR7A2B3C4D5E6F7G8H9J0K1L",
    "workspace_slug": "acme-corp",
    "...": "event-specific fields"
  }
}

The event id is globally unique. Use it to deduplicate if you receive the same event twice during retries. The event type is the type field; switch on it in your handler. livemode is true for production deliveries and false for events fired by the "Send test event" button in the dashboard. workspace_id and workspace_slug appear on every event type; route on workspace_id (stable) and use workspace_slug for human-readable destinations such as channel names or filenames.

Event types

EventDescription
scan.completedA scan finished successfully; results are ready to read.
scan.failedA scan terminated with an unrecoverable error.
scan.progressPeriodic progress update while a scan runs. Rate-limited server-side.
alert.triggeredA configured alert rule matched a citation change.
citation.changedA tracked brand's citation rate for a platform crossed a meaningful threshold.
domain.addedA new domain was registered on the account.
competitor.discoveredThe AI discovered a new competitor from scan output.
report.readyA generated report is available to fetch.

scan.completed

Fires once when a scan finishes successfully.

FieldTypeMeaning
scan_idstringUnique scan id.
domain_idstringDomain the scan ran for.
domainstringDomain name.
scan_typestringRaw internal type. One of pulse, paid_initial, paid_scheduled, console_on_demand, free_audit.
measurement_scopestringdaily_monitoring (when scan_type is pulse) or full_benchmark (for all other values).
measurement_labelstringHuman label: Daily Monitoring or Full Benchmark.
visibility_scoreintegerVisibility Score at the end of the scan.
started_atISO 8601When the scan started.
completed_atISO 8601When the scan finished.
results_summary.total_cost_usdfloatInternal cost of the scan in USD.
results_summary.duration_secondsfloatWall-clock duration.
results_summary.citation_ratefloatBrand Coverage percentage.
results_summary.total_citationsintegerTotal citations across all platforms.
{
  "id": "evt_01HR7A2B3C4D5E6F7G8H9J0K1L",
  "type": "scan.completed",
  "created_at": "2026-04-16T12:34:56Z",
  "livemode": true,
  "data": {
    "workspace_id": "wsp_01HR7A2B3C4D5E6F7G8H9J0K1L",
    "workspace_slug": "acme-corp",
    "scan_id": "scn_01HR7B2K9...",
    "domain_id": "dom_01HR7B2KA...",
    "domain": "example.com",
    "scan_type": "paid_scheduled",
    "measurement_scope": "full_benchmark",
    "measurement_label": "Full Benchmark",
    "visibility_score": 72,
    "started_at": "2026-04-16T12:30:00Z",
    "completed_at": "2026-04-16T12:34:55Z",
    "results_summary": {
      "total_cost_usd": 1.42,
      "duration_seconds": 295.0,
      "citation_rate": 38.4,
      "total_citations": 121
    }
  }
}

Fetch the full analytics with GET /domains/{domain_id}/analytics?scan_id={scan_id}.

scan.failed

Fires when a scan terminates with an unrecoverable error. The next scheduled scan runs normally; you do not need to retry.

FieldTypeMeaning
scan_idstringUnique scan id.
domain_idstringDomain the scan ran for.
domainstringDomain name.
scan_typestringRaw internal type (see scan.completed).
error_summarystringFirst 500 characters of the failure reason.

scan.progress

Periodic progress update while a scan runs. Rate-limited to at most one message per scan per five seconds.

FieldTypeMeaning
scan_idstringUnique scan id.
domain_idstringDomain the scan ran for.
domainstringDomain name.
scan_typestringRaw internal type.
phasestringCurrent scan phase (for example testing_platforms).
percentageinteger0 to 100 progress.
platformsarrayOptional per-platform progress objects when the phase has per-platform data.

alert.triggered

Fires whenever an alert is generated. The same alert types are documented under Alerts.

FieldTypeMeaning
alert_idstringAlert id.
alert_typestringOne of: first_scan_complete, visibility_drop, citation_rate_change, competitor_surpass, platform_drop, new_competitor, lens_emerged, lens_decayed.
severitystringinfo, warning, or critical.
titlestringHuman-readable alert title.
summarystringShort prose summary of what changed.
domain_idstringDomain the alert is scoped to.
scan_idstringScan id that produced the alert (nullable for non-scan alerts).
detailsobjectType-specific context (numeric deltas, platform key, competitor domain, lens slug, etc.).
{
  "id": "evt_01HR7A2B3C4D5E6F7G8H9J0K1M",
  "type": "alert.triggered",
  "created_at": "2026-04-16T12:35:10Z",
  "livemode": true,
  "data": {
    "workspace_id": "wsp_01HR7A2B3C4D5E6F7G8H9J0K1L",
    "workspace_slug": "acme-corp",
    "alert_id": "alt_01HR7B2KB...",
    "alert_type": "platform_drop",
    "severity": "warning",
    "title": "Dropped on Perplexity",
    "summary": "AI Mentions on Perplexity fell from 38 to 12 since the last Full Benchmark.",
    "domain_id": "dom_01HR7B2KA...",
    "scan_id": "scn_01HR7B2K9...",
    "details": {
      "platform": "perplexity",
      "previous_count": 38,
      "new_count": 12
    }
  }
}

citation.changed

Fires once per scan completion when the total citation count for a domain changes versus the previous comparable scan.

FieldTypeMeaning
scan_idstringCurrent scan id.
previous_scan_idstringScan id used as the comparison anchor.
domain_idstringDomain id.
domainstringDomain name.
previous_countintegerCitation count on the previous comparable scan.
new_countintegerCitation count on the current scan.
deltaintegernew_count - previous_count.
scan_typestringRaw internal type of the current scan.
measurement_scopestringdaily_monitoring or full_benchmark.
measurement_labelstringDaily Monitoring or Full Benchmark.
previous_measurement_scopestringScope of the comparison anchor.
comparison_scopestringsame_scope when both sides are the same scope, mixed_scope when they differ. Treat mixed_scope deltas as directional only.

domain.added

Fires when a domain is added to a workspace.

FieldTypeMeaning
domain_idstringNewly created domain id.
domainstringDomain name.
sourcestringdashboard when added through the UI or api when added through the REST API.

competitor.discovered

Fires for every newly discovered competitor at the end of a scan that ran competitor discovery.

FieldTypeMeaning
competitor_idstringCompetitor row id.
competitor_domainstringRoot domain of the competitor.
domain_idstringTracked domain the competitor was discovered for.
domainstringDomain name.
discovery_sourcestringInternal source key (for example ai_responses).

report.ready

Fires when a generated report is ready to fetch.

FieldTypeMeaning
report_idstringReport id.
domain_idstringDomain the report covers.
titlestringReport title.
audience_presetstringAudience preset used to generate the report.
generated_bystringmanual or schedule.

Order and delivery

Events are not guaranteed to be delivered in order. Do not assume a scan.completed event for scan A arrives before scan.completed for scan B just because scan A started first. Use the created_at timestamp if order matters to your handler.

On this page