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
Send SMS with Webhook
Include receipt: true and receiptURL when sending an SMS. Both are required to receive webhooks.
Message Status Changes
When the message is sent, delivered, or fails, our system triggers a webhook.
Receive POST Request
We send a POST request to your webhook URL with the delivery status.
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
Field Type Description idstring Internal message UUID externalIdstring Your reference ID from your system (if provided when sending) statusstring Current status: SENT, DELIVERED, FAILED, UNDELIVERABLE recipientstring Recipient phone number timestampstring ISO 8601 timestamp when status changed errorstring Error details if failed (null otherwise)
Status Values
Status Description When Triggered SENTMessage sent to carrier Immediately after SMPP submission DELIVEREDConfirmed delivered to phone When carrier confirms DLR FAILEDDelivery failed When carrier rejects message UNDELIVERABLENumber unreachable When 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:
Attempt Delay 1st retry 30 seconds 2nd retry 2 minutes 3rd retry 10 minutes 4th retry 30 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 Address Description 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
Use HTTPS
Always use HTTPS for your webhook endpoint to encrypt data in transit.
Whitelist IP Addresses
Only allow requests from 185.170.198.15 and 5.189.181.34.
Verify Message Exists
Before processing, verify the id exists in your system and was sent by you.
Rate Limit
Implement rate limiting to protect against potential abuse.