Receipt history
After a message is sent, WhatsApp returns delivery and read confirmations. The receipt history records these confirmations: delivered (delivered to the device) and read (read by the recipient), in ascending chronological order.
List receipts
To list a message’s receipt history, make a GET request with the message_id returned when you sent it.
Endpoint: GET /v1/messages/{message_id}/receipts for the main number. For extra numbers, use GET /v1/numbers/{ref}/messages/{message_id}/receipts, where {ref} is the number’s current alias or the number’s id (num_…, the message’s number_id).
Required scope: messages:read
Path parameters:
| Parameter | Type | Description |
|---|---|---|
ref | string | (extra number route only) Alias of the number the message belongs to (main or the alias of an extra number) or its id (num_…). |
message_id | string | Public message identifier (msg_{hex32}) |
curl -X GET \ https://api.turbonotify.com/v1/messages/msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7/receipts \ -H "Authorization: Bearer YOUR_KEY"const response = await fetch( 'https://api.turbonotify.com/v1/messages/msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7/receipts', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_KEY' } });
const result = await response.json();console.log(result);import requests
url = "https://api.turbonotify.com/v1/messages/msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7/receipts"headers = { "Authorization": "Bearer YOUR_KEY"}
response = requests.get(url, headers=headers)result = response.json()print(result)package main
import ( "fmt" "io" "net/http")
func main() { req, _ := http.NewRequest("GET", "https://api.turbonotify.com/v1/messages/msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7/receipts", nil) req.Header.Set("Authorization", "Bearer YOUR_KEY") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body))}<?php$url = 'https://api.turbonotify.com/v1/messages/msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7/receipts';
$options = [ 'http' => [ 'header' => [ 'Authorization: Bearer YOUR_KEY' ], 'method' => 'GET' ]];
$context = stream_context_create($options);$result = file_get_contents($url, false, $context);$response = json_decode($result, true);var_dump($response);?>var client = new HttpClient();client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_KEY");
var response = await client.GetAsync( "https://api.turbonotify.com/v1/messages/msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7/receipts");var result = await response.Content.ReadAsStringAsync();Console.WriteLine(result);import java.net.http.*;import java.net.URI;
public class TurboNotify { public static void main(String[] args) throws Exception { var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder() .uri(URI.create("https://api.turbonotify.com/v1/messages/msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7/receipts")) .header("Authorization", "Bearer YOUR_KEY") .GET() .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); }}Response examples
With full history (direct message)
{
"message_id": "msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7",
"receipts": [
{
"from": {
"contact_id": "ct_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"name": "Ana Souza",
"number": "5511988887777"
},
"status": "delivered",
"at": "2025-07-16T10:00:05Z"
},
{
"from": {
"contact_id": "ct_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"name": "Ana Souza",
"number": "5511988887777"
},
"status": "read",
"at": "2025-07-16T10:01:12Z"
}
]
} In a group (multiple recipients)
The contact_id field appears only for participants who are already registered contacts in Turbo Notify. The name field (the WhatsApp display name) is independent: it can appear even without a contact_id. In the example below, Ana Souza is already a registered contact; Carlos Lima has a display name but is not a registered contact yet.
{
"message_id": "msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7",
"receipts": [
{
"from": {
"contact_id": "ct_b1c2d3e4f5a6b1c2d3e4f5a6b1c2d3e4",
"name": "Ana Souza",
"number": "5511988887777"
},
"status": "delivered",
"at": "2025-07-16T10:00:05Z"
},
{
"from": {
"name": "Carlos Lima",
"number": "5511977776666"
},
"status": "delivered",
"at": "2025-07-16T10:00:08Z"
},
{
"from": {
"contact_id": "ct_b1c2d3e4f5a6b1c2d3e4f5a6b1c2d3e4",
"name": "Ana Souza",
"number": "5511988887777"
},
"status": "read",
"at": "2025-07-16T10:01:12Z"
}
]
} No receipts yet
{
"message_id": "msg_0198d3c5a1b24d3e9f01a2b3c4d5e6f7",
"receipts": []
} Response structure
| Field | Type | Description |
|---|---|---|
message_id | string | Public message identifier. |
receipts | array | List of status changes, in ascending chronological order. |
receipts[].from | object | Who confirmed the receipt. Omitted when WhatsApp gives no information about who confirmed it. |
receipts[].from.contact_id | string | Public contact identifier (ct_{hex32}). Omitted when not available. |
receipts[].from.name | string | WhatsApp display name (personal data, under your organization’s responsibility). Omitted when not available. |
receipts[].from.number | string | E.164 number without + of who confirmed the receipt. Omitted when WhatsApp does not reveal the number of who confirmed it. |
receipts[].status | string | Status reached in this change (delivered or read). |
receipts[].at | string | ISO 8601 date and time when the status was recorded. |
Status table
| Status | Description |
|---|---|
delivered | Confirmation from WhatsApp that the message was delivered to the device. |
read | Confirmation from WhatsApp that the recipient read the message. |
Note: Only confirmations sent by WhatsApp (
deliveredandread) are recorded as receipts. Thesentstatus (sent to WhatsApp) can be checked withGET /v1/numbers/{ref}/messages/{id}/status.
Requires the Business plan or higher
The detailed receipt history (GET .../messages/{id}/receipts) requires the read_receipts feature, available from the Business plan up. Organizations on the Free and Lone Wolf plans receive:
{
"error": "feature_not_available"
} This restriction applies to both routes (the nested /v1/numbers/{ref}/messages/{id}/receipts and the short /v1/messages/{id}/receipts route of the main number). The restriction does not affect the overall message status: GET .../messages/{id}/status stays available on every plan, including Free and Lone Wolf. Only the detailed history, change by change, requires the Business plan or higher.
See Quota and usage for the limits and restrictions of each plan.
Possible errors
| Status | Error code | Description |
|---|---|---|
402 | feature_not_available | Organization on the Free or Lone Wolf plan: the receipt history requires the Business plan or higher. |
401 | not_authenticated | The API key is invalid or missing. |
403 | insufficient_scope | The API key does not have the messages:read scope. |
404 | not_found | The message was not found, belongs to another organization, or belongs to a number other than the one in the URL (on the shortcut route without {ref}, the main number): all of these cases return the same response, with no distinction. |
404 | number_not_found | (only on the route with {ref}) The alias or id in the URL does not match any registered number. |
400 | invalid_number_id | (only on the route with {ref}) The value in the URL starts with num_ but is not a valid number id. |
500 | internal_error | Unexpected internal server error. |