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

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:

  1. Create a webhook endpoint in your application
  2. Configure the webhook URL in your Sent Dashboard
  3. 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 '', 200
func 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:

  1. Go to the Activities page
  2. Filter by message status, date range, or template
  3. Click on any message for detailed information
  4. View delivery timeline and any errors

Status Reference

StatusDescriptionNext States
QUEUEDMessage accepted, awaiting processingSENT, FAILED
SENTDispatched to channel providerDELIVERED, FAILED
DELIVEREDConfirmed delivery to deviceREAD (WhatsApp only)
READRecipient opened message-
FAILEDDelivery 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 CodeDescriptionAction
VALIDATION_002Phone number format issueVerify E.164 format
BUSINESS_007Recipient opted outRemove from list
BUSINESS_008Carrier rejected messageCheck content compliance
BUSINESS_005Template not approvedWait for approval or use different template
BUSINESS_003Account balance lowAdd funds
BUSINESS_002Rate limit exceededImplement 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

On this page