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

  1. Go to SMS > Webhooks in your dashboard
  2. Click Add Webhook
  3. Enter your endpoint URL
  4. Select the events you want to receive
  5. 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

EventDescription
sms.queuedMessage accepted and queued
sms.sentMessage sent to carrier
sms.deliveredMessage delivered to recipient
sms.failedDelivery failed
sms.undeliveredCarrier unable to deliver

Opt-Out Events

EventDescription
sms.opt_outRecipient opted out
sms.opt_inRecipient opted back in

Inbound Events

EventDescription
sms.inboundIncoming 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:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 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

  1. Go to your webhook settings
  2. Click Send Test Event
  3. Select an event type
  4. 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