Webhooks Documentation

Real-time notifications for delivery receipts and opt-out events with HMAC-SHA256 signature verification.

Overview

The SMSDESK platform supports three types of webhooks:

Type Purpose Trigger Security
INCOMING Incoming SMS SMS received on your channel Security Key (Base64)
DLR Delivery Receipt Message delivered, failed, or status changed HMAC-SHA256
OPT-OUT Blacklist/STOP Contact sends STOP keyword HMAC-SHA256
ℹ️ Note: DLR and Opt-out webhooks use HMAC-SHA256 signatures. Incoming SMS webhooks use a security key with Base64 encoding for backward compatibility.

2. Security & Authentication

HMAC-SHA256 Signature

Every webhook request is signed using HMAC-SHA256 to ensure authenticity.

Signature Process

  1. Take the exact raw JSON body bytes (no normalization or whitespace changes)
  2. Compute HMAC-SHA256(key = sharedSecret, message = rawBodyBytes)
  3. Hex-encode the result in lowercase
  4. Prepend sha256=
  5. Send in the X-Signature header

Example Header

X-Signature: sha256=3a7f2c8d9e1f4b5a6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
⚠️ Important: The signature must be calculated using the exact bytes sent on the wire. Any re-serialization will result in a different hash.

Signature Verification (C# Example)

using System.Security.Cryptography;
using System.Text;

public bool VerifySignature(string payload, string secret, string signatureHeader)
{
    if (string.IsNullOrEmpty(signatureHeader) || !signatureHeader.StartsWith("sha256="))
        return false;

    // Calculate expected signature
    byte[] payloadBytes = Encoding.UTF8.GetBytes(payload);
    byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
    
    using (var hmac = new HMACSHA256(secretBytes))
    {
        byte[] hashBytes = hmac.ComputeHash(payloadBytes);
        string expectedSignature = BitConverter.ToString(hashBytes)
            .Replace("-", "")
            .ToLowerInvariant();
        
        string receivedSignature = signatureHeader.Substring(7); // Remove "sha256="
        
        // Constant-time comparison
        return CryptographicEquals(expectedSignature, receivedSignature);
    }
}

private bool CryptographicEquals(string a, string b)
{
    if (a == null || b == null || a.Length != b.Length)
        return false;
    
    int result = 0;
    for (int i = 0; i < a.Length; i++)
        result |= a[i] ^ b[i];
    
    return result == 0;
}

3. Configuration

Web Portal Configuration

Configure webhooks through your SMSDESK web portal:

  1. Log in to your SMSDESK portal (e.g., https://desk.sms.com.na for Namibia deployment)
  2. Navigate to Account → SMS Channels
  3. Click on your channel to edit
  4. Scroll to the Webhook Settings section
  5. Configure the following fields:
Field Description Required
Opt-out Webhook URL Your endpoint for STOP/opt-out notifications Optional
Opt-out Shared Secret Secret token for HMAC signature (auto-generated or custom) Optional
Opt-in Webhook URL Your endpoint for JOIN/opt-in notifications Optional
Opt-in Shared Secret Secret token for HMAC signature (auto-generated or custom) Optional
Blacklist Response Custom opt-out confirmation message (fallback if webhook returns empty) Optional
Whitelist Response Custom opt-in confirmation message (fallback if webhook returns empty) Optional
ℹ️ Note: You can use the "Generate" button next to the secret fields to create a cryptographically secure 64-character secret token.

3. Incoming SMS Webhook

Overview

Receive real-time notifications when SMS messages are received on your channels. Your webhook endpoint will be called for each incoming message, allowing you to process and respond automatically.

Configuration

Configure incoming SMS webhooks in the SMS Channel Settings page of the SMSDESK portal:

  1. Navigate to Account → SMS Channels
  2. Select your channel
  3. Configure the API/Webhook Settings section:
    • WebService Handler: Choose GET or SOAP
    • WebService URL: Your endpoint URL
    • Security Key: Secret key for authentication

HTTP GET Method

When using the GET method, the platform will call your endpoint with the following query parameters:

Request Parameters

Parameter Type Description
securitykey string Your configured security key for authentication
incomingsmsid long Unique ID of the incoming message
timestamp long Unix timestamp (seconds since epoch)
sender string Mobile number of the sender (e.g., +264811234567)
recipient string Your channel/shortcode that received the message
msg string Base64-encoded message wrapped with security key: <{key}>{message}<{key}>
message string Plain text message content

Example GET Request

GET https://your-api.com/webhook/incoming?securitykey=YOUR_KEY&incomingsmsid=12345×tamp=1720522800&sender=%2B264811234567&recipient=30470&msg=PFlPVVJfS0VZPkhlbGxvPFlPVVJfS0VZPg==&message=Hello

Response

Your endpoint should return a plain text response. This response will be sent back to the sender as an auto-reply SMS:

Example Response

Thank you for your message. We will respond shortly.

SOAP Method

When using SOAP, the platform will send a SOAP 1.2 request to your endpoint.

SOAP Request

<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                 xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
  <soap12:Body>
    <incomingsms xmlns="http://tempuri.org/">
      <incomingsmsid>12345</incomingsmsid>
      <timestamp>1720522800</timestamp>
      <sender>+264811234567</sender>
      <msg>PFlPVVJfS0VZPkhlbGxvPFlPVVJfS0VZPg==</msg>
      <securitykey>YOUR_KEY</securitykey>
    </incomingsms>
  </soap12:Body>
</soap12:Envelope>

SOAP Response

Return a SOAP response with the auto-reply message in the response body. The platform will extract and send the text content.

Security

The msg parameter contains the message wrapped with your security key and Base64-encoded:

  1. Original message: Hello
  2. Wrapped: <YOUR_KEY>Hello<YOUR_KEY>
  3. Base64 encoded: PFlPVVJfS0VZPkhlbGxvPFlPVVJfS0VZPg==

Verify the security key by decoding the msg parameter and checking that it's wrapped with your configured key.

⚠️ Important: Always validate the securitykey parameter matches your configured key before processing the message.

Processing Flow

  1. SMS received on your channel
  2. Platform calls your webhook endpoint (GET or SOAP)
  3. Your endpoint processes the message and returns a response
  4. Platform sends your response as an auto-reply SMS to the sender
  5. If webhook fails or returns empty, default auto-response is used (if configured)

Timeout & Retry

4. Delivery Receipt (DLR) Webhook

✅ Implemented: DLR webhooks are fully implemented as of version 4.8.43 (July 9, 2026). Delivery reports are automatically sent to your configured webhook endpoint with HMAC-SHA256 signatures.

How It Works

  1. 📨 Delivery report received from SMSC via SMPP
  2. 💾 Stored in sms_delivery_reports table
  3. 🔗 Automatically matched to original message in sms_outgoing
  4. 📡 Webhook sent to your configured endpoint (fire-and-forget, non-blocking)
  5. 🔄 Retries on 5xx errors (3 attempts with exponential backoff)

Trigger Conditions

DLR webhooks are sent when:

POST https://{your-host}/webhooks/v1/connect/{dlrSourceId}

Request Headers

Content-Type: application/json
X-Signature: sha256=<hmac-sha256-signature>
User-Agent: SMSDesk-Webhook/1.0

Request Payload

{
  "type": "delivery_receipt",
  "timestamp": "2026-07-08T14:25:10.1234567Z",
  "data": {
    "messageId": "987654321",
    "externalId": "SMSC-MSG-ID-12345",
    "mobile": "+27821234567",
    "channel": "30470",
    "status": "DELIVERED",
    "statusCode": 2,
    "statusDescription": "Message delivered to handset",
    "deliveredAt": "2026-07-08T14:25:09.0000000Z",
    "clientId": 1026,
    "reference": "REF-ABC-123"
  }
}

Payload Fields

Field Type Description
type string Always "delivery_receipt"
timestamp string (ISO 8601) UTC timestamp when webhook was sent
data.messageId string Your internal message ID
data.externalId string SMSC/carrier message ID
data.mobile string Recipient mobile number
data.channel string Channel/shortcode used to send
data.status string DELIVERED, FAILED, PENDING, EXPIRED
data.statusCode integer Numeric status code (see table below)
data.statusDescription string Detailed status description
data.deliveredAt string (ISO 8601) UTC timestamp of delivery/failure
data.clientId integer Client ID
data.reference string Your reference ID (if provided when sending)

Status Codes

Code Status Description
0 PENDING Message queued, awaiting submission
1 SUBMITTED Submitted to SMSC, awaiting delivery
2 DELIVERED Successfully delivered to handset
3 DELIVERED Delivered (alternative code)
4 FAILED Permanent failure (invalid number, blocked, etc.)
5 FAILED Temporary failure (will retry)
9 DELETED Message deleted/cancelled

5. Opt-out/STOP Webhook

Trigger Conditions

The opt-out webhook is triggered when an incoming SMS contains any of these keywords (case-insensitive):

English Keywords

  • STOP
  • UNSUBSCRIBE
  • CANCEL
  • END
  • QUIT
  • OPTOUT
  • OPT-OUT
  • OPT OUT
  • REMOVE
  • STOPALL
  • UNSUB

French Keywords

  • ARRET
  • ARRETER
POST https://{your-host}/webhooks/v1/connect/{blocklistSourceId}

Request Headers

Content-Type: application/json
X-Signature: sha256=<hmac-sha256-signature>
User-Agent: SMSDesk-Webhook/1.0

Request Payload

{
  "type": "opt_out",
  "timestamp": "2026-07-08T14:23:45.1234567Z",
  "data": {
    "from": "+27821234567",
    "to": "30470",
    "message": "STOP",
    "keyword": "STOP",
    "messageId": "123456789",
    "receivedAt": "2026-07-08T14:23:44.0000000Z",
    "clientId": 1026
  }
}

Payload Fields

Field Type Description
type string Always "opt_out"
timestamp string (ISO 8601) UTC timestamp when webhook was sent
data.from string Mobile number that sent the opt-out (international format)
data.to string Channel/shortcode that received the message
data.message string Full message text received
data.keyword string Detected opt-out keyword (uppercase)
data.messageId string Unique ID of the incoming SMS
data.receivedAt string (ISO 8601) UTC timestamp when message was received
data.clientId integer Client ID associated with the channel

What Happens After Opt-out

  1. ✅ Contact is immediately blacklisted in database (status = 0)
  2. 📝 status_description is set to: "OPTED OUT - 2026-07-08 14:23:45 UTC - SMS ID: 123456789"
  3. 📡 Opt-out webhook is sent to your endpoint
  4. � Confirmation SMS is sent to the contact (see Response section below)
  5. �🚫 Future SMS to this contact will be blocked with error: "Contact has opted out"

Webhook Response (Custom Confirmation Message)

Your webhook endpoint can return a custom confirmation message that will be sent to the contact as an SMS.

Response Priority

  1. Webhook Response: If your endpoint returns a plain text response (200 OK with body), that message will be sent to the contact
  2. Configured Message: If webhook returns empty or fails, the configured blacklist_response or whitelist_response from the database will be used
  3. Default Message: If no configured message exists, a generic default message will be generated

Example Response

HTTP/1.1 200 OK
Content-Type: text/plain

You have successfully unsubscribed from Lexna Insurance. You will no longer receive marketing messages. Reply JOIN to re-subscribe.
ℹ️ Note: HTML tags in the response will be automatically stripped. Return plain text or empty body to use the default configured message.

Default Messages

Opt-out: "You, {mobile}, have successfully opted out of {clientName} contact lists. You will no longer receive any messages. You can opt back in by sending JOIN to {channel}."

Opt-in: "You, {mobile}, have successfully registered as a contact of {clientName}. To opt-out, send STOP to {channel}."

6. Retry Logic

Expected Response Codes

Response Action Description
200 OK ✅ Success Webhook processed successfully
204 No Content ✅ Success Webhook processed successfully (no response body)
4xx Client Error ❌ No Retry Permanent error, will not retry
5xx Server Error 🔄 Retry Temporary error, will retry with exponential backoff

Retry Configuration

ℹ️ Note: On 5xx responses, please ensure your endpoint is idempotent as we will retry the same request.

7. Testing & Validation

Test Opt-out Webhook

Send a test SMS with a STOP keyword to your configured channel:

# From your mobile phone, send:
TO: 30470
MESSAGE: STOP

# Expected result:
# 1. Contact blacklisted in database
# 2. Webhook sent to your opt_out_url
# 3. Future messages to this number will be rejected

Test DLR Webhook

Send a test SMS and monitor for delivery receipt:

curl -X POST https://{host}/sms/2/text/single \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "30470",
    "to": "+27821234567",
    "text": "Test message",
    "notifyUrl": "https://your-host/webhooks/v1/connect/dlr123"
  }'

# Expected result:
# 1. Message sent
# 2. DLR webhook sent when delivered/failed

Webhook Signature Tester

Use this online tool to test your signature verification:

# Example payload
{
  "type": "opt_out",
  "timestamp": "2026-07-08T14:23:45.1234567Z",
  "data": {
    "from": "+27821234567",
    "keyword": "STOP"
  }
}

# Secret: your-secret-token-here
# Expected signature: sha256=... (calculated by your implementation)

8. Code Examples

ASP.NET Core Webhook Receiver

[ApiController]
[Route("webhooks/v1/connect")]
public class WebhookController : ControllerBase
{
    private readonly string _sharedSecret = "your-secret-token-here";

    [HttpPost("{sourceId}")]
    public async Task<IActionResult> ReceiveWebhook(string sourceId)
    {
        // Read raw body
        using var reader = new StreamReader(Request.Body);
        string payload = await reader.ReadToEndAsync();

        // Verify signature
        string signature = Request.Headers["X-Signature"];
        if (!VerifySignature(payload, _sharedSecret, signature))
        {
            return Unauthorized(new { error = "Invalid signature" });
        }

        // Parse payload
        var webhook = JsonSerializer.Deserialize<WebhookPayload>(payload);

        // Process based on type
        if (webhook.Type == "opt_out")
        {
            await ProcessOptOut(webhook.Data);
        }
        else if (webhook.Type == "delivery_receipt")
        {
            await ProcessDLR(webhook.Data);
        }

        return Ok();
    }

    private bool VerifySignature(string payload, string secret, string signatureHeader)
    {
        if (string.IsNullOrEmpty(signatureHeader) || !signatureHeader.StartsWith("sha256="))
            return false;

        byte[] payloadBytes = Encoding.UTF8.GetBytes(payload);
        byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
        
        using var hmac = new HMACSHA256(secretBytes);
        byte[] hashBytes = hmac.ComputeHash(payloadBytes);
        string expectedSignature = BitConverter.ToString(hashBytes)
            .Replace("-", "")
            .ToLowerInvariant();
        
        string receivedSignature = signatureHeader.Substring(7);
        return CryptographicEquals(expectedSignature, receivedSignature);
    }
}

Node.js Webhook Receiver

const express = require('express');
const crypto = require('crypto');
const app = express();

const SHARED_SECRET = 'your-secret-token-here';

app.post('/webhooks/v1/connect/:sourceId', express.text({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-signature'];
    const payload = req.body;

    // Verify signature
    if (!verifySignature(payload, SHARED_SECRET, signature)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }

    // Parse payload
    const webhook = JSON.parse(payload);

    // Process webhook
    if (webhook.type === 'opt_out') {
        processOptOut(webhook.data);
    } else if (webhook.type === 'delivery_receipt') {
        processDLR(webhook.data);
    }

    res.status(200).send();
});

function verifySignature(payload, secret, signatureHeader) {
    if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
        return false;
    }

    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(payload, 'utf8');
    const expectedSignature = hmac.digest('hex');
    const receivedSignature = signatureHeader.substring(7);

    return crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(receivedSignature)
    );
}

app.listen(3000);

PHP Webhook Receiver

<?php
$sharedSecret = 'your-secret-token-here';

// Read raw body
$payload = file_get_contents('php://input');

// Get signature header
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Verify signature
if (!verifySignature($payload, $sharedSecret, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Parse payload
$webhook = json_decode($payload, true);

// Process webhook
if ($webhook['type'] === 'opt_out') {
    processOptOut($webhook['data']);
} elseif ($webhook['type'] === 'delivery_receipt') {
    processDLR($webhook['data']);
}

http_response_code(200);

function verifySignature($payload, $secret, $signatureHeader) {
    if (empty($signatureHeader) || strpos($signatureHeader, 'sha256=') !== 0) {
        return false;
    }

    $expectedSignature = hash_hmac('sha256', $payload, $secret);
    $receivedSignature = substr($signatureHeader, 7);

    return hash_equals($expectedSignature, $receivedSignature);
}
?>

📞 Support

For technical support or questions about webhook integration:

Redirecting to Webhooks Documentation...