Webhooks
Receive real-time message and session events from Sendable.
How Webhooks Work
- You configure a webhook URL in your session settings.
- Sendable sends HTTP POST requests to your URL when subscribed events occur.
- Your application verifies the Svix signature, processes the event, and returns HTTP 200 quickly.
Setting Up Webhooks
1. Create an Endpoint
Your endpoint must:
- Accept POST requests
- Preserve the raw request body for signature verification
- Return HTTP 200 quickly, ideally in under 5 seconds
- Handle duplicate events because delivery is at-least-once
Example in Express:
import express from 'express'
import { Sendable } from '@sendable-dev/sdk'
const sendable = new Sendable({
apiKey: process.env.SENDABLE_API_KEY!,
apiKeyScope: 'session',
webhookSecret: process.env.SENDABLE_WEBHOOK_SECRET,
})
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const payload = req.body.toString()
let event
try {
event = await sendable.webhooks.verifyAndParse(payload, req.headers)
} catch {
return res.status(401).send('Invalid signature')
}
processEvent(event).catch(console.error)
res.sendStatus(200)
})2. Configure in Dashboard
- Go to your session
- Navigate to
Webhooks - Click
Add Webhook - Enter your URL
- Select event types
- Save
3. Keep the Secret Safe
Each webhook endpoint has a Svix signing secret in the form whsec_.... Store it securely and pass it into the SDK as webhookSecret.
Event Types
Message Events
| Event | Description |
|---|---|
message.sent | Message queued for delivery |
message.delivered | Message delivered to recipient |
message.read | Recipient read the message |
message.failed | Message delivery failed |
message.received | Incoming direct message received |
message-personal.received | Incoming personal-chat message received |
message-group.received | Incoming group or community message received |
Session Events
| Event | Description |
|---|---|
session.socket-connected | WhatsApp socket connected successfully |
session.socket-disconnected | WhatsApp socket disconnected |
See Webhook Events for payload examples.
Security
Verify Svix Signatures
Sendable delivers webhooks with Svix headers:
svix-idsvix-timestampsvix-signature
Professional and Enterprise setups may use white-labeled webhook-* header names. The SDK accepts both forms.
The safest path is to verify and parse in one step:
const event = await sendable.webhooks.verifyAndParse(payload, req.headers)If you want to separate verification from parsing, use:
await sendable.webhooks.assertSignature(payload, req.headers)
const event = JSON.parse(payload)Use HTTPS
Always use HTTPS for production webhook endpoints.
Best Practices
Respond Quickly
Do the verification work inline, then hand off processing:
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const payload = req.body.toString()
const event = await sendable.webhooks.verifyAndParse(payload, req.headers)
queueWebhook(event).catch(console.error)
res.sendStatus(200)
})Handle Retries
Webhook delivery is at-least-once. If your endpoint fails or times out, Sendable will retry.
Use Idempotency
Use a stable key built from the envelope:
async function processEvent(event) {
const id = event.data.id ?? event.data.sessionId
const idempotencyKey = `${event.type}-${id}-${event.createdAt}`
if (await alreadyProcessed(idempotencyKey)) {
return
}
await handleEvent(event)
}Log the Envelope
console.log('webhook', {
type: event.type,
createdAt: event.createdAt,
id: event.data.id ?? event.data.sessionId ?? null,
})Troubleshooting
Invalid Signature
- Confirm you are passing the raw request body, not
JSON.stringify(req.body) - Confirm
SENDABLE_WEBHOOK_SECRETmatches the endpoint secret from the dashboard - Confirm your framework has not consumed or transformed the body before verification
Missing Events
- Confirm the webhook is active
- Confirm the subscribed event type matches the event you expect
- Check the delivery logs in the dashboard
Timeouts
- Return HTTP 200 immediately after verification
- Move heavy work into a queue or background job