Webhooks
Receive real-time HTTP callbacks when events occur in your JoltSMS account. Webhooks are signed with HMAC-SHA256 for security and delivered with automatic retries.
Event Format
All webhook payloads follow the Jolt Events v1 format. Each event is a JSON object with a consistent structure:
{
"id": "evt_abc123",
"type": "sms.received",
"createdAt": "2025-01-15T10:30:00.000Z",
"accountId": "user_xyz789",
"numberId": "num_abc456",
"data": {
// Event-specific payload
}
}| Field | Type | Description |
|---|---|---|
| id | string | Unique event identifier. Use this for idempotency. |
| type | string | Event type in dot notation (e.g., sms.received). |
| accountId | string | The account (user) ID that owns the resource. |
| numberId | string? | The number ID, if the event is related to a specific number. |
| createdAt | string | ISO 8601 timestamp of when the event was generated. |
| data | object | Event-specific payload. Contents vary by event type. |
Event Types
JoltSMS delivers the following event types. You can subscribe to specific events when configuring your webhook endpoint.
| Event | Description | Data Fields |
|---|---|---|
| sms.received | New SMS message received on one of your numbers | messageId, from, to, body, otp, receivedAt |
| number.expiring.7d | Number expiring in 7 days | numberId, phoneNumber, expiresAt, daysUntilExpiry |
| number.expiring.1d | Number expiring in 1 day | numberId, phoneNumber, expiresAt, daysUntilExpiry |
| billing.issue | Payment failed or action required | invoiceId, subscriptionId, amount, currency, reason, attemptCount, nextRetryAt |
Example: sms.received payload
{
"id": "evt_sms_abc123",
"type": "sms.received",
"createdAt": "2025-01-15T10:30:00.000Z",
"accountId": "user_xyz789",
"numberId": "num_abc456",
"data": {
"messageId": "msg_def789",
"from": "+18005559876",
"to": "+16505551234",
"body": "Your verification code is 847291",
"otp": "847291",
"receivedAt": "2025-01-15T10:29:58.000Z"
}
}Signature Verification
JoltSMS signs every webhook delivery with HMAC-SHA256. The signature is included in the X-Jolt-Signature header (prefixed with v1=). A X-Jolt-Timestamp header contains the Unix millisecond timestamp used in signing. Your webhook secret is available in Dashboard → Notifications → Webhook endpoint.
Node.js verification example
import crypto from 'crypto';
function verifyWebhook(body, signature, timestamp, secret) {
// Signature format: "v1=<hex>"
if (!signature.startsWith('v1=')) return false;
const provided = signature.slice(3);
// Signing input is "timestamp.body"
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(provided, 'hex'),
Buffer.from(expected, 'hex')
);
}
// In your Express/Fastify handler:
const body = JSON.stringify(req.body);
const signature = req.headers['x-jolt-signature'];
const timestamp = req.headers['x-jolt-timestamp'];
// Reject requests older than 5 minutes
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) {
return res.status(401).json({ error: 'Timestamp too old' });
}
if (!verifyWebhook(body, signature, timestamp, process.env.JOLT_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature valid -- process the event
const event = req.body;
console.log(`Received ${event.type}: ${event.id}`);Python verification example
import hmac
import hashlib
def verify_webhook(body: str, signature: str, timestamp: str, secret: str) -> bool:
if not signature.startswith("v1="):
return False
provided = signature[3:] # Remove "v1=" prefix
# Signing input is "timestamp.body"
signing_input = f"{timestamp}.{body}".encode()
expected = hmac.new(
secret.encode(),
signing_input,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(provided, expected)Always verify signatures
Never process webhook payloads without verifying the HMAC-SHA256 signature first. Unsigned or tampered payloads should be rejected with a 401 response. Use a constant-time comparison function (such as timingSafeEqual or hmac.compare_digest) to prevent timing attacks.
Retry Policy
If your endpoint returns a non-2xx HTTP status code (or the connection times out), JoltSMS retries the delivery with exponential backoff.
| Attempt | Delay After Failure |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 2 minutes |
| 3rd retry | 4 minutes |
| 4th retry | 8 minutes |
| 5th retry | 16 minutes |
| 6th retry | 32 minutes |
| 7th retry (final) | ~1 hour |
After 8 total attempts (1 initial + 7 retries), the event is moved to a dead letter queue (DLQ). Delays use exponential backoff starting from 60 seconds. You can view and replay failed deliveries in Dashboard → Notifications → Delivery Logs.
Your endpoint should respond within 10 seconds. If processing takes longer, return a 200 immediately and handle the event asynchronously. A slow response is treated the same as a timeout and will trigger a retry.
Endpoint Setup
Configure webhook endpoints from the JoltSMS dashboard in a few steps:
- 1
Navigate to Dashboard → Notifications → Add Endpoint and select Webhook.
- 2
Enter your HTTPS endpoint URL. We recommend a dedicated path such as
/webhooks/joltsms. - 3
Select which event types you want to receive. You can subscribe to all events or only specific ones.
- 4
Copy your webhook secret for signature verification. Store it securely in your server's environment variables.
- 5
Use the Test button to send a test event and confirm your endpoint is receiving and processing payloads correctly.
Webhook endpoints must use HTTPS. HTTP URLs are rejected during configuration. We recommend a dedicated endpoint path like /webhooks/joltsms to keep your webhook handler isolated from the rest of your application.
Best Practices
Return 200 quickly, process asynchronously
Acknowledge receipt with a 200 OK response as fast as possible. Enqueue the event for background processing rather than doing work inline. Slow responses trigger retries.
Handle duplicate events idempotently
Retries may deliver the same event more than once. Use the id field to deduplicate. Store processed event IDs and skip any that have already been handled.
Verify signatures before processing
Always check the X-Jolt-Signature header before acting on any event data. Reject requests with missing or invalid signatures.
Log raw payloads for debugging
Store the raw request body and headers before processing. This makes it straightforward to diagnose issues and replay events manually if needed.
Monitor your delivery logs
Check Dashboard → Notifications → Delivery Logs regularly for failed deliveries. Set up alerts if your failure rate increases -- this usually indicates an endpoint issue on your side.