Skip to main content

SMS Webhooks

Webhooks allow you to receive real-time notifications when SMS message status changes. Instead of polling the status endpoint, you can configure a callback URL to receive automatic updates.

How Webhooks Work

1

Send SMS with Webhook

Include receipt: true and receiptURL when sending an SMS. Both are required to receive webhooks.
2

Message Status Changes

When the message is sent, delivered, or fails, our system triggers a webhook.
3

Receive POST Request

We send a POST request to your webhook URL with the delivery status.
4

Confirm Receipt

Your server should respond with HTTP 200 to confirm receipt.

Enabling Webhooks

Setting Webhook URL

Webhook URL must be provided with each SMS request using the receiptURL parameter:
curl -X POST "https://api.loyalty.lt/lt/sms/send" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -H "X-API-Secret: your_api_secret" \
  -d '{
    "source": "MyBrand",
    "destination": "+37061234567",
    "message": "Your verification code: 123456",
    "receipt": true,
    "receiptURL": "https://your-site.com/webhooks/sms"
  }'
You must include receiptURL in every request where you want to receive delivery status updates. There is no default webhook URL setting.

Webhook Payload

When a message status changes, we send a POST request with the following JSON payload:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "externalId": "your-reference-123",
  "status": "DELIVERED",
  "recipient": "+37061234567",
  "timestamp": "2025-01-15T10:30:05.000Z",
  "error": null
}

Payload Fields

FieldTypeDescription
idstringInternal message UUID
externalIdstringYour reference ID from your system (if provided when sending)
statusstringCurrent status: SENT, DELIVERED, FAILED, UNDELIVERABLE
recipientstringRecipient phone number
timestampstringISO 8601 timestamp when status changed
errorstringError details if failed (null otherwise)

Status Values

StatusDescriptionWhen Triggered
SENTMessage sent to carrierImmediately after SMPP submission
DELIVEREDConfirmed delivered to phoneWhen carrier confirms DLR
FAILEDDelivery failedWhen carrier rejects message
UNDELIVERABLENumber unreachableWhen number is invalid, expired, or rejected
Statuses are uppercase: SENT, DELIVERED, FAILED, UNDELIVERABLE

Handling Webhooks

Example: Node.js / Express

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

app.use(express.json());

app.post('/webhooks/sms', async (req, res) => {
  const { id, externalId, status, recipient, timestamp, error } = req.body;
  
  console.log(`SMS ${id} to ${recipient}: ${status}`);
  
  switch (status) {
    case 'DELIVERED':
      // Mark message as delivered in your database
      await db.messages.update({ 
        where: { externalId: externalId },
        data: { status: 'delivered', deliveredAt: new Date(timestamp) }
      });
      break;
      
    case 'FAILED':
    case 'UNDELIVERABLE':
      // Handle failure - maybe retry or notify admin
      console.error(`SMS failed: ${error}`);
      await db.messages.update({
        where: { externalId: externalId },
        data: { status: 'failed', errorMessage: error }
      });
      break;
      
    case 'SENT':
      // Message sent to carrier, waiting for DLR
      await db.messages.update({
        where: { externalId: externalId },
        data: { status: 'sent', sentAt: new Date(timestamp) }
      });
      break;
  }
  
  // Always respond with 200 to acknowledge receipt
  res.status(200).json({ received: true });
});

app.listen(3000);

Example: PHP

<?php
// webhooks/sms.php

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

$id = $payload['id'];
$externalId = $payload['externalId'];
$status = $payload['status'];
$recipient = $payload['recipient'];
$timestamp = $payload['timestamp'];
$error = $payload['error'];

// Log the webhook
error_log("SMS Webhook: $id to $recipient - $status");

// Update your database
switch ($status) {
    case 'DELIVERED':
        $stmt = $pdo->prepare("UPDATE sms_messages SET status = 'delivered', delivered_at = ? WHERE external_id = ?");
        $stmt->execute([$timestamp, $externalId]);
        break;
        
    case 'SENT':
        $stmt = $pdo->prepare("UPDATE sms_messages SET status = 'sent', sent_at = ? WHERE external_id = ?");
        $stmt->execute([$timestamp, $externalId]);
        break;
        
    case 'FAILED':
    case 'UNDELIVERABLE':
        $stmt = $pdo->prepare("UPDATE sms_messages SET status = 'failed', error_message = ? WHERE external_id = ?");
        $stmt->execute([$error, $externalId]);
        break;
}

// Respond with 200
http_response_code(200);
echo json_encode(['received' => true]);

Example: Python / Flask

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/sms', methods=['POST'])
def sms_webhook():
    payload = request.json
    
    msg_id = payload['id']
    external_id = payload['externalId']
    status = payload['status']
    recipient = payload['recipient']
    timestamp = payload['timestamp']
    error = payload.get('error')
    
    print(f"SMS {msg_id} to {recipient}: {status}")
    
    if status == 'DELIVERED':
        db.execute(
            "UPDATE sms_messages SET status = 'delivered', delivered_at = %s WHERE external_id = %s",
            [timestamp, external_id]
        )
    elif status == 'SENT':
        db.execute(
            "UPDATE sms_messages SET status = 'sent', sent_at = %s WHERE external_id = %s",
            [timestamp, external_id]
        )
    elif status in ['FAILED', 'UNDELIVERABLE']:
        db.execute(
            "UPDATE sms_messages SET status = 'failed', error_message = %s WHERE external_id = %s",
            [error, external_id]
        )
    
    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

Best Practices

Respond Quickly

Return HTTP 200 within 5 seconds. Process data asynchronously if needed.

Handle Duplicates

Webhooks may be sent multiple times. Use message_id to deduplicate.

Verify Source

Validate webhook comes from Loyalty.lt IP ranges or use signature verification.

Log Everything

Log all webhook payloads for debugging and audit purposes.

Retry Policy

If your webhook endpoint fails to respond with HTTP 2xx, we retry with increasing delays:
AttemptDelay
1st retry30 seconds
2nd retry2 minutes
3rd retry10 minutes
4th retry30 minutes
5th (final)1 hour
After 5 failed attempts (over ~2 hours total), the webhook is abandoned. Make sure your endpoint is reliable and responds quickly.

Testing Webhooks

Using ngrok for Local Development

# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# Expose it publicly
ngrok http 3000

# Use the ngrok URL as your receiptURL
# https://abc123.ngrok.io/webhooks/sms

Test Payload

Use this sample payload to test your webhook handler:
curl -X POST "https://your-site.com/webhooks/sms" \
  -H "Content-Type: application/json" \
  -H "User-Agent: Loyalty-LT-SMPP-Gateway/1.0" \
  -d '{
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "externalId": "my-ref-123",
    "status": "DELIVERED",
    "recipient": "+37061234567",
    "timestamp": "2025-01-15T10:30:05.000Z",
    "error": null
  }'
Webhooks from Loyalty.lt include the header User-Agent: Loyalty-LT-SMPP-Gateway/1.0 which you can use for verification.

Security Recommendations

Webhook IP Addresses

All webhooks are sent from the following IP addresses. Whitelist these in your firewall:
IP AddressDescription
185.170.198.15Primary webhook server
5.189.181.34Secondary webhook server
# Nginx example
location /webhooks/sms {
    allow 185.170.198.15;
    allow 5.189.181.34;
    deny all;
    
    # ... your proxy config
}

Additional Security Steps

1

Use HTTPS

Always use HTTPS for your webhook endpoint to encrypt data in transit.
2

Whitelist IP Addresses

Only allow requests from 185.170.198.15 and 5.189.181.34.
3

Verify Message Exists

Before processing, verify the id exists in your system and was sent by you.
4

Rate Limit

Implement rate limiting to protect against potential abuse.