Webhook Events
The event types SurfacedBy delivers via outbound webhooks.
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
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
| Event | Description |
|---|---|
scan.completed | A scan finished successfully; results are ready to read. |
scan.failed | A scan terminated with an unrecoverable error. |
scan.progress | Periodic progress update while a scan runs. Rate-limited server-side. |
alert.triggered | A configured alert rule matched a citation change. |
citation.changed | A tracked brand's citation rate for a platform crossed a meaningful threshold. |
domain.added | A new domain was registered on the account. |
competitor.discovered | The AI discovered a new competitor from scan output. |
report.ready | A generated report is available to fetch. |
scan.completed
Fires once when a scan finishes successfully.
| Field | Type | Meaning |
|---|---|---|
scan_id | string | Unique scan id. |
domain_id | string | Domain the scan ran for. |
domain | string | Domain name. |
scan_type | string | Raw internal type. One of pulse, paid_initial, paid_scheduled, console_on_demand, free_audit. |
measurement_scope | string | daily_monitoring (when scan_type is pulse) or full_benchmark (for all other values). |
measurement_label | string | Human label: Daily Monitoring or Full Benchmark. |
visibility_score | integer | Visibility Score at the end of the scan. |
started_at | ISO 8601 | When the scan started. |
completed_at | ISO 8601 | When the scan finished. |
results_summary.total_cost_usd | float | Internal cost of the scan in USD. |
results_summary.duration_seconds | float | Wall-clock duration. |
results_summary.citation_rate | float | Brand Coverage percentage. |
results_summary.total_citations | integer | Total citations across all platforms. |
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.
| Field | Type | Meaning |
|---|---|---|
scan_id | string | Unique scan id. |
domain_id | string | Domain the scan ran for. |
domain | string | Domain name. |
scan_type | string | Raw internal type (see scan.completed). |
error_summary | string | First 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.
| Field | Type | Meaning |
|---|---|---|
scan_id | string | Unique scan id. |
domain_id | string | Domain the scan ran for. |
domain | string | Domain name. |
scan_type | string | Raw internal type. |
phase | string | Current scan phase (for example testing_platforms). |
percentage | integer | 0 to 100 progress. |
platforms | array | Optional 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.
| Field | Type | Meaning |
|---|---|---|
alert_id | string | Alert id. |
alert_type | string | One of: first_scan_complete, visibility_drop, citation_rate_change, competitor_surpass, platform_drop, new_competitor, lens_emerged, lens_decayed. |
severity | string | info, warning, or critical. |
title | string | Human-readable alert title. |
summary | string | Short prose summary of what changed. |
domain_id | string | Domain the alert is scoped to. |
scan_id | string | Scan id that produced the alert (nullable for non-scan alerts). |
details | object | Type-specific context (numeric deltas, platform key, competitor domain, lens slug, etc.). |
citation.changed
Fires once per scan completion when the total citation count for a domain changes versus the previous comparable scan.
| Field | Type | Meaning |
|---|---|---|
scan_id | string | Current scan id. |
previous_scan_id | string | Scan id used as the comparison anchor. |
domain_id | string | Domain id. |
domain | string | Domain name. |
previous_count | integer | Citation count on the previous comparable scan. |
new_count | integer | Citation count on the current scan. |
delta | integer | new_count - previous_count. |
scan_type | string | Raw internal type of the current scan. |
measurement_scope | string | daily_monitoring or full_benchmark. |
measurement_label | string | Daily Monitoring or Full Benchmark. |
previous_measurement_scope | string | Scope of the comparison anchor. |
comparison_scope | string | same_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.
| Field | Type | Meaning |
|---|---|---|
domain_id | string | Newly created domain id. |
domain | string | Domain name. |
source | string | dashboard 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.
| Field | Type | Meaning |
|---|---|---|
competitor_id | string | Competitor row id. |
competitor_domain | string | Root domain of the competitor. |
domain_id | string | Tracked domain the competitor was discovered for. |
domain | string | Domain name. |
discovery_source | string | Internal source key (for example ai_responses). |
report.ready
Fires when a generated report is ready to fetch.
| Field | Type | Meaning |
|---|---|---|
report_id | string | Report id. |
domain_id | string | Domain the report covers. |
title | string | Report title. |
audience_preset | string | Audience preset used to generate the report. |
generated_by | string | manual 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.