Webhooks
Receive real-time HTTP callbacks when events happen in your account.
Setup
Configure your webhook endpoint in Settings → Webhooks within the app. You provide a URL and receive a signing secret. Webhooks are available on Growth and Pro plans.
All payloads are sent as POST requests
with Content-Type: application/json.
Headers
| X-Webhook-Signature | HMAC-SHA256 signature of the raw request body |
| X-Webhook-Event | The event type (e.g. prompt.run.completed) |
Retries
Failed deliveries are retried up to 3 times with increasing backoff: 10 seconds, 60 seconds, 5 minutes. A delivery is considered failed if your endpoint returns a non-2xx status or times out after 10 seconds.
Signature verification
Verify the X-Webhook-Signature header
to confirm the payload came from Cited Monitor. Compute an HMAC-SHA256 of the raw request body
using your signing secret and compare it to the header value.
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your Express handler:
app.post('/webhooks/citedmonitor', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhook(req.rawBody, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
// Handle event...
res.status(200).send('OK');
});
import hmac
import hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $payload, $webhookSecret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
Events
All payloads share the same envelope structure.
{
"event": "event.type",
"timestamp": "2026-06-13T14:22:03.000000Z",
"data": { ... }
}
prompt.run.completed
Fired when a prompt run finishes successfully and results have been processed.
Data fields
| prompt_run_id | integer | The completed run ID |
| prompt_id | integer | The prompt that was run |
| entity_id | integer | The brand being monitored |
| provider | string | Provider slug (e.g. openai) |
| model | string | Model used (e.g. gpt-4o) |
| entity_count | integer | Number of entities extracted |
| citation_count | integer | Number of citations found |
{
"event": "prompt.run.completed",
"timestamp": "2026-06-13T14:22:03.000000Z",
"data": {
"prompt_run_id": 91,
"prompt_id": 12,
"entity_id": 1,
"provider": "openai",
"model": "gpt-4o",
"entity_count": 7,
"citation_count": 2
}
}
prompt.run.failed
Fired when a prompt run fails due to an API error, invalid key, rate limit, etc.
Data fields
| prompt_run_id | integer | The failed run ID |
| prompt_id | integer | The prompt that was run |
| provider | string | Provider slug |
| model | string | Model used |
| error_type | string | auth_failure rate_limited provider_error unknown |
| error_message | string | Human-readable error description |
{
"event": "prompt.run.failed",
"timestamp": "2026-06-13T14:22:05.000000Z",
"data": {
"prompt_run_id": 92,
"prompt_id": 12,
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"error_type": "rate_limited",
"error_message": "Rate limit exceeded. Please try again in 30 seconds."
}
}
entity.new
Fired when a previously unseen brand or entity is discovered in an AI response.
Data fields
| entity_id | integer | The new entity ID |
| name | string | The entity name |
| entity_type | string | business product place person media |
| domain | string|null | Associated domain, if detected |
{
"event": "entity.new",
"timestamp": "2026-06-13T14:22:03.000000Z",
"data": {
"entity_id": 23,
"name": "Fischer's at Baslow Hall",
"entity_type": "business",
"domain": null
}
}