Incoming Webhooks
Incoming webhooks are provider-to-Kravos.ai callbacks. They let external systems deliver events to the web app so Kravos.ai can verify them, normalize them, ...
Incoming Webhooks
Incoming webhooks are provider-to-Kravos.ai callbacks. They let external systems deliver events to the web app so Kravos.ai can verify them, normalize them, queue work, and update the database.
Not outbound event subscriptions
This page covers inbound provider webhooks already implemented in the web app. Customer-managed outbound webhook event subscriptions are currently unavailable.
Implemented Webhook Endpoints
| Endpoint | Provider | Main purpose | Verification implemented |
|---|---|---|---|
GET /api/webhooks/whatsapp | Meta WhatsApp | Webhook challenge | META_VERIFY_TOKEN challenge check |
POST /api/webhooks/whatsapp | Meta WhatsApp | Inbound channel messages | x-hub-signature-256 HMAC with META_APP_SECRET |
GET /api/webhooks/messenger | Meta Messenger | Webhook challenge | META_VERIFY_TOKEN challenge check |
POST /api/webhooks/messenger | Meta Messenger | Inbound messages and Page echoes | x-hub-signature-256 HMAC with META_APP_SECRET |
POST /api/webhooks/elevenlabs | ElevenLabs | Voice session events and transcripts | ElevenLabs SDK verification with ELEVENLABS_WEBHOOK_SECRET |
POST /api/webhooks/paddle | Paddle | Billing, subscription, and credit events | Paddle SDK verification with PADDLE_WEBHOOK_SECRET |
POST /api/webhooks/sendgrid | SendGrid | Email delivery log updates | Payload validation only |
Verification Details
Meta challenge requests
WhatsApp and Messenger use the same GET challenge contract.
hub.modemust besubscribe.hub.verify_tokenmust equalMETA_VERIFY_TOKEN.hub.challengemust be present.
Kravos.ai returns the challenge text directly on success. It returns 403 when the token does not match and 503 when the verify token is not configured.
Meta signed POST requests
WhatsApp and Messenger POST handlers:
- read the raw request body with
request.text() - read
x-hub-signature-256 - calculate
sha256=+ HMAC SHA-256 of the raw body usingMETA_APP_SECRET - compare the expected and provided digests with a timing-safe comparison
- parse JSON only after signature verification passes
Invalid signatures return:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid webhook signature"
}
}
The implemented Meta handlers do not enforce a timestamp replay window. Replay safety comes from provider message ID dedupe and database uniqueness checks.
ElevenLabs signed POST requests
The ElevenLabs route reads the raw body and accepts either x-elevenlabs-signature or elevenlabs-signature. It passes the body, signature, and ELEVENLABS_WEBHOOK_SECRET to the ElevenLabs SDK constructor.
Invalid signatures return 401 UNAUTHORIZED. Invalid event shape returns 400 VALIDATION_ERROR.
Paddle signed POST requests
The Paddle route reads the raw body, reads paddle-signature, and calls the Paddle SDK unmarshal helper with PADDLE_WEBHOOK_SECRET.
Invalid signatures return 400 INVALID_SIGNATURE. Unknown Paddle event types are acknowledged with 200 so Paddle does not keep retrying events the app intentionally ignores.
SendGrid delivery events
The SendGrid route validates that the request body is an array of SendGrid event objects, then updates email logs. It does not verify a SendGrid signature, so treat it as a delivery-log integration endpoint, not a general untrusted ingestion endpoint.
Replay And Duplicate Safety
| Provider/path | Implemented duplicate handling |
|---|---|
| Meta WhatsApp and Messenger | Redis key dedupe:{channel}:{providerMessageId} for 24 hours before queuing. |
| Channel message persistence | Database uniqueness on conversationId + channelMessageId. |
| Channel outbound replies | Redis key channel:outbound:{outboundId} for 7 days before sending again. |
| ElevenLabs | Event ID hash plus Redis key webhook:elevenlabs:event:{eventId} for 24 hours; BullMQ job ID also uses the event ID. |
| Paddle | Handlers check current database state and domain ledger uniqueness before writing. |
| SendGrid | Delivery log updates are applied per event; there is no route-level dedupe key. |
Secrets And Safe Handling
- Store webhook secrets in deployment secrets or environment variables, not in source control.
- Use different verify tokens and app secrets per environment.
- Rotate provider tokens when ownership changes or a secret may have leaked.
- Never send provider app secrets to clients. The Meta verify token is not the app secret.
- Preserve the raw request body for signature verification.
- Do not log raw webhook payloads or authorization headers in production.
Retries, Failures, And Dead Letters
Provider retry behavior depends on the provider. Kravos.ai should return 2xx only after it verifies and accepts the event.
Implemented queue behavior:
| Queue | Attempts | Backoff | Retention |
|---|---|---|---|
channel-inbound | 5 | Exponential, starts at 2 seconds | 100 completed, 500 failed |
channel-outbound | 3 | Exponential, starts at 2 seconds | 100 completed, 500 failed |
voice-webhook | 5 | Exponential, starts at 2 seconds | 100 completed, 500 failed |
There is no separate dead-letter queue. Failed jobs remain in BullMQ until the configured failed-job retention limit removes older entries. Worker failure events are logged, and final-failure admin email alerts can be sent when SendGrid email sending is configured.
Testing Incoming Webhooks
For Meta channels, prefer provider verification plus a real test message. If you need to test a signed POST manually, compute the signature over the exact body bytes you send.
Example signature shape:
x-hub-signature-256: sha256=<hex HMAC SHA-256 of raw body using META_APP_SECRET>
Expected success response for Meta channel events:
{
"data": {
"received": true,
"deduped": false,
"dedupedCount": 0,
"enqueued": 1
}
}
Troubleshooting
Verification succeeds but no message appears
- Confirm Redis is reachable so the handler can claim the dedupe key.
- Confirm the worker process is running.
- Confirm
channel-inboundjobs are not failing. - Confirm first-message routing has exactly one active tenant channel config for the channel.
Duplicate provider events appear
- Confirm provider message IDs are present in the payload.
- Confirm Redis is not flushing dedupe keys early.
- Check whether the duplicate was delivered after the 24-hour Meta dedupe window.
Provider keeps retrying
- Check whether the route is returning
401,403,400, or503. - Fix signature secrets before retrying. A successful retry with the same provider message ID should dedupe safely.
- For queued work failures, inspect BullMQ failed jobs rather than replaying the provider event blindly.
Related Docs
Channels
Understand channel routing, queue processing, and supported channel behavior.
WhatsApp Setup
Configure Meta WhatsApp Cloud API callbacks and replies.
Messenger Setup
Configure Facebook Messenger callbacks, Page replies, and echo events.


