Message Status Tracking
Track the delivery status of your messages in real-time using webhooks, the API, or the Sent Dashboard.
Overview
After sending a message, it progresses through several statuses:
Tracking Methods
Method 1: Webhooks (Recommended)
Receive real-time status updates via HTTP callbacks to your server.
Advantages:
- Real-time updates (within seconds)
- No polling required
- Scalable for high volume
Setup:
- Create a webhook endpoint in your application
- Configure the webhook URL in your Sent Dashboard
- Handle incoming events
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/sent', async (req, res) => {
res.sendStatus(200); // Acknowledge quickly
const { field, payload } = req.body;
if (field === 'messages') {
const { message_id, message_status, channel } = payload;
// Update your database
await db.messages.update(message_id, {
status: message_status,
channel: channel,
updatedAt: new Date()
});
// Trigger business logic
if (message_status === 'DELIVERED') {
await handleDeliveryConfirmation(message_id);
} else if (message_status === 'FAILED') {
await handleDeliveryFailure(message_id);
}
}
});from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/sent', methods=['POST'])
def handle_webhook():
data = request.json
field = data['field']
if field == 'messages':
p = data['payload']
message_id = p['message_id']
message_status = p['message_status']
channel = p['channel']
# Update database
db.messages.update(message_id, status=message_status, channel=channel)
# Business logic
if message_status == 'DELIVERED':
handle_delivery_confirmation(message_id)
elif message_status == 'FAILED':
handle_delivery_failure(message_id)
return '', 200func webhookHandler(w http.ResponseWriter, r *http.Request) {
var event WebhookEvent
json.NewDecoder(r.Body).Decode(&event)
w.WriteHeader(http.StatusOK) // Acknowledge quickly
if event.Field == "messages" {
messageID := event.Payload.MessageID
messageStatus := event.Payload.MessageStatus
channel := event.Payload.Channel
// Update database
db.Messages.Update(messageID, messageStatus, channel)
// Business logic
if messageStatus == "DELIVERED" {
handleDeliveryConfirmation(messageID)
} else if messageStatus == "FAILED" {
handleDeliveryFailure(messageID)
}
}
}Webhook Event Structure:
{
"field": "messages",
"timestamp": "2025-01-15T08:30:15Z",
"payload": {
"account_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8",
"message_status": "DELIVERED",
"channel": "sms",
"inbound_number": "+1234567890",
"outbound_number": "+1987654321",
"template_id": "9ba7b840-9dad-11d1-80b4-00c04fd430c8"
}
}See the Webhooks Guide for complete setup instructions.
Method 2: API Polling
Query message status via the API. Useful for one-off checks or debugging.
curl "https://api.sent.dm/v3/messages/msg_1234567890" \
-H "x-api-key: $SENT_API_KEY"const message = await client.messages.get('msg_1234567890');
console.log(`Status: ${message.data.status}`);
console.log(`Events:`, message.data.events);message = client.messages.get("msg_1234567890")
print(f"Status: {message.data.status}")
print(f"Events: {message.data.events}")message, err := client.Messages.Get(context.Background(), "msg_1234567890")
fmt.Printf("Status: %s\n", message.Data.Status)
fmt.Printf("Events: %v\n", message.Data.Events)var message = client.messages().get("msg_1234567890");
System.out.println("Status: " + message.data().status());
System.out.println("Events: " + message.data().events());var message = await client.Messages.GetAsync("msg_1234567890");
Console.WriteLine($"Status: {message.Data.Status}");
Console.WriteLine($"Events: {message.Data.Events}");$message = $client->messages->get("msg_1234567890");
echo "Status: {$message->data->status}\n";
echo "Events: " . json_encode($message->data->events) . "\n";message = sent_dm.messages.get("msg_1234567890")
puts "Status: #{message.data.status}"
puts "Events: #{message.data.events}"Response:
{
"success": true,
"data": {
"id": "msg_1234567890",
"customer_id": "cust_abc123",
"contact_id": "contact_def456",
"phone": "+1234567890",
"phone_international": "+1 234-567-890",
"region_code": "US",
"template_id": "tmpl_123",
"template_name": "order_confirmation",
"template_category": "UTILITY",
"channel": "sms",
"message_body": {
"header": null,
"content": "Your order #12345 has been shipped!",
"footer": null,
"buttons": null
},
"status": "DELIVERED",
"created_at": "2025-01-15T08:30:00Z",
"price": 0.0055,
"active_contact_price": 0.001,
"events": [
{ "status": "QUEUED", "timestamp": "2025-01-15T08:30:00Z", "description": "Message queued for sending" },
{ "status": "SENT", "timestamp": "2025-01-15T08:30:01Z", "description": "Message sent via SMS" },
{ "status": "DELIVERED", "timestamp": "2025-01-15T08:30:15Z", "description": "Message delivered to recipient" }
]
},
"error": null,
"meta": {
"request_id": "req_xyz789",
"timestamp": "2025-01-15T08:30:16Z",
"version": "v3"
}
}Don't poll for status updates in production. Use webhooks instead. Polling is only recommended for debugging or one-off checks.
Method 3: Dashboard
View message status in the Sent Dashboard:
- Go to the Activities page
- Filter by message status, date range, or template
- Click on any message for detailed information
- View delivery timeline and any errors
Status Reference
| Status | Description | Next States |
|---|---|---|
QUEUED | Message accepted, awaiting processing | SENT, FAILED |
SENT | Dispatched to channel provider | DELIVERED, FAILED |
DELIVERED | Confirmed delivery to device | READ (WhatsApp only) |
READ | Recipient opened message | - |
FAILED | Delivery failed | - |
Handling Failed Messages
When a message fails, the webhook event includes the FAILED status. Fetch the message via the API for full error details:
{
"field": "messages",
"timestamp": "2025-01-15T08:30:15Z",
"payload": {
"account_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8",
"message_status": "FAILED",
"channel": "sms",
"inbound_number": "+1234567890",
"outbound_number": "+1987654321",
"template_id": "9ba7b840-9dad-11d1-80b4-00c04fd430c8"
}
}Common Failure Reasons
| Error Code | Description | Action |
|---|---|---|
VALIDATION_002 | Phone number format issue | Verify E.164 format |
BUSINESS_007 | Recipient opted out | Remove from list |
BUSINESS_008 | Carrier rejected message | Check content compliance |
BUSINESS_005 | Template not approved | Wait for approval or use different template |
BUSINESS_003 | Account balance low | Add funds |
BUSINESS_002 | Rate limit exceeded | Implement backoff (200 req/min std) |
Best Practices
1. Implement Idempotency
Webhooks may be delivered multiple times. Handle this gracefully:
async function handleWebhook(eventData: any) {
const { message_id, message_status } = eventData.payload;
// Check if already processed
const existing = await db.messages.findById(message_id);
if (existing?.status === message_status) {
return; // Already at this status — skip
}
// Process update
await db.messages.update(message_id, { status: message_status });
}2. Queue Webhook Processing
Don't do heavy work in the webhook handler:
app.post('/webhooks/sent', async (req, res) => {
// Acknowledge immediately
res.sendStatus(200);
// Queue for background processing
await queue.add('process-webhook', req.body);
});
// Worker processes in background
queue.process('process-webhook', async (job) => {
await processWebhookEvent(job.data);
});3. Handle Late Deliveries
Some messages may be delivered hours later (e.g., device offline):
if (message_status === 'DELIVERED') {
const sentAt = new Date(eventData.created_at || message.sent_at);
const deliveredAt = new Date(eventData.timestamp);
const delayHours = (deliveredAt - sentAt) / (1000 * 60 * 60);
if (delayHours > 1) {
console.log(`Late delivery: ${delayHours} hours`);
}
}4. Monitor Delivery Rates
Track your delivery performance:
// Daily delivery rate
const stats = await db.messages.aggregate([
{
$match: {
createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
}
},
{
$group: {
_id: '$status',
count: { $sum: 1 }
}
}
]);
const delivered = stats.find(s => s._id === 'DELIVERED')?.count || 0;
const total = stats.reduce((sum, s) => sum + s.count, 0);
const deliveryRate = (delivered / total) * 100;
console.log(`Delivery rate: ${deliveryRate}%`);Read Receipts (WhatsApp)
WhatsApp supports read receipts when the recipient opens the message:
{
"field": "messages",
"timestamp": "2025-01-15T09:15:30Z",
"payload": {
"account_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8",
"message_status": "READ",
"channel": "whatsapp",
"inbound_number": "+1234567890",
"outbound_number": "+1987654321",
"template_id": "9ba7b840-9dad-11d1-80b4-00c04fd430c8"
}
}Read receipts are only available for WhatsApp and only when the recipient has read receipts enabled in their privacy settings.
Troubleshooting
Webhook not receiving events?
- Verify webhook URL is accessible from the internet
- Check that your endpoint returns 2xx status
- Review webhook delivery logs in the dashboard
- Verify the webhook is configured for the correct event types
Status stuck in "queued"?
- Normal for first few seconds
- Check if account has sufficient balance
- Verify KYC is approved
- Contact support if stuck > 5 minutes
Missing status updates?
- Ensure webhook endpoint is responding quickly (< 5 seconds)
- Check for duplicate event handling (idempotency)
- Review failed webhook deliveries in dashboard