Skip to main content

UC-004 — Check Delivery Status

FieldValue
IDUC-004
GoalCheck message delivery status with polling and webhooks
ChannelMulti-channel (SMS, RCS, WhatsApp)
ComplexityBasic
Estimated time15 minutes
APIs involvedGET /api/partner-gateway/v1/messages/status/{customerMessageId}, GET /api/partner-gateway/v1/messages/status

Real-world scenarios​

  • OTP dashboard — Send verification: BancaSicura monitors in real time whether OTP codes are delivered within 5 seconds, activating an alternative channel in case of failure.
  • Campaign delivery report: MarketingPro generates an end-of-campaign SMS report with delivery percentages, errors, and average times.
  • Real-time monitoring: TechStore integrates the status check into their CRM to show a "Delivered" or "Pending" badge next to each sent communication.

Prerequisites​

Before you begin, make sure you have:

Test without costs

Add "simulation": true in the request body to validate the flow without actually sending messages and without consuming credit.

Polling vs Webhook​

FeaturePollingWebhook
DirectionYour server calls the APIThe API calls your server
LatencyDepends on polling intervalReal time
API loadProportional to the number of callsOne call per event
ComplexityLow (just a loop)Medium (requires a public endpoint)
Ideal forSpot checks, debug, small volumesProduction, high volumes, real-time
Which one to choose?

Use polling for tests, debugging, and manual checks. Use webhooks in production to receive real-time notifications without overloading the API. See the Webhook guide for configuration.

Step 1 — Send a message (prerequisite)​

To check the status, you must first have sent a message and saved the messageId. Quick example with SMS:

curl -s -X POST https://api.qlara.ai/api/message-server/sms/send \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"destination": "+393471234567",
"sender": "BancaSicura",
"body": "Il tuo codice OTP e: 847293. Valido per 5 minuti.",
"enableNotification": true
}'

Response​

{
"messageId": "d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890",
"simulation": false,
"results": {
"sms": {
"accepted": true,
"unicode": false,
"parts": 1,
"reasons": []
}
}
}
Behind the scenes — Status timeline

After sending, the message goes through these states in sequence:

  1. In flight — The gateway accepted the message and forwarded it, but no receipt has come back yet. The status reads ACCEPTED on SMS and UNKNOWN on RCS and WhatsApp, which have no intermediate status of their own.
  2. Final status — The carrier or platform receipt arrives and the status no longer changes:
    • SMS: DELIVERED, or a failure: REJECTED, UNDELIVERABLE, EXPIRED (not delivered within its validity window) or DELETED.
    • RCS: DELIVERED, or a failure: ERROR, DISABLED, UNSUPPORTED or EXPIRED.
    • WhatsApp: the same as RCS, plus CONVERSATION_CLOSED.
  3. Read (RCS/WhatsApp only) — Reading is not a status: the message stays DELIVERED. On WhatsApp the read time is set in readDate; on RCS and WhatsApp the webhook also sends a READ event.

The transition from ACCEPTED/UNKNOWN to DELIVERED typically takes 1-5 seconds for SMS, 0.5-2 seconds for RCS/WhatsApp.

Step 2 — Single polling​

Query the status of a single message using the messageId as a path parameter and the channel as a query parameter.

curl -X GET "https://api.qlara.ai/api/partner-gateway/v1/messages/status/d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890?channel=SMS" \
-H "X-Api-Key: YOUR_API_KEY"

Response — DELIVERED​

{
"customerMessageId": "d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890",
"channel": "SMS",
"destination": "+393471234567",
"deliveryStatus": "DELIVERED",
"deliveryStatusDescription": "delivered",
"sendDate": "2026-04-09T15:00:00+02:00",
"deliveryDate": "2026-04-09T15:00:03+02:00",
"readDate": null
}

Response — UNDELIVERABLE​

{
"customerMessageId": "d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890",
"channel": "SMS",
"destination": "+393471234567",
"deliveryStatus": "UNDELIVERABLE",
"deliveryStatusDescription": "undeliverable",
"sendDate": "2026-04-09T15:00:00+02:00",
"deliveryDate": null,
"readDate": null
}
Behind the scenes — Complete status table
StatusWebhook codedeliveryStatusDescriptionChannelDescription
ACCEPTED1acceptedSMSHanded to the carrier, no receipt yet. Not final
REJECTED2rejectedSMSThe carrier refused the message. Final
DELIVERED3deliveredSMS, RCS, WhatsAppMessage delivered to the handset. Final
EXPIRED4expiredSMS, RCS, WhatsAppNot delivered within its validity window. Final
DELETED5deletedSMSCancelled before delivery. Final
UNDELIVERABLE6undeliverableSMSThe destination cannot receive it (unreachable or invalid number). Final
ERROR9general errorRCS, WhatsAppDelivery failed. Final
DISABLED10disabledRCS, WhatsAppThe recipient has the channel switched off. Final
UNSUPPORTED11unsupportedRCS, WhatsAppThe device or number does not support the channel. Final
CONVERSATION_CLOSED12conversation closedWhatsAppThe 24-hour customer-service window had closed. Final
UNKNOWN0unknownSMS, RCS, WhatsAppNo status held for the message. On RCS and WhatsApp, the normal state while in flight

Note on webhook codes: The delivery-status webhook sends the same numeric codes as this table in statusCode, and the lowercase description in description. In the status polling API, the deliveryStatus field is the name in the first column. The webhook fires when the carrier or platform receipt arrives, so it never sends ACCEPTED (1) or UNKNOWN (0).

Step 3 — Batch polling​

To check the status of multiple messages in a single call, use the batch endpoint. All IDs must belong to the same channel.

curl -X GET "https://api.qlara.ai/api/partner-gateway/v1/messages/status?channel=SMS&ids=d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890,a1b2c3d4-e5f6-7890-abcd-ef1234567890,f47ac10b-58cc-4372-a567-0e02b2c3d479" \
-H "X-Api-Key: YOUR_API_KEY"

Response — Batch status​

[
{
"customerMessageId": "d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890",
"channel": "SMS",
"destination": "+393471234567",
"deliveryStatus": "DELIVERED",
"deliveryStatusDescription": "delivered",
"sendDate": "2026-04-09T15:00:00+02:00",
"deliveryDate": "2026-04-09T15:00:03+02:00",
"readDate": null
},
{
"customerMessageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channel": "SMS",
"destination": "+393481234567",
"deliveryStatus": "DELIVERED",
"deliveryStatusDescription": "delivered",
"sendDate": "2026-04-09T15:00:01+02:00",
"deliveryDate": "2026-04-09T15:00:04+02:00",
"readDate": null
},
{
"customerMessageId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"channel": "SMS",
"destination": "+393491234567",
"deliveryStatus": "UNDELIVERABLE",
"deliveryStatusDescription": "undeliverable",
"sendDate": "2026-04-09T15:00:01+02:00",
"deliveryDate": null,
"readDate": null
}
]
IDs not found

IDs that do not match any message are silently omitted from the response. If you send 3 IDs and receive only 2 results, the third was not found.

Behind the scenes — Batch limits and best practices
  • ID limit: You can send up to several hundred IDs in a single call. For higher volumes, split into multiple requests.
  • Same channel: All IDs must belong to the same channel (SMS, RCS, or WHATSAPP). For mixed channels, make separate calls.
  • Result order: Results are not ordered. Use customerMessageId to match with your records.
  • Polling interval: For automated checks, use an interval of at least 5 seconds between calls. For high volumes, switch to webhooks.

Example: polling loop with retry​

A bash script that checks the status of an SMS every 3 seconds, with a 30-second timeout. It stops on DELIVERED or on a final SMS failure; the failure statuses depend on the channel, so for RCS use ERROR|DISABLED|UNSUPPORTED|EXPIRED and for WhatsApp add CONVERSATION_CLOSED:

#!/bin/bash
MESSAGE_ID="d1e2f3a4-b5c6-7890-d1e2-f3a4b5c67890"
API_KEY="YOUR_API_KEY"
MAX_ATTEMPTS=10
ATTEMPT=0

while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
ATTEMPT=$((ATTEMPT + 1))
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS..."

STATUS=$(curl -s -X GET \
"https://api.qlara.ai/api/partner-gateway/v1/messages/status/${MESSAGE_ID}?channel=SMS" \
-H "X-Api-Key: ${API_KEY}" | jq -r '.deliveryStatus')

echo "Status: $STATUS"

case "$STATUS" in
DELIVERED)
echo "Message delivered!"
exit 0
;;
REJECTED|UNDELIVERABLE|EXPIRED|DELETED) # final SMS failures
echo "Delivery error: $STATUS"
exit 1
;;
esac

sleep 3
done

echo "Timeout: final status not reached"
exit 2

Channel comparison​

FieldSMSRCSWhatsApp
deliveryStatusACCEPTED, REJECTED, DELIVERED, EXPIRED, DELETED, UNDELIVERABLE, UNKNOWNDELIVERED, EXPIRED, ERROR, DISABLED, UNSUPPORTED, UNKNOWNDELIVERED, EXPIRED, ERROR, DISABLED, UNSUPPORTED, CONVERSATION_CLOSED, UNKNOWN
In-flight statusACCEPTEDUNKNOWNUNKNOWN
readDateAlways nullAlways nullPresent if read
Typical DELIVERED latency1-5 seconds0.5-2 seconds0.5-2 seconds
READ webhookNot availableAvailableAvailable

Common errors​

404 — Message not found​

{}

Solution: Verify that the customerMessageId is correct and that the channel parameter matches the channel used for sending. Very old messages may no longer be available.

Wrong channel in channel parameter​

If you send a message via SMS but query the status with channel=RCS, you will get a 404 even if the message exists. Make sure the channel in the query matches the one used for sending.

Persistent ACCEPTED or UNKNOWN status​

Until the receipt arrives, a message reads ACCEPTED on SMS and UNKNOWN on RCS and WhatsApp. What a long wait means depends on the channel:

  • RCS and WhatsApp: UNKNOWN is the normal in-flight state, not a routing problem. These channels have no intermediate status, so the message stays UNKNOWN until it becomes DELIVERED or fails, which can take a while (for example, if the recipient's phone is off).
  • SMS: if the status remains ACCEPTED for more than 60 seconds, the carrier has not returned a receipt yet. Check:
    • The recipient's number is valid and reachable
    • The recipient's carrier is supported
    • There are no ongoing network issues

Expected result​

StepActionResult
1POST /sms/send (or RCS/WhatsApp)messageId saved
2GET /messages/status/{id}?channel=SMSdeliveryStatus: "DELIVERED"
3GET /messages/status?channel=SMS&ids=id1,id2,id3Array of statuses for each message

Next steps​