Webhooks
Send events to your own systems as they happen: the event list, the payload, the signature header, and the retry behaviour.
A webhook pushes an event to your endpoint as it happens, so your own systems can react without polling.
What you'll learn
- The events you can subscribe to
- How a delivery is signed, and how to verify it
- Retry behaviour and what to build for
Create one
- Open API settings.
- Add an endpoint URL and choose the events. Leave the event list empty to receive all of them.
- Save, and keep the secret. It is what signs every delivery.
- Send a test delivery.
Webhooks are a plan feature. See plans and limits.
The events
| Event | Fires when |
|---|---|
citation.gained | A prompt starts being cited |
citation.lost | A prompt stops being cited |
citation.negative | An answer mentions you negatively |
competitor.detected | A new competitor appears in your answers |
insight.generated | An insight is produced |
action.created | An action is created |
brief.generated | An agent brief is produced |
score.changed | A headline score moves |
prediction.completed | A forecast run finishes |
recommendation.created | A change is proposed |
recommendation.generated | A change is generated |
recommendation.critical | A change is flagged critical |
recommendation.actioned | A change is approved or actioned |
recommendation.dismissed | A change is dismissed |
agent.episode.completed | An agent run finishes |
agent.alert.dispatched | An agent alert is sent |
pipeline.first_ai_visit | The first AI-sourced visit is seen for a prompt |
pipeline.conversion_attributed | A conversion is attributed to an AI referral |
pipeline.impact_measured | An impact measurement lands |
coverage.exhausted | Your coverage wallet emptied and work paused |
coverage.not_allocated | A coverage lane has no allocation at all, which is a billing fault on our side |
The last two are separate on purpose. Exhaustion means you spent what you bought. Not allocated means something went wrong before you could spend anything, and it deserves a different alarm.
The delivery
A POST with a JSON body and three headers:
| Header | Contents |
|---|---|
X-Signature-256 | sha256= followed by the HMAC-SHA256 of the raw body, keyed with your secret |
X-Webhook-Event | The event type |
X-Webhook-Delivery | A unique id for this delivery |
Verify the signature
Compute the HMAC-SHA256 of the raw body, not of a re-serialised object, and compare with the header value after the
sha256= prefix. Use a constant-time comparison. Reject anything that does not match.
import crypto from 'crypto'
function verify(rawBody, header, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(header ?? '')
const b = Buffer.from(expected)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Retries and timeouts
- A delivery is attempted up to three times, with exponential backoff starting at five seconds.
- Each attempt has a ten second timeout, so answer fast and do the work afterwards.
- Return a 2xx as soon as you have the payload safely stored. Anything else is treated as a failure and retried.
Build your handler to be idempotent: use X-Webhook-Delivery as the deduplication key, since a retry after a timeout can
deliver an event your system already processed.
Endpoint requirements
Your URL must be publicly reachable over HTTPS. Deliveries go through a fetch that rejects private address ranges and revalidates redirects, so an endpoint on a private network or behind a redirect to one will not receive anything.
Try this in Outercite
Create an endpoint subscribed to citation.lost and coverage.exhausted only. Those two carry the events that should
wake up a system rather than a person.
Related
API access
Keys, permissions and the endpoints.
Set up alerts
The same events, delivered to people instead.
Coverage and credits
What the two coverage events mean.
Last updated on
