Skip to content

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/v1

Authentication ​

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_here

API 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 (then en) is preferred; otherwise pass language_code.
  • Base code such as en: if the template has exactly one approved translation in that language (for example en_US), it's used and the response includes a warnings array 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 CodeMeaning
TEMPLATE_NOT_FOUNDNo template with this name exists on your WhatsApp Business Account
TEMPLATE_NOT_APPROVEDThe template (or the requested translation) is pending, rejected, paused or disabled
TEMPLATE_LANGUAGE_NOT_FOUNDThe template has no approved translation in the requested language
TEMPLATE_LANGUAGE_AMBIGUOUSlanguage_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 TypePlatformUser ExperienceSetup ComplexityUse Case
Copy CodeAll platformsUser taps to copy code to clipboard✅ SimpleUniversal compatibility
One-TapAndroid (fallback: copy code)Automatically opens your app with code🔶 MediumEnhanced Android UX
Zero-TapAndroid (fallback: one-tap/copy)Code appears automatically in app🔴 ComplexInvisible authentication
Platform Compatibility Matrix ​
FeatureAndroidiOSWeb WhatsAppWhatsApp 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:

ParameterTypeDescription
recipientstringRecipient phone number (10-15 digits)
template_namestringName of the authentication template (must exist in WhatsApp Business Manager)
codestringThe OTP/verification code to send (1-15 characters)

Optional Parameters:

ParameterTypeDescription
language_codestringLanguage code (e.g., en_US). Defaults to the template's approved language; see Template Language
reference_idstringCustom reference ID for tracking
componentsobjectOptional 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:

  1. Using Google's Method: Follow Google's instructions

  2. 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 CodeDescriptionSolution
TEMPLATE_NOT_FOUNDAuthentication template doesn't existCreate the template first
TEMPLATE_NOT_APPROVEDTemplate or translation isn't approved yetWait for Meta's review, or check its status in WhatsApp Manager
TEMPLATE_LANGUAGE_NOT_FOUNDNo approved translation in language_codeUse a language from details.available_languages, or omit language_code
INVALID_SIGNATURE_HASHSignature hash format is incorrectVerify hash is exactly 11 characters
INVALID_PACKAGE_NAMEPackage name format is invalidCheck package name requirements
ZERO_TAP_TERMS_NOT_ACCEPTEDTerms not accepted for zero-tapSet zero_tap_terms_accepted: true
INVALID_OTP_TYPEUnknown OTP button typeUse copy_code, one_tap, or zero_tap
Best Practices ​

Security:

  1. Never log or store verification codes in plain text
  2. Use HTTPS for all API communications
  3. Implement rate limiting on your authentication endpoints
  4. Validate codes server-side before accepting them
  5. Use appropriate expiration times (5-15 minutes recommended)

User Experience:

  1. Start with copy code templates for maximum compatibility
  2. Implement one-tap for Android users when possible
  3. Consider zero-tap only for high-security applications
  4. Always include security recommendations for sensitive operations
  5. Use clear, branded template names

Template Management:

  1. Create templates in all required languages
  2. Test templates thoroughly before production use
  3. Monitor template approval status
  4. Keep template names consistent across languages
  5. 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
}
FieldTypeRequiredDescription
latitudenumberYesLatitude in decimal degrees, from -90 to 90
longitudenumberYesLongitude in decimal degrees, from -180 to 180
namestringNoName of the place (max 1,024 characters)
addressstringNoAddress 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/list

Request 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 ​

ParameterTypeRequiredDescription
recipientstringYesRecipient phone number (10-15 digits)
headerobjectNoOptional header section
header.typestringYes*Must be "text"
header.textstringYes*Header text (max 60 characters)
bodyobjectYesMain message content
body.textstringYesBody text (1-1024 characters)
footerobjectNoOptional footer section
footer.textstringYes*Footer text (max 60 characters)
actionobjectYesList action configuration
action.buttonstringYesButton text (max 20 characters)
action.sectionsarrayYesList sections (1-10 sections)
reference_idstringNoCustom reference ID for tracking

Section Object ​

ParameterTypeRequiredDescription
titlestringYesSection title (max 24 characters)
rowsarrayYesList rows (1-10 rows per section)

Row Object ​

ParameterTypeRequiredDescription
idstringYesUnique row ID (max 200 characters)
titlestringYesRow title (max 24 characters)
descriptionstringNoRow 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/button

Request 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 ​

ParameterTypeRequiredDescription
recipientstringYesRecipient phone number (10-15 digits)
headerobjectNoOptional header section
bodyobjectYesMain message content
body.textstringYesBody text (1-1024 characters)
footerobjectNoOptional footer section
footer.textstringYes*Footer text (max 60 characters)
actionobjectYesButton action configuration
action.buttonsarrayYesReply buttons (1-3 buttons)
reference_idstringNoCustom 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 ​

ParameterTypeRequiredDescription
typestringYesMust be "reply"
replyobjectYesReply button configuration
reply.idstringYesUnique button ID (max 256 characters)
reply.titlestringYesButton 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
}
FieldTypeRequiredDescription
body.textstringYesText 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 processing
  • processing: Message is being processed
  • sent: Message has been sent to WhatsApp
  • delivered: Message has been delivered to the recipient's device
  • read: Message has been read by the recipient
  • failed: 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.details when 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 identifiers
  • type (optional): Type of identifiers - message_id, external_message_id, or reference_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 since
  • limit (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 number
  • status (string): Filter by message status (pending, processing, sent, delivered, read, failed)
  • from_date (ISO 8601): Start date
  • to_date (ISO 8601): End date
  • limit (integer, 1-100, default 50): Maximum number of results
  • offset (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.pdf
Response (200 OK) ​

The file itself, with these headers:

HeaderValue
Content-TypeThe file's type, for example image/jpeg or application/pdf
Content-LengthThe file size in bytes, when WhatsApp provides it
Content-Dispositionattachment, with the customer's filename for documents, otherwise media-{mediaId}.{ext}
Errors ​
StatusWhen
400The media id has characters other than letters, digits, _ and -
404The media wasn't sent to your number, has expired, or was received while no message.received webhook was registered
413The file is larger than 100 MB
502WhatsApp couldn't return the file; try again
  • Media can only be downloaded if your message.received webhook 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>.sha256 from 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.

  1. Get the signature from the X-TS-Signature header
  2. Compute an HMAC-SHA256 signature of the raw request body using your webhook secret
  3. 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 key
  • ValidationError: Invalid request parameters
  • RateLimitError: Rate limit exceeded
  • QuotaExceededError: Message quota exceeded
  • ResourceNotFoundError: Requested resource not found
  • WhatsAppAPIError: Error from the WhatsApp Business API

Status Codes ​

  • 200 OK: Request succeeded
  • 201 Created: Resource created successfully
  • 202 Accepted: Request accepted for asynchronous processing; it does not mean that WhatsApp delivered the message
  • 400 Bad Request: Invalid request parameters
  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: Authentication valid but insufficient permissions
  • 404 Not Found: Resource not found
  • 413 Content Too Large: The requested media is larger than the download limit
  • 429 Too Many Requests: Rate limit exceeded
  • 500 Internal Server Error: Server error

Best Practices ​

  1. Store and reuse message IDs: When you send a message, store the returned message_id for future reference in status updates.
  2. Handle webhooks asynchronously: Respond to webhook events quickly (with a 200 response) and process the data asynchronously to avoid timeouts.
  3. Implement webhook verification: Always verify webhook signatures to ensure security.
  4. Implement proper error handling: Handle API errors gracefully in your application.
  5. Use message templates for notifications: Pre-approved templates have higher delivery rates for notifications.
  6. Monitor your quota: Implement tracking for your message quota to avoid disruptions.
  7. 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.
  8. Deduplicate retries: Store processed X-TS-Event-Id values 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 ​

  • latitude from -90 to 90 and longitude from -180 to 180, in decimal degrees
  • name and address: 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

Talking Shops product guides and API reference