Tracking delivery statuses allows you to monitor message deliverability, confirm customer engagement (read receipts), and detect messaging failures in real time.
Wazapin pushes these status updates to your HTTPS endpoint via the message.status_update event.
Status transition flow
Outbound messages navigate through the following statuses:
stateDiagram-v2
[*] --> queued: API Request
queued --> sent: Dispatched to Meta
sent --> delivered: Arrived at Phone
delivered --> read: Opened by User
queued --> failed: Validation / Meta Error
sent --> failed: Meta Delivery Errorqueued: The message has been stored in Wazapin’s queue. (Returned immediately by thePOST /v1/messagesrequest).sent: Meta/WhatsApp accepted the message.delivered: The message was successfully delivered to the recipient’s phone (double checkmarks in WhatsApp).read: The user opened the message (blue double checkmarks in WhatsApp).failed: The message could not be sent or delivered.
Webhook payload structure
Unlike inbound message webhooks, message.status_update payloads contain the updated status directly in the webhook body. You do not need to call GET /v1/messages/{messageID} to extract the status.
Here is an example payload:
{
"message_id": "9f1fd66d-c37a-4b50-a8c2-b4dca523f9c8",
"conversation_id": "0f89b0f9-74b4-44f9-b9b6-48f6d4de57aa",
"status": "delivered",
"organization_id": "org_123"
}Code example
Here is how to catch the webhook and process the status transitions:
# If you miss a webhook, check the status via the API:
curl -X GET "https://api.wazapin.com/v1/messages/9f1fd66d-c37a-4b50-a8c2-b4dca523f9c8/status" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Accept: application/json"import express from "express";
import { Webhook } from "svix";
const app = express();
const wh = new Webhook(process.env.WAZAPIN_WEBHOOK_SECRET!);
app.post("/webhooks/wazapin", express.raw({ type: "application/json" }), async (req, res) => {
try {
wh.verify(req.body, req.headers as Record<string, string>);
} catch (err) {
return res.status(403).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString("utf8"));
// Check for status update event
// Some payloads map event names externally, or filter per endpoint
if (payload.status && payload.message_id) {
const messageId = payload.message_id;
const currentStatus = payload.status; // sent, delivered, read, failed
console.log(`Message ${messageId} updated to: ${currentStatus}`);
// Update your database state here...
// db.messages.update({ where: { id: messageId }, data: { status: currentStatus } });
if (currentStatus === "failed") {
// Handle delivery failure (e.g. check error codes)
console.error(`Message ${messageId} failed to deliver.`);
}
}
res.status(200).send("OK");
});from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
import os
app = FastAPI()
wh = Webhook(os.environ["WAZAPIN_WEBHOOK_SECRET"])
@app.post("/webhooks/wazapin")
async def handle_webhook(request: Request):
body = await request.body()
try:
wh.verify(body, dict(request.headers))
except WebhookVerificationError:
raise HTTPException(status_code=403, detail="Invalid signature")
payload = await request.json()
status = payload.get("status")
message_id = payload.get("message_id")
if status and message_id:
print(f"Message {message_id} status updated to: {status}")
# Update your database record here
# db.update_message_status(message_id, status)
if status == "failed":
print(f"Message {message_id} failed to deliver.")
return {"ok": True}Polling fallback
If your webhook receiver goes offline or you fail to receive a status update webhook, you can query the API directly as a fallback.
Wazapin provides two endpoints for status checks:
- GET /v1/messages/{messageID}: Retrieves the full message record including its current
status. - GET /v1/messages/{messageID}/status: A lightweight endpoint returning only the status metadata.
Troubleshooting
Status updates arrive out of order
Due to network concurrency, a read status webhook can occasionally arrive before a delivered webhook.
- Always check the existing status in your database before writing an update.
- Do not overwrite a terminal status (like
readorfailed) with an earlier status (likesentordelivered).
Handling failures
When a message status updates to failed, query the full message details using GET /v1/messages/{messageID}. The response will contain an error block (like error_code or failure_reason) explaining why the message could not be sent. Check the Error codes reference for details.