Webhooks
Receive real-time SMS delivery events via webhooks.
Overview
Webhooks allow you to receive real-time notifications when SMS events occur, such as delivery confirmations, failures, and inbound messages.
Setting Up Webhooks
Via Dashboard
- Go to SMS > Webhooks in your dashboard
- Click Add Webhook
- Enter your endpoint URL
- Select the events you want to receive
- Save the webhook
Via API
const webhook = await client.sms.webhooks.create({
url: 'https://your-app.com/webhooks/sms',
events: ['sms.sent', 'sms.delivered', 'sms.failed', 'sms.inbound'],
});Webhook Events
Delivery Events
| Event | Description |
|---|---|
sms.queued | Message accepted and queued |
sms.sent | Message sent to carrier |
sms.delivered | Message delivered to recipient |
sms.failed | Delivery failed |
sms.undelivered | Carrier unable to deliver |
Opt-Out Events
| Event | Description |
|---|---|
sms.opt_out | Recipient opted out |
sms.opt_in | Recipient opted back in |
Inbound Events
| Event | Description |
|---|---|
sms.inbound | Incoming message received |
Webhook Payload
Delivery Event Example
{
"event": "sms.delivered",
"timestamp": "2024-01-01T12:00:00.000Z",
"webhookId": "wh_abc123",
"data": {
"messageId": "sms_abc123",
"from": "+18005551234",
"to": "+14155551234",
"status": "DELIVERED",
"direction": "OUTBOUND",
"segments": 1,
"country": "US",
"tag": "verification",
"metadata": {
"userId": "user_123"
},
"queuedAt": "2024-01-01T11:59:58.000Z",
"sentAt": "2024-01-01T11:59:59.000Z",
"deliveredAt": "2024-01-01T12:00:00.000Z"
}
}Failed Event Example
{
"event": "sms.failed",
"timestamp": "2024-01-01T12:00:00.000Z",
"webhookId": "wh_abc123",
"data": {
"messageId": "sms_def456",
"from": "+18005551234",
"to": "+14155551234",
"status": "FAILED",
"errorCode": 30003,
"errorMessage": "Unreachable destination handset",
"queuedAt": "2024-01-01T11:59:58.000Z",
"failedAt": "2024-01-01T12:00:00.000Z"
}
}Inbound Event Example
{
"event": "sms.inbound",
"timestamp": "2024-01-01T12:30:00.000Z",
"webhookId": "wh_abc123",
"data": {
"messageId": "sms_inb_ghi789",
"from": "+14155551234",
"to": "+18005551234",
"body": "Thanks for the update!",
"receivedAt": "2024-01-01T12:30:00.000Z",
"providerMessageId": "SM12345"
}
}Opt-Out Event Example
{
"event": "sms.opt_out",
"timestamp": "2024-01-01T12:35:00.000Z",
"webhookId": "wh_abc123",
"data": {
"phoneNumber": "+14155551234",
"phoneNumberHash": "abc123...",
"keyword": "STOP",
"optedOutAt": "2024-01-01T12:35:00.000Z"
}
}Handling Webhooks
Express.js Example
import express from 'express';
import crypto from 'crypto';
const app = express();
// Verify webhook signature
function verifySignature(payload: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.post('/webhooks/sms', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'] as string;
const payload = req.body.toString();
// Verify signature
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
switch (event.event) {
case 'sms.delivered':
handleDelivered(event.data);
break;
case 'sms.failed':
handleFailed(event.data);
break;
case 'sms.inbound':
handleInbound(event.data);
break;
case 'sms.opt_out':
handleOptOut(event.data);
break;
}
res.status(200).send('OK');
});
function handleDelivered(data: any) {
console.log(`Message ${data.messageId} delivered to ${data.to}`);
// Update your database, notify user, etc.
}
function handleFailed(data: any) {
console.log(`Message ${data.messageId} failed: ${data.errorMessage}`);
// Alert team, retry logic, etc.
}
function handleInbound(data: any) {
console.log(`Received SMS from ${data.from}: ${data.body}`);
// Process reply, update conversation, etc.
}
function handleOptOut(data: any) {
console.log(`User ${data.phoneNumber} opted out`);
// Update user preferences, stop marketing, etc.
}Webhook Security
Signature Verification
All webhooks include an X-Signature header containing an HMAC-SHA256 signature of the payload:
const signature = crypto
.createHmac('sha256', webhookSecret)
.update(requestBody)
.digest('hex');Always verify signatures to ensure webhooks are from Transactional.
IP Allowlisting
You can allowlist our webhook IPs for additional security. Contact support for our current IP ranges.
Retry Logic
Failed webhook deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the webhook is marked as failed.
Response Requirements
Your endpoint must:
- Return 2xx status code within 30 seconds
- Accept POST requests with JSON body
- Be publicly accessible (no authentication)
Testing Webhooks
Test Endpoint
Use a tool like webhook.site or ngrok to test locally.
Dashboard Testing
- Go to your webhook settings
- Click Send Test Event
- Select an event type
- View the test payload and response
Manual Testing
const result = await client.sms.webhooks.test(webhookId, {
event: 'sms.delivered',
});
console.log('Test result:', result.success);Webhook Management
List Webhooks
const webhooks = await client.sms.webhooks.list();Update Webhook
await client.sms.webhooks.update(webhookId, {
url: 'https://new-url.com/webhooks/sms',
events: ['sms.delivered', 'sms.failed'],
});Delete Webhook
await client.sms.webhooks.delete(webhookId);Disable/Enable Webhook
// Disable
await client.sms.webhooks.update(webhookId, { isActive: false });
// Enable
await client.sms.webhooks.update(webhookId, { isActive: true });Best Practices
1. Respond Quickly
Return 200 immediately, then process asynchronously:
app.post('/webhooks/sms', (req, res) => {
// Queue for async processing
queue.add('process-sms-webhook', req.body);
// Return immediately
res.status(200).send('OK');
});2. Handle Duplicates
Webhooks may be delivered more than once. Use messageId for idempotency:
async function handleDelivered(data: any) {
// Check if already processed
const existing = await db.query.smsEvents.findFirst({
where: eq(smsEvents.messageId, data.messageId),
});
if (existing) {
return; // Already processed
}
// Process event
await processDeliveryEvent(data);
}3. Log Everything
Store webhook payloads for debugging:
await db.insert(webhookLogs).values({
event: event.event,
payload: event,
receivedAt: new Date(),
});Next Steps
- Suppressions - Manage opt-out lists
- API Reference - Complete API documentation
- FAQ - Common questions
On This Page
- Overview
- Setting Up Webhooks
- Via Dashboard
- Via API
- Webhook Events
- Delivery Events
- Opt-Out Events
- Inbound Events
- Webhook Payload
- Delivery Event Example
- Failed Event Example
- Inbound Event Example
- Opt-Out Event Example
- Handling Webhooks
- Express.js Example
- Webhook Security
- Signature Verification
- IP Allowlisting
- Retry Logic
- Response Requirements
- Testing Webhooks
- Test Endpoint
- Dashboard Testing
- Manual Testing
- Webhook Management
- List Webhooks
- Update Webhook
- Delete Webhook
- Disable/Enable Webhook
- Best Practices
- 1. Respond Quickly
- 2. Handle Duplicates
- 3. Log Everything
- Next Steps