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
  }
}
FieldTypeDescription
idstringUnique event identifier. Use this for idempotency.
typestringEvent type in dot notation (e.g., sms.received).
accountIdstringThe account (user) ID that owns the resource.
numberIdstring?The number ID, if the event is related to a specific number.
createdAtstringISO 8601 timestamp of when the event was generated.
dataobjectEvent-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.

EventDescriptionData Fields
sms.receivedNew SMS message received on one of your numbersmessageId, from, to, body, otp, receivedAt
number.expiring.7dNumber expiring in 7 daysnumberId, phoneNumber, expiresAt, daysUntilExpiry
number.expiring.1dNumber expiring in 1 daynumberId, phoneNumber, expiresAt, daysUntilExpiry
billing.issuePayment failed or action requiredinvoiceId, 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.

AttemptDelay After Failure
1st retry1 minute
2nd retry2 minutes
3rd retry4 minutes
4th retry8 minutes
5th retry16 minutes
6th retry32 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. 1

    Navigate to Dashboard → Notifications → Add Endpoint and select Webhook.

  2. 2

    Enter your HTTPS endpoint URL. We recommend a dedicated path such as /webhooks/joltsms.

  3. 3

    Select which event types you want to receive. You can subscribe to all events or only specific ones.

  4. 4

    Copy your webhook secret for signature verification. Store it securely in your server's environment variables.

  5. 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.