API reference
Generated from the API itself, so it cannot fall out of date. Read the quickstart first.
Base URL
| URL | Environment |
|---|---|
https://api.waagent.in/v1 | WA AGENT API |
Authentication
Your API key, e.g. wa_live_….
Authorization: Bearer wa_live_YOUR_KEY/messagesSend a template message
Sends one WhatsApp template message to one recipient. Returns as soon as Meta accepts it — acceptance is not delivery, so poll GET /messages/{id} or read the DLR report for that.
Parameters
| Name | In | Description | |
|---|---|---|---|
Idempotency-Key | header | optional | Retrying with the same value returns the original message instead of sending again. |
Request body
| Field | Type | Description | |
|---|---|---|---|
messaging_product | "whatsapp" | required | |
phone_number_id | string | required | Meta's phone number id for the number you are sending FROM. Shown on the WhatsApp Numbers screen. It must be connected — a number in review or paused is refused. |
to | string | required | The recipient, in E.164 including the leading `+`. The + is required: without a country code we would have to guess one, and ten digits is a complete number in several countries. Meta accepts the bare form; we do not, because guessing wrong delivers your message to a stranger.pattern: ^\+[1-9]\d{6,14}$Example: +919876543210 |
type | "template" | "text" | required | template sends an approved template and needs template. text sends a free-form message and needs text; it is only deliverable within 24 hours of the contact last writing to you (otherwise SERVICE_WINDOW_CLOSED or SERVICE_WINDOW_NEVER_OPENED, HTTP 409). A text message counts against your plan quota like a template, and is refunded if delivery fails. |
text | object | optional | Required when type is text. |
body | string | required | |
template | object | optional | |
name | string | optional | The template's name at Meta. Requires language. Provide this or id, not both. |
id | string | optional | The template id from the Templates screen. Unambiguous on its own, so language is not needed with it. |
language | object | optional | Required with name. Must match the template exactly — en and en_US are two different templates at Meta. |
code | string | required | Example: en, en_US |
variables | object<string, string> | required | Values keyed by slot id — see the variables table above. Every slot the template declares must be filled; a missing one is refused rather than sent blank, because Meta would deliver "Hi , your order shipped" to a real person. Example: {"body.1":"Arjit","body.2":"SR-4471","button.0.1":"SR-4471"} |
header_media | object | optional | Your own file for an IMAGE/VIDEO/DOCUMENT header, replacing the one stored with the template, for this message only. Refused on a template with no media header (HEADER_MEDIA_NOT_APPLICABLE). |
link | string | required | A public https URL. Meta fetches it at send time; a dead or slow link fails the message with Meta's own meta_code.pattern: ^https:// |
Example
curl -X POST https://api.waagent.in/v1/messages \
-H "Authorization: Bearer wa_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"phone_number_id": "106540352242922",
"to": "+919876543210",
"type": "text",
"text": {
"body": "Your order has shipped."
}
}'Responses
| Status | Meaning |
|---|---|
200 | Meta accepted the message. |
401 | The API key is missing, malformed, revoked, or suspended. |
402 | The plan quota is used up, or the subscription has lapsed. |
403 | This plan does not include API access. |
404 | No such template, or no stored file for its media header. |
409 | The number or the template cannot send right now. |
422 | The body failed validation, a template variable is missing, or `header_media` was sent for a template with no media header. |
429 | Too many requests on this key. See `Retry-After`. |
502 | Meta refused the message, or the media upload it needed. See `meta_code`. |
/messages/{id}Look up a message
The delivery state of one message. Accepts either the wamid a send returned or our own message_id.
Status moves queued → sent → delivered → read, or to failed. It only ever moves forward: a late webhook cannot walk it back.
Parameters
| Name | In | Description | |
|---|---|---|---|
id | path | required | A wamid.… or a message_id. |
Example
curl -X GET https://api.waagent.in/v1/messages/wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSN0QxRjhCMzk2NkE1RDcyRTVFAA== \
-H "Authorization: Bearer wa_live_YOUR_KEY"Responses
| Status | Meaning |
|---|---|
200 | The message. |
401 | The API key is missing, malformed, revoked, or suspended. |
403 | This plan does not include API access. |
404 | No message with that id on this account. |
429 | Too many requests on this key. |
/media/{id}Download an inbound attachment
The photo, document, audio or video a contact sent. This is the media.url of a message.received webhook event, and {id} is that event's media.id.
The link is stable for as long as the message is retained (30 days), so a late retry of the webhook can still use it — unlike WhatsApp's own media links, which expire within minutes and need a token. Authenticate with your API key, as for every other endpoint.
Usually answers 302 to a short-lived signed download link: follow the redirect, and do not forward your API key to it (most HTTP clients already strip Authorization across origins). Before we have stored our own copy it answers 200 with the file itself. Both are correct; handle both.
Parameters
| Name | In | Description | |
|---|---|---|---|
id | path | required | The media.id from a message.received event. |
Example
curl -X GET https://api.waagent.in/v1/media/65f1c0a2b7d4e83a9c1f0b22 \
-H "Authorization: Bearer wa_live_YOUR_KEY"Responses
| Status | Meaning |
|---|---|
200 | The file, when it is served directly. |
302 | A short-lived signed link to the file. Follow it. |
401 | The API key is missing, malformed, revoked, or suspended. |
403 | This plan does not include API access. |
404 | No attachment with that id on this account. |
409 | Your WhatsApp access was removed. Reconnect your number; do not retry. |
410 | The attachment is no longer available. |
413 | The attachment is over 16 MB and not stored yet. |
429 | Too many requests on this key. |
/numbersList your WhatsApp numbers
Every number on your account, connected or not. phone_number_id is the field POST /messages and GET /numbers itself both key on.
Example
curl -X GET https://api.waagent.in/v1/numbers \
-H "Authorization: Bearer wa_live_YOUR_KEY"Responses
| Status | Meaning |
|---|---|
200 | Your numbers. |
401 | The API key is missing, malformed, revoked, or suspended. |
403 | This plan does not include API access. |
429 | Too many requests on this key. |
/templatesList your templates
Your templates with variables and buttons already computed — see "Numbers and templates" above. Unfiltered and unpaged is the common case for a typical WABA's template catalogue.
Parameters
| Name | In | Description | |
|---|---|---|---|
status | query | optional | Only templates in this status — APPROVED for "what can I send". |
waba_id | query | optional | Only templates on this WABA. From GET /numbers' waba_id. |
page | query | optional | |
page_size | query | optional |
Example
curl -X GET https://api.waagent.in/v1/templates \
-H "Authorization: Bearer wa_live_YOUR_KEY"Responses
| Status | Meaning |
|---|---|
200 | Your templates. |
401 | The API key is missing, malformed, revoked, or suspended. |
403 | This plan does not include API access. |
422 | A query parameter failed validation. |
429 | Too many requests on this key. |
/templates/{id}Look up a template
One template, by our own id (the id field GET /templates returns).
Parameters
| Name | In | Description | |
|---|---|---|---|
id | path | required |
Example
curl -X GET https://api.waagent.in/v1/templates/{id} \
-H "Authorization: Bearer wa_live_YOUR_KEY"Responses
| Status | Meaning |
|---|---|
200 | The template. |
401 | The API key is missing, malformed, revoked, or suspended. |
403 | This plan does not include API access. |
404 | No template with that id on this account. |
429 | Too many requests on this key. |
/leadsCreate a lead from your website's form
For website integrations only. Authenticate with a lead key, never an API key — a lead key (wa_lead_…) can only ever create a lead, so it is safe to use from a browser or a form-builder tool, unlike your messaging API key. Create one under Developer → Website Leads in the customer portal.
The contact lands in your Contacts with source website. The response never says whether the contact was new or already existed, by design.
Request body
| Field | Type | Description | |
|---|---|---|---|
name | string | required | |
phone | string | required | Loosely accepted here and normalised on our side; an unusable number comes back as a 422 your form can show the visitor. Example: 9876543210 |
Example
curl -X POST https://api.waagent.in/v1/leads \
-H "Authorization: Bearer wa_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Arjit",
"phone": "9876543210"
}'Responses
| Status | Meaning |
|---|---|
202 | Accepted. Deliberately generic — the same body whether this created a new contact or matched one you already had. |
401 | The lead key is missing, malformed, revoked, or suspended. |
403 | This key's origin allow-list does not include the caller. |
422 | The body failed validation, or the phone is unusable. |
429 | Too many requests on this key, or from this IP. |
/openapi.jsonNo auth requiredThis document
The machine-readable spec. Import the URL into Postman or Insomnia.
Example
curl -X GET https://api.waagent.in/v1/openapi.json \
-H "Authorization: Bearer wa_live_YOUR_KEY"Responses
| Status | Meaning |
|---|---|
200 | The OpenAPI document. |
Webhooks
message.receivedA contact messaged you
Sent when a contact sends a message to one of your numbers. Subscribe to it when you set up your webhook.
Request headers
| Name | Description | |
|---|---|---|
X-WAAgent-Event | always sent | The event type — the same as type in the body. |
X-WAAgent-Delivery | always sent | The event id. Identical on every retry of the same event, so it is the key to de-duplicate on: you can receive the same event more than once. |
X-WAAgent-Timestamp | always sent | Unix seconds when this attempt was signed. Reject requests more than 5 minutes from your clock. |
X-WAAgent-Signature | always sent | v1= plus the hex HMAC-SHA256 of {timestamp}.{raw body} keyed with your signing secret. During the 24 hours after a secret rotation there are two comma-separated v1= values — accept the request if either matches. |
Payload
| Field | Type | Description | |
|---|---|---|---|
id | string | required | Stable across retries; equals X-WAAgent-Delivery. |
type | "message.received" | required | |
created_at | string | required | When the thing happened, not when this attempt was made. |
data | object | required | |
message | object | optional | |
id | string | optional | Our id for this message. |
wamid | string | optional | WhatsApp's message id. |
type | string | optional | text, image, video, audio, document, sticker, location, contacts, … |
text | stringnull | optional | The body, or the caption of a media message. Null when there is none. |
timestamp | string | optional | When the contact sent it, per WhatsApp. |
reply_to_wamid | stringnull | optional | The message this one replied to, if it was a reply. |
location | objectnull | optional | For type: location: latitude, longitude, name, address. |
contacts | arraynull | optional | For type: contacts: the shared contact cards. |
from | object | optional | Who sent it. Exactly one of phone / user_id is set. |
phone | stringnull | optional | E.164 with a leading +.Example: +919876543210 |
user_id | stringnull | optional | WhatsApp’s username-era identifier, when the contact’s number is hidden. |
name | stringnull | optional | Their WhatsApp profile name. |
to | object | optional | |
phone_number_id | string | optional | Which of your numbers received it — the same id POST /messages takes. |
media | objectnull | optional | Present for an image, video, audio, document or sticker. |
Example
{
"id": "msg_in_65f1c0a2b7d4e83a9c1f0b22",
"type": "message.received",
"created_at": "2026-10-26T10:00:00.000Z",
"data": {
"message": {
"id": "65f1c0a2b7d4e83a9c1f0b22",
"wamid": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgg",
"type": "image",
"text": "Is this the right size?",
"timestamp": "2026-10-26T10:00:00.000Z",
"reply_to_wamid": null,
"location": null,
"contacts": null
},
"from": {
"phone": "+919876543210",
"user_id": null,
"name": "Jane"
},
"to": {
"phone_number_id": "106540352242922"
},
"media": {
"id": "65f1c0a2b7d4e83a9c1f0b22",
"type": "image",
"mime_type": "image/jpeg",
"filename": null,
"url": "https://api.waagent.in/v1/media/65f1c0a2b7d4e83a9c1f0b22"
}
}
}Your response
| Status | Meaning |
|---|---|
200 | Any `2xx` means delivered. Respond quickly (within 10 seconds) and do your work afterwards — anything else, or a timeout, is retried. |
message.statusA message you sent changed state
Sent as a message you sent through this API is sent, delivered, read or fails — once per change. Messages sent from campaigns or the Inbox are not announced here.
Request headers
| Name | Description | |
|---|---|---|
X-WAAgent-Event | always sent | The event type — the same as type in the body. |
X-WAAgent-Delivery | always sent | The event id. Identical on every retry of the same event, so it is the key to de-duplicate on: you can receive the same event more than once. |
X-WAAgent-Timestamp | always sent | Unix seconds when this attempt was signed. Reject requests more than 5 minutes from your clock. |
X-WAAgent-Signature | always sent | v1= plus the hex HMAC-SHA256 of {timestamp}.{raw body} keyed with your signing secret. During the 24 hours after a secret rotation there are two comma-separated v1= values — accept the request if either matches. |
Payload
| Field | Type | Description | |
|---|---|---|---|
id | string | required | Stable across retries; equals X-WAAgent-Delivery. |
type | "message.status" | required | |
created_at | string | required | When the thing happened, not when this attempt was made. |
data | object | required | |
message_id | string | optional | The message_id that POST /messages returned — match this to your own send. |
wamid | string | optional | |
status | "sent" | "delivered" | "read" | "failed" | optional | |
timestamp | string | optional | |
recipient | object | optional | |
phone | stringnull | optional | E.164 with a leading +.Example: +919876543210 |
user_id | stringnull | optional | |
error | objectnull | optional | Set only when status is failed. |
Example
{
"id": "status_wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS_delivered",
"type": "message.status",
"created_at": "2026-10-26T10:00:05.000Z",
"data": {
"message_id": "66f3a1c2e4b0a1d2c3e4f5a6",
"wamid": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS",
"status": "delivered",
"timestamp": "2026-10-26T10:00:05.000Z",
"recipient": {
"phone": "+919876543210",
"user_id": null
},
"error": null
}
}Your response
| Status | Meaning |
|---|---|
200 | Any `2xx` means delivered. Respond quickly (within 10 seconds) and do your work afterwards — anything else, or a timeout, is retried. |
webhook.testA test request
Sent when you save a webhook (with a challenge to echo) and when you press Send test event. Not something you subscribe to.
Request headers
| Name | Description | |
|---|---|---|
X-WAAgent-Event | always sent | The event type — the same as type in the body. |
X-WAAgent-Delivery | always sent | The event id. Identical on every retry of the same event, so it is the key to de-duplicate on: you can receive the same event more than once. |
X-WAAgent-Timestamp | always sent | Unix seconds when this attempt was signed. Reject requests more than 5 minutes from your clock. |
X-WAAgent-Signature | always sent | v1= plus the hex HMAC-SHA256 of {timestamp}.{raw body} keyed with your signing secret. During the 24 hours after a secret rotation there are two comma-separated v1= values — accept the request if either matches. |
Payload
| Field | Type | Description | |
|---|---|---|---|
id | string | required | Stable across retries; equals X-WAAgent-Delivery. |
type | "webhook.test" | required | |
created_at | string | required | When the thing happened, not when this attempt was made. |
data | object | required | |
message | string | optional | |
challenge | string | optional | Only on the request sent when you save a webhook. Answer 2xx with JSON {"challenge": "<this value>"}; the URL is only stored if you do. |
Example
{
"id": "evt_test_0b7f2c1e-5a1d-4c88-9a43-2f4d8e6b7a10",
"type": "webhook.test",
"created_at": "2026-10-26T10:00:00.000Z",
"data": {
"message": "Verification request from WA AGENT. Respond with HTTP 200 and the JSON body {\"challenge\": \"<value below>\"}.",
"challenge": "9f2c6a41d3b84e07a5c1e2f3b4d5a697"
}
}Your response
| Status | Meaning |
|---|---|
200 | Any `2xx` means delivered. Respond quickly (within 10 seconds) and do your work afterwards — anything else, or a timeout, is retried. |