Webhooks let your server receive a real-time HTTP callback whenever a configured Agent event occurs (a prompt is submitted, a CTA is clicked, a guardrail trips, and more). Each webhook notification sends an HTTP POST with a JSON body to the URL you provide in Apologist Ignite.
This page is for developers receiving these webhooks: it describes delivery, the payload shape, security modes, and how to verify signatures. Agents can also send email and Slack notifications for the same events; those use the same payload data but different transports and are not covered in detail here.
Configuration #
Webhook notifications are configured per Agent in Apologist Ignite (Notifications). You choose:
- Which events to subscribe to
- The destination URL
- An optional secret, and whether to use signature mode (
sign_request) or shared-secret mode
Webhook delivery does not use your Fusion API key. Securing the endpoint is your responsibility via the modes below. For inbound Fusion API calls, see Authentication on the Overview page.
Delivery #
- Method:
POST - Content-Type:
application/json - Body: the canonical JSON payload described below (sent verbatim)
- A
2xxresponse is treated as success. Non-2xxresponses or connection errors are logged on our side; there is currently no automatic retry, so acknowledge quickly and do heavy work asynchronously.
Headers #
| Header | When sent | Description |
|---|---|---|
Content-Type | always | application/json |
X-Webhook-Secret | shared-secret mode only | The plaintext shared secret you configured |
X-Webhook-Timestamp | signature mode only | Unix time in seconds at which the request was signed |
X-Webhook-Signature | signature mode only | sha256=<hex> — the HMAC-SHA256 signature |
Payload Structure #
The body is a JSON object. notification, event, and agent are always present; the remaining sections appear only when relevant to the event.
{
"notification": { "id": 17, "name": "Prod Hook" },
"event": {
"key": "cta_click",
"label": "Call to Action Click",
"occurred_at": "2026-06-14T20:54:37.123Z"
},
"agent": { "id": 7, "name": "Example Agent" },
"completion": {
"id": "11111111-1111-1111-1111-111111111111",
"prompt": "…",
"response": "…",
"language": "en",
"liked": null,
"flagged": false,
"cached": false,
"client": "api",
"user_id": "crm-123",
"device_id": null,
"session_id": null,
"conversation_id": "conv-1",
"prompted_at": "…",
"response_started_at": "…",
"response_completed_at": "…",
"response_duration": 1.234,
"automations": [
{
"id": "22222222-2222-2222-2222-222222222222",
"evaluator_id": 2,
"evaluator_name": "Relevance",
"agent_id": 7,
"benchmark_run_id": null,
"benchmark_question_id": null,
"content": "…",
"started_at": "2026-06-14T20:54:38.000Z",
"completed_at": "2026-06-14T20:54:39.100Z",
"score": 0.92,
"passed": true,
"reasoning": "…",
"input_tokens": 120,
"output_tokens": 40,
"reasoning_tokens": null,
"evaluator_option_id": 55,
"responder_id": null,
"tag_id": 3
}
],
"tags": [
{ "id": 3, "name": "Praise" }
]
},
"channel": { "id": 3, "name": "SMS" },
"platform": { "id": 1, "name": "Twilio" },
"cta": { "id": 9, "name": "Buy", "content": "…" },
"guardrail": { "id": 4, "name": "Off-topic" },
"evaluator": { "id": 2, "name": "Relevance" },
"evaluation": { "score": 1, "passed": true, "content": "…" }
}
| Section | Included when |
|---|---|
notification | Always — the notification’s id and name |
event | Always — key, human-readable label, ISO-8601 occurred_at |
agent | Always |
completion | The event is tied to a prompt/completion (includes automations and tags when available, plus timing and identity fields) |
channel / platform | The completion arrived via a channel (e.g. SMS, Discord) |
cta | cta_trigger, cta_click |
guardrail | guardrail_trigger |
evaluator / evaluation | CTA or guardrail events that ran an evaluation |
Treat the payload as additive: new keys may be introduced over time. Ignore fields you do not recognize rather than failing the request.
Event keys #
| Key | Label |
|---|---|
prompt_submit | Prompt Submit |
response_start | Response Start |
response_end | Response Complete |
automations_end | Automations Complete |
response_like | Response Like |
response_flag | Response Flag |
response_feedback | Response Feedback |
referral_click | Source Referral Click |
cta_trigger | Call to Action Trigger |
cta_click | Call to Action Click |
guardrail_trigger | Guardrail Trigger |
attribution_click | Attribution Click |
footer_click | Footer Click |
new_user | New User |
new_device | New Device |
new_session | New Session |
new_conversation | New Conversation |
error | Error |
Security Modes #
Each webhook notification is configured with an optional secret and a sign_request flag that selects how that secret is used.
1. Shared-secret mode (default) #
When sign_request is false (the default) and a secret is set, the plaintext secret is sent in the X-Webhook-Secret header. Verify by comparing it to your stored secret using a constant-time comparison.
This mode is simple but places the secret on the wire with every request. Prefer signature mode for new integrations.
2. Signature mode (HMAC-SHA256) — recommended #
When sign_request is true and a secret is set, the secret never leaves our servers. Instead we send:
X-Webhook-Timestamp: <unix-seconds>X-Webhook-Signature: sha256=<hex>
The signature is the lowercase-hex HMAC-SHA256 of the string ${timestamp}.${rawBody} (the timestamp, a literal ., then the exact raw request body), keyed by your secret:
signature = HEX( HMAC_SHA256( secret, timestamp + "." + rawBody ) )
Binding the timestamp into the signed string lets you reject replayed requests.
Verifying the Signature #
- Read the raw request body as bytes/text before JSON parsing — you must sign the exact bytes you received, not a re-serialized object.
- Read
X-Webhook-Timestampand reject the request if it is too old (e.g. more than 5 minutes from now) to mitigate replay attacks. - Compute
HMAC_SHA256(secret, timestamp + "." + rawBody)and hex-encode it. - Compare
sha256=<computed>to theX-Webhook-Signatureheader using a constant-time comparison. Reject on mismatch. - Only after the signature is valid should you parse and act on the JSON.
Best Practices #
- Always verify the signature (or shared secret) before trusting a payload.
- Sign over the raw request bytes — never a re-serialized object.
- Use a constant-time comparison (
crypto.timingSafeEqual,hmac.compare_digest) to avoid timing attacks. - Enforce a timestamp tolerance and, ideally, de-duplicate on a value you persist to defend against replays.
- Respond quickly with a
2xx; offload slow processing to a queue. - Keep your secret confidential and rotate it if you suspect exposure.