Skip to content

Webhooks

Configure webhooks to receive real-time delivery status updates for your messages.

Overview

Webhooks allow you to receive HTTP POST notifications when message events occur, such as delivery confirmations or failures.

Webhook Events

MsGine sends webhooks for the following events:

EventDescription
message.sentMessage has been sent to the carrier
message.deliveredMessage was successfully delivered
message.failedMessage delivery failed

Setting Up Webhooks

Per-Message Webhooks

Specify a webhook URL when sending a message:

typescript
await client.sendSms({
  to: '+256701521269',
  message: 'Your verification code is 123456',
  callbackUrl: 'https://your-app.com/webhooks/msgine'
})

Using the REST API:

bash
curl -X POST https://api.msgine.net/api/v1/messages/sms \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+256701521269",
    "message": "Your verification code is 123456",
    "callbackUrl": "https://your-app.com/webhooks/msgine"
  }'

Global Webhooks

Configure a default webhook URL in your account settings:

  1. Navigate to Developer Settings
  2. Click Add Webhook
  3. Enter your webhook URL
  4. Select the events you want to receive
  5. Save the configuration

All messages will send events to this URL unless overridden by a per-message callbackUrl.

Webhook Payload

When an event occurs, MsGine sends a POST request to your webhook URL:

json
{
  "id": "msg_1234567890",
  "event": "message.delivered",
  "status": "delivered",
  "to": "+256701521269",
  "from": "MyApp",
  "timestamp": "2024-01-15T10:30:15Z",
  "messageId": "msg_1234567890",
  "sid": "SM1234567890abcdef"
}

Payload Fields

FieldTypeDescription
idstringMessage identifier
eventstringEvent type (e.g., message.delivered)
statusstringCurrent message status
tostringRecipient phone number
fromstringSender ID
timestampstringEvent timestamp (ISO 8601)
messageIdstringOriginal message ID
sidstringProvider-specific message ID
failureReasonstringReason for failure (if status is failed)

Failed Message Payload

When a message fails, the webhook includes additional failure information:

json
{
  "id": "msg_1234567890",
  "event": "message.failed",
  "status": "failed",
  "to": "+256701521269",
  "from": "MyApp",
  "timestamp": "2024-01-15T10:30:15Z",
  "failureReason": "Invalid phone number",
  "errorCode": "invalid_phone_number"
}

Implementing a Webhook Handler

Node.js/Express

typescript
import express from 'express'
import crypto from 'crypto'

const app = express()
app.use(express.json())

app.post('/webhooks/msgine', (req, res) => {
  // Verify webhook signature (recommended)
  if (!verifyWebhookSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  const event = req.body

  console.log('Webhook received:', event)

  // Handle different event types
  switch (event.event) {
    case 'message.delivered':
      console.log(`Message ${event.id} delivered to ${event.to}`)
      // Update database, send notification, etc.
      break

    case 'message.failed':
      console.log(`Message ${event.id} failed: ${event.failureReason}`)
      // Handle failure, retry, or notify user
      break

    case 'message.sent':
      console.log(`Message ${event.id} sent to carrier`)
      break
  }

  // Acknowledge receipt
  res.status(200).json({ received: true })
})

app.listen(3000)

Python/Flask

python
from flask import Flask, request, jsonify
import hmac
import hashlib

app = Flask(__name__)

@app.route('/webhooks/msgine', methods=['POST'])
def webhook():
    # Verify webhook signature (recommended)
    if not verify_webhook_signature(request):
        return jsonify({'error': 'Invalid signature'}), 401

    event = request.json

    print(f"Webhook received: {event}")

    # Handle different event types
    if event['event'] == 'message.delivered':
        print(f"Message {event['id']} delivered to {event['to']}")
        # Update database, send notification, etc.

    elif event['event'] == 'message.failed':
        print(f"Message {event['id']} failed: {event['failureReason']}")
        # Handle failure, retry, or notify user

    elif event['event'] == 'message.sent':
        print(f"Message {event['id']} sent to carrier")

    # Acknowledge receipt
    return jsonify({'received': True}), 200

PHP

php
<?php
// webhook.php

$payload = file_get_contents('php://input');
$event = json_decode($payload, true);

// Verify webhook signature (recommended)
if (!verifyWebhookSignature($_SERVER['HTTP_X_MSGINE_SIGNATURE'], $payload)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Handle different event types
switch ($event['event']) {
    case 'message.delivered':
        error_log("Message {$event['id']} delivered to {$event['to']}");
        // Update database, send notification, etc.
        break;

    case 'message.failed':
        error_log("Message {$event['id']} failed: {$event['failureReason']}");
        // Handle failure, retry, or notify user
        break;

    case 'message.sent':
        error_log("Message {$event['id']} sent to carrier");
        break;
}

// Acknowledge receipt
http_response_code(200);
echo json_encode(['received' => true]);
?>

Webhook Security

Signature Verification

MsGine signs all webhook requests with a signature in the X-MsGine-Signature header:

typescript
import crypto from 'crypto'

function verifyWebhookSignature(req: Request): boolean {
  const signature = req.headers['x-msgine-signature']
  const webhookSecret = process.env.MSGINE_WEBHOOK_SECRET!

  const payload = JSON.stringify(req.body)
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

Your webhook secret is available in the Developer Dashboard.

HTTPS Only

MsGine only sends webhooks to HTTPS URLs. HTTP URLs will be rejected.

IP Whitelisting

For additional security, whitelist MsGine's webhook IP addresses:

52.89.214.238
34.212.75.30
54.218.53.128

Webhook Best Practices

1. Respond Quickly

Acknowledge webhooks within 5 seconds:

typescript
app.post('/webhooks/msgine', async (req, res) => {
  // Immediately acknowledge
  res.status(200).json({ received: true })

  // Process asynchronously
  processWebhookAsync(req.body).catch(console.error)
})

2. Handle Duplicates

Webhooks may be delivered multiple times. Use idempotency:

typescript
const processedWebhooks = new Set()

app.post('/webhooks/msgine', (req, res) => {
  const webhookId = req.body.id

  if (processedWebhooks.has(webhookId)) {
    console.log('Duplicate webhook, skipping')
    return res.status(200).json({ received: true })
  }

  processedWebhooks.add(webhookId)

  // Process webhook
  // ...

  res.status(200).json({ received: true })
})

3. Retry Failed Processing

If processing fails, return an error and MsGine will retry:

typescript
app.post('/webhooks/msgine', async (req, res) => {
  try {
    await processWebhook(req.body)
    res.status(200).json({ received: true })
  } catch (error) {
    console.error('Webhook processing failed:', error)
    res.status(500).json({ error: 'Processing failed' })
    // MsGine will retry
  }
})

4. Log Webhooks

Log all webhooks for debugging and audit trails:

typescript
app.post('/webhooks/msgine', (req, res) => {
  console.log('Webhook received:', {
    id: req.body.id,
    event: req.body.event,
    timestamp: req.body.timestamp
  })

  // Process and respond
  // ...
})

Webhook Retries

If your webhook endpoint:

  • Returns a non-2xx status code
  • Times out (5 second limit)
  • Is unreachable

MsGine will retry with exponential backoff:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry15 minutes
4th retry1 hour
5th retry6 hours

After 5 failed attempts, the webhook is marked as failed and you'll receive an email notification.

Testing Webhooks

Local Development

Use a tool like ngrok to expose your local server:

bash
ngrok http 3000

Then use the ngrok URL as your webhook URL:

https://abc123.ngrok.io/webhooks/msgine

Webhook Testing Tool

Use the webhook testing tool in the Developer Dashboard to send test events.

Troubleshooting

Webhooks Not Received

  1. Check HTTPS: Ensure your URL uses HTTPS
  2. Verify URL: Check the URL is publicly accessible
  3. Check firewall: Ensure webhook IPs aren't blocked
  4. Review logs: Check your server logs for incoming requests

Signature Verification Fails

  1. Check secret: Ensure you're using the correct webhook secret
  2. Raw payload: Use the raw request body, not parsed JSON
  3. Encoding: Ensure UTF-8 encoding

Webhooks Delayed

Webhooks are usually delivered within seconds, but may be delayed due to:

  • Retries from previous failures
  • High volume of messages
  • Network issues

Next Steps

Released under the MIT License.