Message Status Callbacks
Receive real-time delivery status updates for your sent messages through webhook callbacks.
Overview
Message status callbacks provide real-time updates about the delivery status of your SMS and email messages. By including a CallbackUrl
in your message payload, you'll receive HTTP POST notifications as the message status changes.
Callback Setup
Basic Implementation
Include a CallbackUrl
field in your message payload:
{
"messageData": [{
"CustomerId": "your-customer-id",
"Password": "your-api-key",
"Recipients": [
{
"MobileNumber": "+447700900123"
}
],
"SMSMessage": "Your message content",
"CallbackUrl": "https://your-app.com/sms-callback"
}]
}
Dynamic URL Parameters
Use dynamic parameters in your callback URL to include message-specific information:
{
"messageData": [{
"CustomerId": "your-customer-id",
"Password": "your-api-key",
"Recipients": [
{
"MobileNumber": "+447700900123"
}
],
"SMSMessage": "Your message content",
"CallbackUrl": "https://your-app.com/sms-callback?msgid={sentid}&ref={clientref}&status={status}&recipient={recipient}"
}]
}
Available Parameters
Parameter | Description | Example |
---|---|---|
{sentid} | Unique message identifier | 1469.20250715.02516506506657265717 |
{contactid} | Contact/recipient identifier | 12345 |
{externalid} | External reference ID | EXT-789 |
{clientref} | Your ClientRef value | MSG-12345 |
{recipient} | Recipient phone number/email | +447700900123 |
{status} | Delivery status | Delivered |
{err} | Error message (if applicable) | Invalid number |
{units} | Number of SMS units used | 2 |
Callback Payload Examples
Successful Delivery
POST /sms-callback?msgid=1469.20250715.02516506506657265717&status=Delivered
Content-Type: application/json
{
"MessageId": "1469.20250715.02516506506657265717-e0684dbcb71f402ab39d4d6b86d44cbb",
"Status": "Delivered",
"Timestamp": "2025-07-15T17:29:12.1234567Z",
"Recipient": "+447700900123",
"ClientRef": "MSG-12345",
"Units": 1,
"CustomerId": "your-customer-id"
}
Failed Delivery
POST /sms-callback?msgid=1469.20250715.02516506506657265717&status=Failed
Content-Type: application/json
{
"MessageId": "1469.20250715.02516506506657265717-e0684dbcb71f402ab39d4d6b86d44cbb",
"Status": "Failed",
"Timestamp": "2025-07-15T17:30:45.9876543Z",
"Recipient": "+447700900123",
"ClientRef": "MSG-12345",
"Units": 0,
"Error": "Invalid destination number",
"CustomerId": "your-customer-id"
}
Status Types
SMS Status Progression
- Queued - Message accepted and queued for sending
- Sent - Message sent to carrier
- Delivered - Message delivered to recipient
- Failed - Message delivery failed
- Expired - Message expired before delivery
Email Status Progression
- Queued - Email accepted and queued for sending
- Sent - Email sent to mail server
- Delivered - Email delivered to recipient's mailbox
- Bounced - Email bounced (hard or soft bounce)
- Failed - Email delivery failed
Implementation Examples
Node.js/Express Handler
app.post('/sms-callback', express.json(), (req, res) => {
try {
const callbackData = req.body;
const queryParams = req.query;
// Log the callback for debugging
console.log('SMS callback received:', {
messageId: callbackData.MessageId,
status: callbackData.Status,
recipient: callbackData.Recipient,
timestamp: callbackData.Timestamp
});
// Update message status in database
updateMessageStatus({
messageId: callbackData.MessageId,
status: callbackData.Status,
recipient: callbackData.Recipient,
timestamp: new Date(callbackData.Timestamp),
error: callbackData.Error,
units: callbackData.Units
});
// Trigger any business logic
if (callbackData.Status === 'Delivered') {
onMessageDelivered(callbackData);
} else if (callbackData.Status === 'Failed') {
onMessageFailed(callbackData);
}
// Always acknowledge receipt
res.status(200).send('OK');
} catch (error) {
console.error('Callback processing error:', error);
res.status(200).send('OK'); // Still acknowledge to prevent retries
}
});
async function updateMessageStatus(statusData) {
await database.messages.update(
{ message_id: statusData.messageId },
{
status: statusData.status,
delivered_at: statusData.timestamp,
error_message: statusData.error,
units_used: statusData.units,
updated_at: new Date()
}
);
}
function onMessageDelivered(callbackData) {
// Handle successful delivery
analytics.track('message_delivered', {
messageId: callbackData.MessageId,
recipient: callbackData.Recipient,
units: callbackData.Units
});
}
function onMessageFailed(callbackData) {
// Handle delivery failure
console.warn('Message delivery failed:', {
messageId: callbackData.MessageId,
recipient: callbackData.Recipient,
error: callbackData.Error
});
// Maybe retry or alert user
notifyMessageFailure(callbackData);
}
Python/Flask Handler
from flask import Flask, request, jsonify
import json
import logging
from datetime import datetime
app = Flask(__name__)
@app.route('/sms-callback', methods=['POST'])
def handle_sms_callback():
try:
callback_data = request.get_json()
query_params = request.args
# Log callback
logging.info(f"SMS callback: {callback_data.get('MessageId')} - {callback_data.get('Status')}")
# Update message status
update_message_status({
'message_id': callback_data.get('MessageId'),
'status': callback_data.get('Status'),
'recipient': callback_data.get('Recipient'),
'timestamp': datetime.fromisoformat(callback_data.get('Timestamp').replace('Z', '+00:00')),
'error': callback_data.get('Error'),
'units': callback_data.get('Units', 0)
})
# Handle status-specific logic
status = callback_data.get('Status')
if status == 'Delivered':
on_message_delivered(callback_data)
elif status == 'Failed':
on_message_failed(callback_data)
return 'OK', 200
except Exception as error:
logging.error(f"Callback processing error: {error}")
return 'OK', 200 # Always acknowledge
def update_message_status(status_data):
# Update your database
pass
def on_message_delivered(callback_data):
# Handle successful delivery
pass
def on_message_failed(callback_data):
# Handle delivery failure
pass
C# ASP.NET Handler
[HttpPost("sms-callback")]
public async Task<IActionResult> HandleSmsCallback([FromBody] SmsCallbackDto callbackData)
{
try
{
_logger.LogInformation("SMS callback: {MessageId} - {Status}",
callbackData.MessageId, callbackData.Status);
// Update message status
await _messageService.UpdateStatusAsync(new MessageStatusUpdate
{
MessageId = callbackData.MessageId,
Status = callbackData.Status,
Recipient = callbackData.Recipient,
Timestamp = callbackData.Timestamp,
Error = callbackData.Error,
Units = callbackData.Units
});
// Handle status-specific logic
switch (callbackData.Status)
{
case "Delivered":
await OnMessageDelivered(callbackData);
break;
case "Failed":
await OnMessageFailed(callbackData);
break;
}
return Ok("OK");
}
catch (Exception ex)
{
_logger.LogError(ex, "Callback processing error");
return Ok("OK"); // Always acknowledge
}
}
public class SmsCallbackDto
{
public string MessageId { get; set; }
public string Status { get; set; }
public DateTime Timestamp { get; set; }
public string Recipient { get; set; }
public string ClientRef { get; set; }
public int Units { get; set; }
public string Error { get; set; }
public string CustomerId { get; set; }
}
Security Considerations
Endpoint Verification
// Verify callbacks come from trusted sources
app.use('/sms-callback', (req, res, next) => {
const allowedIPs = ['185.xxx.xxx.xxx']; // VenturEd IP ranges
const clientIP = req.ip || req.connection.remoteAddress;
if (process.env.NODE_ENV === 'production' && !allowedIPs.includes(clientIP)) {
return res.status(403).send('Forbidden');
}
next();
});
Idempotency Handling
const processedCallbacks = new Set();
app.post('/sms-callback', (req, res) => {
const callbackData = req.body;
const callbackKey = `${callbackData.MessageId}-${callbackData.Status}-${callbackData.Timestamp}`;
// Check if we've already processed this callback
if (processedCallbacks.has(callbackKey)) {
console.log('Duplicate callback ignored:', callbackKey);
return res.status(200).send('OK');
}
// Process callback
try {
processCallback(callbackData);
processedCallbacks.add(callbackKey);
res.status(200).send('OK');
} catch (error) {
console.error('Callback error:', error);
res.status(200).send('OK');
}
});
Testing Callbacks
Local Development with ngrok
# Install and run ngrok
npm install -g ngrok
ngrok http 3000
# Use ngrok URL in your callback URL
# https://abc123.ngrok.io/sms-callback
Callback Simulation
// Simulate callback for testing
const simulateCallback = async (messageId, status = 'Delivered') => {
const callbackPayload = {
MessageId: messageId,
Status: status,
Timestamp: new Date().toISOString(),
Recipient: '+447700900123',
ClientRef: 'TEST-MSG',
Units: 1,
CustomerId: 'test-customer'
};
const response = await fetch('http://localhost:3000/sms-callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(callbackPayload)
});
console.log('Callback simulation:', response.status);
};
Monitoring and Analytics
Callback Metrics
const callbackMetrics = {
total: 0,
byStatus: {},
errors: 0,
avgProcessingTime: 0
};
app.post('/sms-callback', (req, res) => {
const startTime = Date.now();
try {
const callbackData = req.body;
const status = callbackData.Status;
// Update metrics
callbackMetrics.total++;
callbackMetrics.byStatus[status] = (callbackMetrics.byStatus[status] || 0) + 1;
processCallback(callbackData);
// Calculate processing time
const processingTime = Date.now() - startTime;
callbackMetrics.avgProcessingTime =
(callbackMetrics.avgProcessingTime + processingTime) / 2;
res.status(200).send('OK');
} catch (error) {
callbackMetrics.errors++;
console.error('Callback error:', error);
res.status(200).send('OK');
}
});
// Metrics endpoint
app.get('/callback-metrics', (req, res) => {
res.json(callbackMetrics);
});
Troubleshooting
Common Issues
Callbacks Not Received
- Verify callback URL is accessible from the internet
- Check firewall settings allow incoming POST requests
- Ensure endpoint returns 200 OK status
- Test with tools like ngrok for local development
Duplicate Callbacks
- Implement idempotency using message ID + status + timestamp
- Log and ignore duplicate callbacks
- Don't fail on duplicates
Callback Processing Errors
- Always return 200 OK status to prevent retries
- Log errors for debugging but acknowledge receipt
- Implement retry logic for your own processing failures
Debugging Tips
// Comprehensive callback logging
app.post('/sms-callback', (req, res) => {
console.log('=== SMS Callback Debug ===');
console.log('Headers:', req.headers);
console.log('Query:', req.query);
console.log('Body:', req.body);
console.log('IP:', req.ip);
console.log('========================');
// Your processing logic
res.status(200).send('OK');
});
Related Documentation: