Appearance
WhatsApp Messaging API Documentation
The WhatsApp Messaging API allows your applications to programmatically send WhatsApp messages to your customers. This API supports sending text messages, media files, and template messages while providing message tracking and delivery status updates.
Base URL
https://api.talkingshops.com/v1Authentication
All API requests (except webhooks) require authentication using an API key. Include your API key in the request header:
x-tenant-api-key: your_api_key_hereAPI keys are tenant-specific. Each tenant currently has one live, full-access key, managed under Dashboard → Developers → API key. Regenerating it immediately invalidates the previous key, so update every integration as part of the rotation.
The Developers section also shows request metadata, message delivery outcomes, quota usage, and webhook configuration. API request logs retain metadata for 30 days and never include request bodies, query values, phone numbers, IP addresses, or secrets.
Rate Limiting
The API implements rate limiting to prevent abuse. By default, each tenant is limited to 100 requests per minute. When you exceed this limit, the API will respond with a 429 status code.
Endpoints
Message Endpoints
Send a Text Message
POST /messages/text
Send a simple text message to a WhatsApp recipient.
Request
json
{
"recipient": "919876543210",
"message": "Hello! This is a test message from Talking Shops.",
"reference_id": "order_123" // optional
}Response (202 Accepted)
json
{
"message_id": "msg_ABC123XYZ",
"status": "queued",
"timestamp": "2023-06-21T14:35:12Z"
}Send a Media Message
POST /messages/media
Send an image, video, document, or audio file to a WhatsApp recipient.
Request
json
{
"recipient": "919876543210",
"media_type": "image", // "image", "video", "document", or "audio"
"media_url": "https://example.com/images/product.jpg",
"caption": "Check out our new product!", // optional
"reference_id": "product_launch_456" // optional
}Response (202 Accepted)
json
{
"message_id": "msg_DEF456UVW",
"status": "queued",
"timestamp": "2023-06-21T15:40:22Z"
}Send a Template Message
POST /messages/template
Send a pre-approved message template with dynamic content.
Request
json
{
"recipient": "919876543210",
"template_name": "order_confirmation",
"language_code": "en_US",
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "https://example.com/images/logo.jpg"
}
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "John Doe"
},
{
"type": "text",
"text": "12345"
},
{
"type": "currency",
"currency": {
"code": "USD",
"amount": 100.50
}
}
]
}
],
"reference_id": "order_conf_789" // optional
}Response (202 Accepted)
json
{
"message_id": "msg_GHI789RST",
"status": "queued",
"timestamp": "2023-06-21T16:45:33Z",
"language_code": "en_US"
}language_code in the response is the language the template will be sent in.
Template Language
language_code is optional. The API checks the template against your WhatsApp Business Account before queuing the message:
- Omitted: the template's approved language is used. If it's approved in several languages,
en_US(thenen) is preferred; otherwise passlanguage_code. - Base code such as
en: if the template has exactly one approved translation in that language (for exampleen_US), it's used and the response includes awarningsarray saying so. - Exact code: used as given when that translation is approved.
json
{
"message_id": "msg_GHI789RST",
"status": "queued",
"timestamp": "2023-06-21T16:45:33Z",
"language_code": "en_US",
"warnings": [
"Template \"order_confirmation\" has no \"en\" translation; sent in \"en_US\" instead"
]
}Templates that can't be sent are rejected with 400 Bad Request instead of being queued. details.available_languages lists the template's translations and their review status:
json
{
"error": {
"code": "TEMPLATE_LANGUAGE_NOT_FOUND",
"message": "Template \"order_confirmation\" has no approved \"hi\" translation",
"request_id": "req-1a2b",
"details": {
"template_name": "order_confirmation",
"available_languages": [
{ "language": "en_US", "status": "APPROVED" }
]
}
}
}| Error Code | Meaning |
|---|---|
TEMPLATE_NOT_FOUND | No template with this name exists on your WhatsApp Business Account |
TEMPLATE_NOT_APPROVED | The template (or the requested translation) is pending, rejected, paused or disabled |
TEMPLATE_LANGUAGE_NOT_FOUND | The template has no approved translation in the requested language |
TEMPLATE_LANGUAGE_AMBIGUOUS | language_code was omitted and the template is approved in several non-English languages |
The API caches each template lookup. A template that wasn't found is checked again after 1 minute, so a newly created and approved template is accepted within about a minute. A template's translations are cached for 5 minutes, so a translation approved after the template was first used can take up to 5 minutes to be accepted. If the lookup itself is unavailable, requests that include language_code are still queued as given.
Dynamic URL Button Parameters
If an approved template contains a dynamic URL such as https://example.com/orders/1, add a button component with sub_type: "url". The text parameter must contain only the value replacing 1, not the complete resolved URL.
bash
curl --request POST "https://api.talkingshops.com/v1/messages/template" \
--header "Content-Type: application/json" \
--header "x-tenant-api-key: your_api_key_here" \
--data '{
"recipient": "919876543210",
"template_name": "order_reminder",
"language_code": "en_US",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "John Doe" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{ "type": "text", "text": "order-123" }
]
}
]
}'The index is the zero-based position of the button in the approved template. Static URL buttons do not require a button component because they have no dynamic value.
Location Headers
If an approved template has a location header, pass the location as the header parameter. latitude and longitude are required; name and address are optional.
json
{
"type": "header",
"parameters": [
{
"type": "location",
"location": {
"latitude": 12.9716,
"longitude": 77.5946,
"name": "MG Road Store",
"address": "12 MG Road, Bengaluru"
}
}
]
}Templates can be sent outside the 24-hour customer service window, so use a template with a location header to send a location to a customer who hasn't messaged you recently.
Send Authentication Template Message
POST /messages/authentication-template
Authentication templates are specialized WhatsApp templates designed to send one-time passwords (OTP) and verification codes to users. They provide enhanced security features and better user experience compared to regular text messages.
Important: Authentication templates must be created first using the WhatsApp Business Management API (one-time setup). The button type (copy_code, one_tap, zero_tap) is configured during template creation.
Key Features
- Linked Device Security: Authentication messages are only delivered to a user's primary WhatsApp device
- Multiple Button Types: Support for one-tap autofill, copy code, and zero-tap authentication
- Security Recommendations: Optional security warning text
- Code Expiration: Configurable expiration times (1-90 minutes)
- Android App Integration: Deep integration with Android apps for seamless user experience
Quick Reference: Button Types
| Button Type | Platform | User Experience | Setup Complexity | Use Case |
|---|---|---|---|---|
| Copy Code | All platforms | User taps to copy code to clipboard | ✅ Simple | Universal compatibility |
| One-Tap | Android (fallback: copy code) | Automatically opens your app with code | 🔶 Medium | Enhanced Android UX |
| Zero-Tap | Android (fallback: one-tap/copy) | Code appears automatically in app | 🔴 Complex | Invisible authentication |
Platform Compatibility Matrix
| Feature | Android | iOS | Web WhatsApp | WhatsApp Desktop |
|---|---|---|---|---|
| Copy Code | ✅ | ✅ | ✅ | ✅ |
| One-Tap | ✅ | ❌ (fallback to copy) | ❌ (fallback to copy) | ❌ (fallback to copy) |
| Zero-Tap | ✅ | ❌ (fallback to one-tap) | ❌ (fallback to one-tap) | ❌ (fallback to one-tap) |
Request
json
{
"recipient": "919876543210",
"template_name": "auth_code_template",
"language_code": "en_US",
"code": "123456",
"reference_id": "auth-123456"
}Note: The components field is optional and used for API validation only. The actual button behavior is determined by how the template was created in WhatsApp Business Manager.
Examples by Template Type
Copy Code Template:
json
{
"recipient": "919876543210",
"template_name": "auth_copy_code",
"language_code": "en_US",
"code": "567890",
"reference_id": "auth-567890-copy"
}One-Tap Autofill Template:
json
{
"recipient": "919876543210",
"template_name": "auth_one_tap",
"language_code": "en_US",
"code": "789012",
"components": {
"buttons": {
"otp_type": "one_tap",
"supported_apps": [
{
"package_name": "com.example.myapp",
"signature_hash": "K8a/AINcGX7"
}
]
}
},
"reference_id": "auth-789012-onetap"
}Zero-Tap Template:
json
{
"recipient": "919876543210",
"template_name": "auth_zero_tap",
"language_code": "en_US",
"code": "345678",
"components": {
"buttons": {
"otp_type": "zero_tap",
"zero_tap_terms_accepted": true,
"supported_apps": [
{
"package_name": "com.example.myapp",
"signature_hash": "K8a/AINcGX7"
}
]
}
},
"reference_id": "auth-345678-zerotap"
}Request Parameters
Required Parameters:
| Parameter | Type | Description |
|---|---|---|
recipient | string | Recipient phone number (10-15 digits) |
template_name | string | Name of the authentication template (must exist in WhatsApp Business Manager) |
code | string | The OTP/verification code to send (1-15 characters) |
Optional Parameters:
| Parameter | Type | Description |
|---|---|---|
language_code | string | Language code (e.g., en_US). Defaults to the template's approved language; see Template Language |
reference_id | string | Custom reference ID for tracking |
components | object | Optional validation object for API validation only |
Android App Integration
Calculating App Signature Hash:
To use one-tap or zero-tap authentication, you need your Android app's signature hash:
Using Google's Method: Follow Google's instructions
Using Shell Script: Download and use the sms_retriever_hash_v9.sh script:
bash./sms_retriever_hash_v9.sh --package "com.example.myapp" --keystore ~/.android/debug.keystore
Package Name Requirements:
- Must have at least two segments (contain dots)
- Each segment must start with a letter
- Only alphanumeric characters and underscores allowed
- Maximum 224 characters
Multiple Apps Support:
json
"supported_apps": [
{
"package_name": "com.mycompany.myapp",
"signature_hash": "K8a/AINcGX7"
},
{
"package_name": "com.mycompany.myapp.debug",
"signature_hash": "L9b/BIOdHY8"
}
]Response (202 Accepted)
json
{
"message_id": "uuid-generated-id",
"status": "queued",
"timestamp": "2024-12-06T12:34:56Z",
"language_code": "en_US"
}Common Errors
| Error Code | Description | Solution |
|---|---|---|
TEMPLATE_NOT_FOUND | Authentication template doesn't exist | Create the template first |
TEMPLATE_NOT_APPROVED | Template or translation isn't approved yet | Wait for Meta's review, or check its status in WhatsApp Manager |
TEMPLATE_LANGUAGE_NOT_FOUND | No approved translation in language_code | Use a language from details.available_languages, or omit language_code |
INVALID_SIGNATURE_HASH | Signature hash format is incorrect | Verify hash is exactly 11 characters |
INVALID_PACKAGE_NAME | Package name format is invalid | Check package name requirements |
ZERO_TAP_TERMS_NOT_ACCEPTED | Terms not accepted for zero-tap | Set zero_tap_terms_accepted: true |
INVALID_OTP_TYPE | Unknown OTP button type | Use copy_code, one_tap, or zero_tap |
Best Practices
Security:
- Never log or store verification codes in plain text
- Use HTTPS for all API communications
- Implement rate limiting on your authentication endpoints
- Validate codes server-side before accepting them
- Use appropriate expiration times (5-15 minutes recommended)
User Experience:
- Start with copy code templates for maximum compatibility
- Implement one-tap for Android users when possible
- Consider zero-tap only for high-security applications
- Always include security recommendations for sensitive operations
- Use clear, branded template names
Template Management:
- Create templates in all required languages
- Test templates thoroughly before production use
- Monitor template approval status
- Keep template names consistent across languages
- Document your template naming convention
Send a Product List Message
POST /messages/product-list
Send a list of products from the WhatsApp catalog.
json
{
"recipient": "1234567890",
"product_list": {
"catalog_id": "your_catalog_id",
"text": "Check out our latest products:",
"product_items": [
{
"product_retailer_id": "product_001"
},
{
"product_retailer_id": "product_002"
}
]
}
}Response (202 Accepted)
json
{
"message_id": "msg_GHI789RST",
"status": "queued",
"timestamp": "2023-06-21T16:45:33Z"
}Send a Location Message
POST /messages/location
Send a location pin. WhatsApp shows it as a map preview, with the name and address underneath.
Request
json
{
"recipient": "919876543210",
"latitude": 12.9716,
"longitude": 77.5946,
"name": "MG Road Store", // optional
"address": "12 MG Road, Bengaluru", // optional
"reference_id": "store_pin_101" // optional
}| Field | Type | Required | Description |
|---|---|---|---|
latitude | number | Yes | Latitude in decimal degrees, from -90 to 90 |
longitude | number | Yes | Longitude in decimal degrees, from -180 to 180 |
name | string | No | Name of the place (max 1,024 characters) |
address | string | No | Address of the place (max 1,024 characters) |
Response (202 Accepted)
json
{
"message_id": "msg_LOC123ABC",
"status": "queued",
"timestamp": "2023-06-21T16:50:10Z"
}Location messages can only be sent within 24 hours of the customer's last message. Outside that window, use a template with a location header.
Interactive List Messages
Interactive List Messages allow you to present multiple options to users in an organized, scrollable list format.
Endpoint
POST /v1/messages/interactive/listRequest Schema
json
{
"recipient": "1234567890",
"header": {
"type": "text",
"text": "Choose an option"
},
"body": {
"text": "Please select from the following options:"
},
"footer": {
"text": "Powered by TalkingShops"
},
"action": {
"button": "View Options",
"sections": [
{
"title": "Main Categories",
"rows": [
{
"id": "option_1",
"title": "Option 1",
"description": "Description for option 1"
},
{
"id": "option_2",
"title": "Option 2",
"description": "Description for option 2"
}
]
}
]
},
"reference_id": "list_msg_001"
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
recipient | string | Yes | Recipient phone number (10-15 digits) |
header | object | No | Optional header section |
header.type | string | Yes* | Must be "text" |
header.text | string | Yes* | Header text (max 60 characters) |
body | object | Yes | Main message content |
body.text | string | Yes | Body text (1-1024 characters) |
footer | object | No | Optional footer section |
footer.text | string | Yes* | Footer text (max 60 characters) |
action | object | Yes | List action configuration |
action.button | string | Yes | Button text (max 20 characters) |
action.sections | array | Yes | List sections (1-10 sections) |
reference_id | string | No | Custom reference ID for tracking |
Section Object
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Section title (max 24 characters) |
rows | array | Yes | List rows (1-10 rows per section) |
Row Object
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique row ID (max 200 characters) |
title | string | Yes | Row title (max 24 characters) |
description | string | No | Row description (max 72 characters) |
Constraints
- Maximum 10 sections per message
- Maximum 10 rows per section
- Maximum 10 total rows across all sections
- Row IDs must be unique across all sections
Interactive Button Messages
Interactive Button Messages allow you to present up to 3 quick reply buttons for immediate user responses.
Endpoint
POST /v1/messages/interactive/buttonRequest Schema
json
{
"recipient": "1234567890",
"header": {
"type": "text",
"text": "Quick Question"
},
"body": {
"text": "Would you like to continue with your order?"
},
"footer": {
"text": "Choose an option below"
},
"action": {
"buttons": [
{
"type": "reply",
"reply": {
"id": "yes_continue",
"title": "Yes, Continue"
}
},
{
"type": "reply",
"reply": {
"id": "no_cancel",
"title": "No, Cancel"
}
},
{
"type": "reply",
"reply": {
"id": "need_help",
"title": "Need Help"
}
}
]
},
"reference_id": "button_msg_001"
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
recipient | string | Yes | Recipient phone number (10-15 digits) |
header | object | No | Optional header section |
body | object | Yes | Main message content |
body.text | string | Yes | Body text (1-1024 characters) |
footer | object | No | Optional footer section |
footer.text | string | Yes* | Footer text (max 60 characters) |
action | object | Yes | Button action configuration |
action.buttons | array | Yes | Reply buttons (1-3 buttons) |
reference_id | string | No | Custom reference ID for tracking |
Header Types
Interactive Button Messages support multiple header types:
Text Header
json
{
"type": "text",
"text": "Header text (max 60 characters)"
}Image Header
json
{
"type": "image",
"image": {
"link": "https://example.com/image.jpg"
}
}Video Header
json
{
"type": "video",
"video": {
"link": "https://example.com/video.mp4"
}
}Document Header
json
{
"type": "document",
"document": {
"link": "https://example.com/document.pdf",
"filename": "document.pdf"
}
}Button Object
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "reply" |
reply | object | Yes | Reply button configuration |
reply.id | string | Yes | Unique button ID (max 256 characters) |
reply.title | string | Yes | Button title (max 20 characters) |
Constraints
- Maximum 3 buttons per message
- Button IDs must be unique within the message
- Button titles are limited to 20 characters
Location Request Messages
Ask the customer to share their location, for example to get a delivery address. WhatsApp shows your text with a Send location button.
Endpoint
POST /messages/interactive/location-request
Request Schema
json
{
"recipient": "919876543210",
"body": {
"text": "Please share your delivery location so we can find the nearest store."
},
"reference_id": "delivery_location_202" // optional
}| Field | Type | Required | Description |
|---|---|---|---|
body.text | string | Yes | Text shown above the button (max 1,024 characters) |
Returns 202 Accepted with a message_id, like the other message endpoints.
When the customer shares a location, you receive a message.received webhook with type set to location and the coordinates in content.location.
Location requests can only be sent within 24 hours of the customer's last message.
Messages in the Live Chat Inbox
Messages you send through the API can also appear in the customer's open conversation in the Talking Shops live chat inbox, as bot messages, so your team sees both sides of the chat. This is off by default. To turn it on, open Live Chat → Settings (the gear icon), make sure WhatsApp live chat is on, and turn on Show API messages. Leave it off if you only use the API for notifications such as order updates or OTPs.
When it's on, text, media, location, interactive and template messages are added, with their delivery ticks; authentication templates appear without the code. A location appears as its name and address with a Google Maps link.
- Only conversations that are already open are updated. Sending to someone who hasn't messaged you (for example, a template campaign) doesn't create a conversation.
- Messages appear a few seconds after they're sent.
Get Message Status
GET /messages/{messageId}/status
Check the delivery status of a sent message. Supports multiple identifier types - you can use message_id, external_message_id (WhatsApp message ID), or reference_id.
Response (200 OK)
json
{
"message_id": "msg_ABC123XYZ",
"external_message_id": "wamid.ABC123XYZ",
"reference_id": "order_123",
"status": "failed",
"recipient": "919876543210",
"type": "text",
"timestamps": {
"created_at": "2023-06-21T14:35:12Z",
"updated_at": "2023-06-21T14:35:50Z",
"sent_at": "2023-06-21T14:35:15Z",
"delivered_at": null,
"read_at": null,
"status_timestamp": "2023-06-21T14:35:50Z"
},
"status_info": {
"is_delivered": false,
"is_read": false,
"is_failed": true,
"delivery_duration_seconds": null,
"read_duration_seconds": null
},
"error": {
"code": "131047",
"message": "Re-engagement message",
"details": {
"code": 131047,
"title": "Re-engagement message",
"message": "Re-engagement message",
"error_data": {
"details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number."
},
"href": "https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/"
}
},
// Legacy fields for backward compatibility
"timestamp": "2023-06-21T14:35:50Z",
"created_at": "2023-06-21T14:35:12Z",
"updated_at": "2023-06-21T14:35:50Z",
"errors": [
{
"code": "131047",
"message": "Re-engagement message",
"details": {}
}
]
}Status Values
pending: Message is queued for processingprocessing: Message is being processedsent: Message has been sent to WhatsAppdelivered: Message has been delivered to the recipient's deviceread: Message has been read by the recipientfailed: Message delivery failed
Failed sends are retried up to three times, except for errors that can't succeed on retry, such as a missing template (132001), a WhatsApp Business Account payment issue (131042) or a recipient outside the 24-hour window (131047). Those are marked failed after the first attempt, with Meta's error in error.
Features
- Auto-detection of identifier type (message_id, external_message_id, or reference_id)
- Complete message lifecycle timestamps
- Full WhatsApp error information, including Meta's
error_data.detailswhen provided - Boolean status flags for quick checking
- Duration calculations for delivery and read times
Get Bulk Message Status
GET /messages/status/bulk?ids=msg1,msg2,msg3&type=message_id
Query the status of multiple messages at once (up to 100 messages per request).
Query Parameters
ids(required): Comma-separated list of message identifierstype(optional): Type of identifiers -message_id,external_message_id, orreference_id(default:message_id)
Response (200 OK)
json
{
"messages": [
{
"message_id": "msg_ABC123XYZ",
"status": "delivered",
"timestamps": {
"created_at": "2023-06-21T14:35:12Z",
"updated_at": "2023-06-21T14:35:50Z",
"sent_at": "2023-06-21T14:35:15Z",
"delivered_at": "2023-06-21T14:35:45Z",
"read_at": null,
"status_timestamp": "2023-06-21T14:35:45Z"
},
"error": null,
// Legacy fields included
"updated_at": "2023-06-21T14:35:50Z"
}
],
"total": 1,
"requested": 3
}Get Status Updates Since Timestamp
GET /messages/status/updates?since=2023-06-21T14:00:00Z&limit=50
Retrieve messages that have been updated since a specific timestamp. Useful for periodic status polling.
Query Parameters
since(required): ISO timestamp to check for updates sincelimit(optional): Maximum number of messages to return (1-100, default: 50)
Response (200 OK)
json
{
"messages": [
{
"message_id": "msg_ABC123XYZ",
"external_message_id": "wamid.ABC123XYZ",
"reference_id": "order_123",
"status": "delivered",
"timestamps": {
"created_at": "2023-06-21T14:35:12Z",
"updated_at": "2023-06-21T14:35:50Z",
"sent_at": "2023-06-21T14:35:15Z",
"delivered_at": "2023-06-21T14:35:45Z",
"read_at": null,
"status_timestamp": "2023-06-21T14:35:45Z"
},
"error": null
}
],
"total": 1,
"since": "2023-06-21T14:00:00Z",
"has_more": false
}Get Message History
GET /messages
Retrieve a list of messages with optional filtering.
Query Parameters
recipient(string): Filter by recipient phone numberstatus(string): Filter by message status (pending,processing,sent,delivered,read,failed)from_date(ISO 8601): Start dateto_date(ISO 8601): End datelimit(integer, 1-100, default 50): Maximum number of resultsoffset(integer, default 0): Result offset for pagination
Response (200 OK)
json
{
"messages": [
{
"message_id": "msg_ABC123XYZ",
"external_message_id": "wamid.ABC123XYZ",
"reference_id": "order_123",
"recipient": "919876543210",
"type": "text",
"status": "delivered",
"timestamps": {
"created_at": "2023-06-21T14:35:12Z",
"updated_at": "2023-06-21T14:35:50Z",
"sent_at": "2023-06-21T14:35:15Z",
"delivered_at": "2023-06-21T14:35:45Z",
"read_at": null,
"status_timestamp": "2023-06-21T14:35:45Z"
},
"error": null,
// Legacy fields for backward compatibility
"created_at": "2023-06-21T14:35:12Z",
"updated_at": "2023-06-21T14:35:50Z"
},
{
"message_id": "msg_DEF456UVW",
"external_message_id": "wamid.DEF456UVW",
"reference_id": "order_456",
"recipient": "919876543210",
"type": "text",
"status": "failed",
"timestamps": {
"created_at": "2023-06-21T15:00:12Z",
"updated_at": "2023-06-21T15:00:50Z",
"sent_at": "2023-06-21T15:00:15Z",
"delivered_at": null,
"read_at": null,
"status_timestamp": "2023-06-21T15:00:50Z"
},
"error": {
"code": "131047",
"message": "Re-engagement message",
"details": {
"error_data": {
"details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number."
},
"href": "https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/"
}
},
// Legacy fields for backward compatibility
"created_at": "2023-06-21T15:00:12Z",
"updated_at": "2023-06-21T15:00:50Z"
}
],
"total": 125,
"limit": 50,
"offset": 0
}Download Inbound Media
GET /media/{mediaId}
Download an image, video, audio file, document or sticker a customer sent to your number. Use the media id from a message.received webhook (for example content.image.id), or call the download_url in the same object directly.
bash
curl --request GET "https://api.talkingshops.com/v1/media/1425388426435512" \
--header "x-tenant-api-key: your_api_key_here" \
--output invoice.pdfResponse (200 OK)
The file itself, with these headers:
| Header | Value |
|---|---|
Content-Type | The file's type, for example image/jpeg or application/pdf |
Content-Length | The file size in bytes, when WhatsApp provides it |
Content-Disposition | attachment, with the customer's filename for documents, otherwise media-{mediaId}.{ext} |
Errors
| Status | When |
|---|---|
400 | The media id has characters other than letters, digits, _ and - |
404 | The media wasn't sent to your number, has expired, or was received while no message.received webhook was registered |
413 | The file is larger than 100 MB |
502 | WhatsApp couldn't return the file; try again |
- Media can only be downloaded if your
message.receivedwebhook was registered when the customer sent it. - WhatsApp keeps inbound media for 30 days. Download it soon after the webhook arrives, and store your own copy if you need it for longer.
- To check the file, compare its SHA-256 with
content.<type>.sha256from the webhook. WhatsApp sends that value base64-encoded, so encode your digest as base64 before comparing.
Webhook Endpoints
Register a Webhook
POST /webhooks
Register a URL to receive notifications about message status changes or incoming messages.
Request
json
{
"url": "https://your-app.example.com/webhooks/whatsapp",
"events": ["message.status.updated", "message.received"],
"secret": "your_webhook_secret_key"
}Response (201 Created)
json
{
"message": "Webhook registered successfully",
"webhook": {
"id": "webhook_123ABC",
"url": "https://your-app.example.com/webhooks/whatsapp",
"events": ["message.status.updated", "message.received"],
"active": true
}
}List Webhooks
GET /webhooks
Retrieve a list of registered webhooks.
Response (200 OK)
json
{
"webhooks": [
{
"id": "webhook_123ABC",
"url": "https://your-app.example.com/webhooks/whatsapp",
"events": ["message.status.updated", "message.received"],
"active": true,
"created_at": "2023-06-21T12:30:45Z",
"updated_at": "2023-06-21T12:30:45Z",
"last_called_at": "2023-06-21T14:35:50Z",
"failure_count": 0
}
]
}Account Endpoints
Get Quota Information
GET /account/quota
Check your message quota usage.
Response (200 OK)
json
{
"messages": {
"limit": 1000,
"used": 125,
"remaining": 875
},
"reset_date": "2023-07-01T00:00:00Z"
}Manage the tenant API key
Generate, reveal, copy, and regenerate the tenant key from Dashboard → Developers → API key. Key management is intentionally not exposed through the tenant-authenticated public API: a compromised tenant key must not be able to rotate itself.
Webhook Events
When a webhook is triggered, the following data structure is sent to your registered URL:
Message Status Update
json
{
"event": "message.status.updated",
"timestamp": "2023-06-21T14:35:50Z",
"data": {
"message_id": "msg_ABC123XYZ",
"external_message_id": "wamid.ABC123XYZ",
"reference_id": "order_123",
"status": "delivered",
"recipient": "919876543210",
"timestamp": "2023-06-21T14:35:50Z"
}
}Message Received
json
{
"event": "message.received",
"timestamp": "2023-06-21T14:40:12Z",
"data": {
"message_id": "wamid.XYZ987ABC",
"from": "919876543210",
"type": "text",
"timestamp": "2023-06-21T14:40:10Z",
"content": {
"text": {
"body": "Hi, I'd like to place an order."
}
}
}
}When a customer shares a location, type is location and the coordinates are in content.location. name and address are included when WhatsApp provides them, for example when the customer shares a named place.
json
{
"event": "message.received",
"timestamp": "2023-06-21T14:42:05Z",
"data": {
"message_id": "wamid.LOC456DEF",
"from": "919876543210",
"type": "location",
"timestamp": "2023-06-21T14:42:03Z",
"content": {
"location": {
"latitude": 12.9716,
"longitude": 77.5946,
"name": "MG Road Store",
"address": "12 MG Road, Bengaluru"
}
}
}
}When a customer sends an image, video, audio file, document or sticker, type is the media type and content.<type> has the media id and a download_url for downloading the file. caption is included when the customer added one, and documents include the customer's filename.
json
{
"event": "message.received",
"timestamp": "2023-06-21T14:45:20Z",
"data": {
"message_id": "wamid.IMG789GHI",
"from": "919876543210",
"type": "image",
"timestamp": "2023-06-21T14:45:18Z",
"content": {
"image": {
"id": "1368688758464562",
"mime_type": "image/jpeg",
"sha256": "fDcDXEW2LgmXrIKdAYQ6Umz9/qiPj1pqDGA9T1FUV1M=",
"caption": "This is the damaged item",
"download_url": "https://api.talkingshops.com/v1/media/1368688758464562"
}
}
}
}Webhook Security
When you register a webhook, you provide a secret key. We use this key to sign each webhook request with an HMAC-SHA256 signature, included in the X-TS-Signature header. Every delivery also includes a stable X-TS-Event-Id and the payload timestamp in X-TS-Timestamp. Retries reuse the same event ID so your application can safely deduplicate them.
- Get the signature from the
X-TS-Signatureheader - Compute an HMAC-SHA256 signature of the raw request body using your webhook secret
- Compare the signatures using a constant-time comparison function
Example verification in Node.js:
javascript
const crypto = require('crypto');
function verifyWebhook(request, secret) {
const signature = request.headers['x-ts-signature'];
const hmac = crypto.createHmac('sha256', secret);
const computedSignature = hmac.update(request.rawBody).digest('hex');
// Use a constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(computedSignature, 'hex')
);
}Timestamped signature (X-Signature)
Every delivery also carries X-Timestamp, the Unix time in seconds when it was sent, and X-Signature. This signature also covers the time, the method and the URL path, so you can reject a replayed or redirected delivery. It is a hex HMAC-SHA256 with your webhook secret over:
{X-Timestamp}.POST.{path}{?query}.{raw body}{path}{?query} is the path of your webhook URL including its query string, exactly as registered. For https://example.com/hooks/ts?source=talkingshops, the signed text is 1800000000.POST./hooks/ts?source=talkingshops.{"event":...}. Each retry gets a fresh X-Timestamp and signature.
javascript
const crypto = require('crypto');
function verifyTimestampedWebhook(request, secret, maxAgeSeconds = 300) {
const timestamp = request.headers['x-timestamp'];
const signature = request.headers['x-signature'];
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > maxAgeSeconds) return false;
// request.originalUrl is the path with its query string (Express).
const signed = `${timestamp}.${request.method}.${request.originalUrl}.${request.rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
return signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}Error Handling
The API uses standard HTTP status codes and returns errors in a consistent format:
json
{
"error": {
"code": "ErrorType",
"message": "Human-readable error message",
"details": {
// Additional error details, if applicable
}
}
}Common error codes include:
AuthenticationError: Invalid or missing API keyValidationError: Invalid request parametersRateLimitError: Rate limit exceededQuotaExceededError: Message quota exceededResourceNotFoundError: Requested resource not foundWhatsAppAPIError: Error from the WhatsApp Business API
Status Codes
200 OK: Request succeeded201 Created: Resource created successfully202 Accepted: Request accepted for asynchronous processing; it does not mean that WhatsApp delivered the message400 Bad Request: Invalid request parameters401 Unauthorized: Missing or invalid authentication403 Forbidden: Authentication valid but insufficient permissions404 Not Found: Resource not found413 Content Too Large: The requested media is larger than the download limit429 Too Many Requests: Rate limit exceeded500 Internal Server Error: Server error
Best Practices
- Store and reuse message IDs: When you send a message, store the returned
message_idfor future reference in status updates. - Handle webhooks asynchronously: Respond to webhook events quickly (with a 200 response) and process the data asynchronously to avoid timeouts.
- Implement webhook verification: Always verify webhook signatures to ensure security.
- Implement proper error handling: Handle API errors gracefully in your application.
- Use message templates for notifications: Pre-approved templates have higher delivery rates for notifications.
- Monitor your quota: Implement tracking for your message quota to avoid disruptions.
- Separate request and delivery status: Use HTTP status and request IDs to diagnose the API call. Use message status polling or webhooks to determine whether WhatsApp sent, delivered, read, or failed the message.
- Deduplicate retries: Store processed
X-TS-Event-Idvalues and return a successful response when the same event is received again.
Message Types and Content Limitations
Text Messages
- Maximum length: 4,096 characters
Media Messages
- Image: JPEG, PNG (max 5MB)
- Video: MP4, 3GPP (max 16MB)
- Document: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX (max 100MB)
- Audio: AAC, MP4, MPEG, AMR, OGG (max 16MB)
Location Messages
latitudefrom -90 to 90 andlongitudefrom -180 to 180, in decimal degreesnameandaddress: up to 1,024 characters each- Location messages and location requests can only be sent within 24 hours of the customer's last message
Templates
- Must be pre-approved by WhatsApp
- Limited to specific formats and content types
- Dynamic parameters allowed within defined constraints