Webhooks
Receive campaign alert events at your own HTTPS endpoint — the exact payload, delivery, and retry behavior.
iSearchFrom can POST an event to an endpoint you control whenever a tracking campaign raises an
alert. This page covers that campaign alert webhook. Use it to route alerts into an incident
tool, an internal dashboard, or a data warehouse. For Slack, Microsoft Teams, Google Chat, Discord or
Telegram, connect the chat directly instead — see Alert Channels.
There are two ways to receive the same payload:
- A campaign’s webhook URL — set in one campaign’s alert settings. Available on every plan.
- A webhook destination — saved once under Settings → Alerts & channels and used by any number of campaigns through routing. Requests are signed and carry an idempotency key (see Verify signatures). Available on plans with chat and webhook destinations.
To receive exported data — rankings, ads, AI visibility, search history and more — as signed batches on a schedule, use an export webhook destination instead.
When to use webhooks
Section titled “When to use webhooks”- Alerts need to enter an internal tool, Slack channel, or incident workflow.
- You want to store alert events in a data warehouse.
- Another workflow should run when a campaign’s rankings, ads, or AI visibility change.
For a complete walkthrough including a relay to Slack, see the cookbook Send alerts to Slack.
Configure a webhook
Section titled “Configure a webhook”- Open the campaign’s alert settings (in the app, or via the campaign’s
alertSettingsthrough the API). - Enable alerts and set a public HTTPS endpoint as the webhook URL.
- Save the campaign.
- Trigger a low-risk alert to confirm delivery before relying on it.
The endpoint must accept POST and return a 2xx quickly. If you need slow processing, acknowledge
fast and process asynchronously.
Payload shape
Section titled “Payload shape”Every delivery is JSON with this exact shape:
{ "event": "tool.alert.created", "timestamp": "2026-07-07T14:05:00.000Z", "data": { "alertId": "…", "targetType": "serp_position_campaigns", "campaignId": "…", "campaignName": "Brand keywords — US", "type": "…", "severity": "…", "metadata": { "…": "…" } }}| Field | Notes |
|---|---|
event | tool.alert.created for alerts (Send test posts notification.test). Branch on it; new event types may be added. |
timestamp | ISO-8601 time the alert was raised; the same on every retry, so it can be older than the delivery. Check freshness with the signature’s t, not this. |
data.alertId | Unique alert id — use it for idempotency (see below). |
data.targetType | Which tool’s campaign raised it — see the values below. |
data.campaignId | The campaign that raised the alert. |
data.campaignName | The campaign’s name, for display. |
data.type | The alert rule that fired. |
data.severity | The alert’s severity. |
data.metadata | Alert-type-specific detail (varies by type). |
data.targetType is one of serp_position_campaigns (SERP Position Tracker),
serp_ad_tracker_campaigns (Ads Position Tracker), ai_visibility_campaigns (AI Visibility),
serp_maps_campaigns (Maps Visibility) and marketplace_campaigns (Marketplace).
Handle unknown fields safely — new fields may be added without changing existing ones.
Delivery behavior
Section titled “Delivery behavior”- Retries: a failed delivery is retried with exponential backoff. For a campaign’s webhook URL
that is 4 attempts in all, waiting about 5s, 10s and 20s between them, so an endpoint that is
down for longer than about half a minute misses the alert; a
4xxother than408or429, or a redirect, is not retried at all. For a webhook destination it is 4 attempts over several minutes, and a429waits for yourRetry-After. After that it’s given up. - Destination health: a webhook destination that answers
404/410/401/403or redirects three times in a row (spread over about a quarter of an hour), or has five deliveries in a row given up on, is set aside as Needs attention; the organization’s owners and the person who set it up get an email and an in-app notice. Other4xxanswers fail that one alert, and five such failures in a row set the destination aside too. An alert not delivered within 24 hours is dropped. - Idempotency: the same alert can arrive more than once (a retry after your endpoint was briefly
slow). De-duplicate on
data.alertIdif duplicates would matter. - Response: return
2xxonly once you’ve accepted the event. Use5xx,429or a timeout for temporary failures (which trigger a retry). For a campaign’s own webhook URL, any other4xxends that delivery: it is not retried. A webhook destination that answers401,403,404,410or a redirect is retried instead (see Destination health above). - Keep responses small — iSearchFrom does not surface your endpoint’s response anywhere.
Verify signatures
Section titled “Verify signatures”Requests to a webhook destination carry two extra headers:
| Header | Value |
|---|---|
ISF-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, keyed with the signing secret |
Idempotency-Key | Stable per alert and destination — the same value on every retry and resend |
When support sends a failed delivery again, it carries the same Idempotency-Key: a receiver that
de-duplicates on it drops the resend as a repeat.
The signing secret is shown once, when the destination is created. New signing secret on the destination replaces it: the old one stops working at once. To verify a request:
- Split
ISF-Signatureon,intotandv1. - Compute HMAC-SHA256 of
`${t}.${rawBody}`with your secret, as hex. Use the raw request body, before any JSON parsing. - Compare it with
v1in constant time, and reject atmore than a few minutes old.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody: string, header: string, secret: string, toleranceS = 300): boolean { const parts = Object.fromEntries(header.split(',').map((part) => part.split('='))); const timestamp = Number(parts.t); if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceS) { return false; } const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex'); const given = Buffer.from(parts.v1 ?? '', 'hex'); const wanted = Buffer.from(expected, 'hex'); return given.length === wanted.length && timingSafeEqual(given, wanted);}Send test in the app posts {"event":"notification.test", …} — branch on event and answer
2xx.
A campaign’s own webhook URL is not signed; put a token in your URL if you need to authenticate it.
Security recommendations
Section titled “Security recommendations”- Use HTTPS and a unique URL per integration so you can rotate or revoke one independently.
- The webhook URL is effectively a secret — treat a leaked one as compromised and rotate it.
- If your receiver needs to authenticate the caller, put a secret token in your own URL path/query or use a gateway rule. Never put API keys in the webhook URL.
- Don’t put secrets in campaign names or alert labels — they appear in the payload.