# WA AGENT developer documentation Source: https://waagent.in/docs and https://waagent.in/docs/api --- # WA AGENT API — Quickstart Send WhatsApp template messages from your own software over HTTP. ## Get an API key Open **API Keys** in your portal and create one. Name it after the system that will use it — that name is how you will know which key to revoke later. The key is shown **once**. We store a one-way hash of it, so we genuinely cannot show it to you again; if you lose it, revoke it and create another. Put it in your environment, never in your source control. Every request carries it as a bearer token: > **Warning:** Call this API from your server, never from a browser or a mobile app. A key in client-side code is a key you have published — anyone who can open your page can read it and send messages as you. **Header** ```http Authorization: Bearer wa_live_YOUR_KEY ``` ## Send a template message One endpoint, one recipient per call: `POST /v1/messages`. You need two things — the `phone_number_id` of the number you are sending from (shown on your **WhatsApp Numbers** screen) and an **approved** template. WhatsApp does not let you send arbitrary text to someone who has not messaged you first. That is why every send here is a template, and why the template has to be approved by Meta before it will go anywhere. **cURL** ```bash curl -X POST https://api.waagent.in/v1/messages \ -H "Authorization: Bearer wa_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-4471-shipped" \ -d '{ "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "order_shipped", "language": { "code": "en" }, "variables": { "body.1": "Arjit", "body.2": "SR-4471", "button.0.1": "SR-4471" } } }' ``` **Node.js** ```javascript const response = await fetch("https://api.waagent.in/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.WA_AGENT_API_KEY}`, "Content-Type": "application/json", // Safe to retry: the same key never sends twice. "Idempotency-Key": `order-${orderId}-shipped`, }, body: JSON.stringify({ messaging_product: "whatsapp", phone_number_id: "106540352242922", to: "+919876543210", type: "template", template: { name: "order_shipped", language: { code: "en" }, variables: { "body.1": "Arjit", "body.2": "SR-4471", "button.0.1": "SR-4471", }, }, }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error.code); console.log(data.messages[0].id); // wamid.HBgM… ``` **Python** ```python import os import requests response = requests.post( "https://api.waagent.in/v1/messages", headers={ "Authorization": f"Bearer {os.environ['WA_AGENT_API_KEY']}", "Idempotency-Key": "order-4471-shipped", }, json={ "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "order_shipped", "language": {"code": "en"}, "variables": { "body.1": "Arjit", "body.2": "SR-4471", "button.0.1": "SR-4471", }, }, }, timeout=20, ) response.raise_for_status() print(response.json()["messages"][0]["id"]) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("WA_AGENT_API_KEY"), "Content-Type: application/json", "Idempotency-Key: order-4471-shipped", ], CURLOPT_POSTFIELDS => json_encode([ "messaging_product" => "whatsapp", "phone_number_id" => "106540352242922", "to" => "+919876543210", "type" => "template", "template" => [ "name" => "order_shipped", "language" => ["code" => "en"], "variables" => [ "body.1" => "Arjit", "body.2" => "SR-4471", "button.0.1" => "SR-4471", ], ], ]), ]); $data = json_decode(curl_exec($ch), true); echo $data["messages"][0]["id"]; ``` ## Send a template with an image, video or document A template whose header is an image, video or document sends the file you uploaded when you created it — send nothing extra and that is what goes out. To send a **different file for this one message** — this customer's invoice, their ticket, the photo of their order — add `header_media` beside `variables`, with a `link` to the file. The link must be a public `https` URL, because Meta fetches the file itself when it sends. Its type has to match the header: JPEG or PNG for an image, MP4 or 3GPP for a video, PDF for a document. A link we cannot reach, or the wrong type, fails that message with Meta's own `meta_code`. For a template created **outside WA AGENT** — directly in WhatsApp Manager — there is no stored file to fall back on, so `header_media` is required. `GET /v1/templates` tells you in advance: `header_media.has_stored_file` is `false`. Sending `header_media` for a template with no media header is refused with `HEADER_MEDIA_NOT_APPLICABLE`. The example fills everything such a template can take at once: the document header, two body variables (`Hi {{1}}, your invoice for order {{2}} is attached.`) and the URL button's variable (`https://shop.example.com/orders/{{1}}`). **cURL** ```bash curl -X POST https://api.waagent.in/v1/messages \ -H "Authorization: Bearer wa_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-4471-invoice" \ -d '{ "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "order_invoice", "language": { "code": "en" }, "variables": { "body.1": "Arjit", "body.2": "SR-4471", "button.0.1": "SR-4471" }, "header_media": { "link": "https://files.example.com/invoices/SR-4471.pdf" } } }' ``` **Node.js** ```javascript const response = await fetch("https://api.waagent.in/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.WA_AGENT_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `order-${orderId}-invoice`, }, body: JSON.stringify({ messaging_product: "whatsapp", phone_number_id: "106540352242922", to: "+919876543210", type: "template", template: { name: "order_invoice", language: { code: "en" }, variables: { "body.1": "Arjit", // Hi {{1}}, "body.2": "SR-4471", // your invoice for order {{2}} is attached. "button.0.1": "SR-4471", // https://shop.example.com/orders/{{1}} }, // Your own file for the document header — public https, fetched by Meta. header_media: { link: `https://files.example.com/invoices/${orderId}.pdf` }, }, }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error.code); console.log(data.messages[0].id); // wamid.HBgM… ``` **Python** ```python import os import requests response = requests.post( "https://api.waagent.in/v1/messages", headers={ "Authorization": f"Bearer {os.environ['WA_AGENT_API_KEY']}", "Idempotency-Key": "order-4471-invoice", }, json={ "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "order_invoice", "language": {"code": "en"}, "variables": { "body.1": "Arjit", "body.2": "SR-4471", "button.0.1": "SR-4471", }, "header_media": { "link": "https://files.example.com/invoices/SR-4471.pdf", }, }, }, timeout=20, ) response.raise_for_status() print(response.json()["messages"][0]["id"]) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("WA_AGENT_API_KEY"), "Content-Type: application/json", "Idempotency-Key: order-4471-invoice", ], CURLOPT_POSTFIELDS => json_encode([ "messaging_product" => "whatsapp", "phone_number_id" => "106540352242922", "to" => "+919876543210", "type" => "template", "template" => [ "name" => "order_invoice", "language" => ["code" => "en"], "variables" => [ "body.1" => "Arjit", "body.2" => "SR-4471", "button.0.1" => "SR-4471", ], "header_media" => [ "link" => "https://files.example.com/invoices/SR-4471.pdf", ], ], ]), ]); $data = json_decode(curl_exec($ch), true); echo $data["messages"][0]["id"]; ``` ## Add a lead form to your website A different job from everything above: instead of your server sending a message, your **website's own form** posts a visitor's name and phone straight to us, and they land in your Contacts with source `website`. This uses a separate credential, a **lead key** — create one under **Developer → Website Leads** in your portal. A lead key can only ever create a lead; it cannot send a message or read anything. That is what makes it safe to put in your website's own code, unlike your API key. Optionally restrict a lead key to your own domain when you create it. If it ever leaks — checked into a public repo, visible in your page source, whatever — the worst it can do is let someone submit junk leads, which is rate-limited on our side either way. > **Warning:** Never put your wa_live_ API key here. That key can send billed messages; a lead key can only create a lead. Using the wrong one in your website's public code hands a stranger the ability to send messages on your account. **Browser fetch** ```javascript // In your website's own page, or a script it loads. const response = await fetch("https://api.waagent.in/v1/leads", { method: "POST", headers: { Authorization: "Bearer wa_lead_YOUR_LEAD_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ name: document.querySelector("#name").value, phone: document.querySelector("#phone").value, }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error.code); console.log(data.status); // "received" ``` **cURL** ```bash curl -X POST https://api.waagent.in/v1/leads \ -H "Authorization: Bearer wa_lead_YOUR_LEAD_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Arjit", "phone": "9876543210" }' ``` ## Phone numbers The `to` field must be in E.164 **including the leading `+`** — `+919876543210`, not `919876543210`. We refuse the bare form on purpose, and it is the one place we are deliberately stricter than Meta. Without a `+` there is no country code to read, and ten digits is a complete national number in India *and* a complete number including its country code in Singapore, Norway and Denmark. Guessing wrong does not fail — it delivers your message to a stranger, and that cannot be undone. ## Filling in template variables Where Meta wants a nested `components` array, we take a flat `variables` map keyed by **slot id**. The id says where in the template the value goes, so you never have to get Meta's parameter *ordering* right — the mistake that silently swaps two customers' values. Every variable your template declares must be filled. A missing one is refused rather than sent blank — Meta would happily deliver "Hi , your order has shipped" to a real person. `header.1` covers a plain text header. A LOCATION header is `header.location`, taking one JSON **string** value — `{"latitude","longitude","name","address"}`. A media header (image, video or document) is **not** a `variables` entry — anything sent under `header.media` is ignored. Leave it out to send the template's stored file, or pass your own with `header_media` — see **Send a template with an image, video or document** above. The number in `button..` is the button's position in the template's **full** button list — starting at 0, counting every button on it, whatever type each one is — not a count of URL buttons alone. Only a URL button ever carries a variable. So a template whose buttons are, in order, a quick reply and then a URL button has its variable at `button.1.1`, not `button.0.1`. You never have to count: `GET /v1/templates` — or the template's **For the API** card in your portal — lists every button with the exact slot id to use. > **Note:** A URL button value must be percent-encoded by you. Meta appends it to the approved URL rather than substituting it in place, so “Gonçalves” has to arrive as “Gon%C3%A7alves”. | Slot id | Where the value goes | | --- | --- | | `body.1` | the first `{{1}}` in the body | | `body.2` | the second, and so on | | `body.first_name` | a named `{{first_name}}` in the body | | `header.1` | the variable in a text header | | `header.location` | a LOCATION header's value | | `button.0.1` | a URL button's variable — see below for how the number is chosen | | `code` | the one-time code, for an authentication template | | `header_media.link` | not a slot — a field beside `variables`: your own file for an image, video or document header | ## Sending a one-time code An authentication template takes exactly one variable, `code`. Meta owns the wording of the message itself, so there is nothing else to fill in. The code is masked before it is written to our database. It appears in no report, no log and no export — including to us. That is a platform rule, not a setting. **Body** ```json { "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "login_code", "language": { "code": "en" }, "variables": { "code": "483920" } } } ``` ## Retrying safely Send an `Idempotency-Key` header and a retry with the same value returns the original message instead of sending a second one. Use something stable from your own system — an order id, an invoice number — rather than a random string, so a retry after a timeout actually matches. Without the header, every request sends. That is deliberate: an OTP resend is a genuine second message, and silently swallowing it would be the wrong answer. A replayed request answers `200` with the original `id` and `"deduplicated": true`. ## Checking delivery A `200` means Meta **accepted** the message, not that it arrived. To find out what happened next, poll `GET /v1/messages/{id}` with the `id` the send returned. Status moves `queued` → `sent` → `delivered` → `read`, or to `failed`. It only ever moves forward, so a late webhook from Meta can never walk it backwards. You can also pass the `message_id` we return alongside it — that one exists even for a message Meta refused, which has no `wamid` at all. **cURL** ```bash curl https://api.waagent.in/v1/messages/wamid.HBgM… \ -H "Authorization: Bearer wa_live_YOUR_KEY" ``` **Response** ```json { "id": "wamid.HBgM…", "message_id": "66f3a1c2e4b0a1d2c3e4f5a6", "status": "delivered", "to": "+919876543210", "template_name": "order_shipped", "error": null, "sent_at": "2026-09-09T10:12:01.000Z", "delivered_at": "2026-09-09T10:12:04.000Z", "read_at": null, "failed_at": null } ``` ## When something goes wrong Every failure has the same shape. Branch on `error.code`, which is a stable string — never on the message, which we may reword. When Meta itself refused the message, `meta_code` carries their own numeric code, which you can look up in Meta's Cloud API error reference. `131026`, for instance, means the number is not on WhatsApp — and it is the only way to find that out, since no API can tell you in advance. The full code list is in the API reference. **Error** ```json { "error": { "code": "MESSAGE_VARIABLES_INCOMPLETE", "message": "Missing values for template variable(s): body.2", "request_id": "0f0b1c9a-…" } } ``` ## Rate limits and quotas Two separate ceilings. Requests are limited per key per minute — every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, and a `429` adds `Retry-After`. Watch `X-RateLimit-Remaining` and slow down before you are refused. Separately, your plan caps messages per month, day and hour. A send counts when WhatsApp accepts it, and is given back if WhatsApp later reports it failed or not charged for — a utility template inside an open 24-hour service window, for example. Hitting a cap answers `402` with `QUOTA_EXCEEDED`, and no amount of backing off will clear it — you need a larger plan. --- # WA AGENT API — Reference Version 1.0.0 Send WhatsApp template messages from your own software. ## Authenticating Every request carries your API key as a bearer token. Keys start with `wa_live_`, are shown in full exactly once when you create them, and are managed under **API Keys** in the customer portal. ``` Authorization: Bearer wa_live_xxxxxxxxxxxxxxxxxxxx ``` **This API is server-to-server.** It sends no CORS headers to arbitrary origins, and you should never call it from a browser or a mobile app — an API key in client-side code is a key you have published. Call it from your backend. ## Template variables Where Meta wants a nested `components` array, this API takes a flat `variables` map keyed by slot id. The ids mirror where the placeholder sits in the template: | Slot id | Where it goes | | --- | --- | | `body.1` | the first `{{1}}` in the body | | `body.first_name` | a named `{{first_name}}` in the body | | `header.1` | the text header — Meta allows at most one variable in a header | | `header.location` | a LOCATION header — a JSON **string** of `{"latitude","longitude","name","address"}` | | `button.0.1` | a URL button's variable — see below for how the number is chosen | | `code` | the one-time code, for an `AUTHENTICATION` template | **A media header (image, video or document) is not a `variables` entry.** Do not send a `header.media` key — any value there is ignored. Instead, either: - send nothing, and the platform attaches the file uploaded when the template was created (resolving a fresh Meta media id itself, the same way the Campaigns and Inbox screens do); or - send `template.header_media: { "link": "https://…" }` to use **your own file for this message** — an invoice PDF, a ticket, a product photo. Meta fetches it from that URL, so it must be publicly reachable over `https`, and its type must match the header (JPEG/PNG for an image, MP4/3GPP for a video, PDF for a document). A template synced from WhatsApp Manager has no stored file, so for one of those `header_media` is required. A template mixes positional and named variables never — Meta forbids it — so a given template uses one style throughout. **The number in `button..` is the button's position in the template's FULL button list — starting at 0, counting every button, whatever type each one is.** It is not a count of URL buttons alone. A template can mix quick-reply, phone-number and URL buttons in one list (Meta only requires quick-replies to sit together, before or after the others), and only a URL button ever carries a variable. So a template whose buttons are `[Quick reply, Call now, Track order]` — where "Track order" is the one URL button — has its variable at `button.2.1`, not `button.0.1`. Call `GET /templates` or `GET /templates/{id}` rather than counting buttons by hand — the response already carries `variables` (every slot id this endpoint will accept, precomputed) and `buttons` (each button's position and type), so `buttons[2].type === "URL"` is what makes `button.2.1` the right id for that example. **A URL button value must be percent-encoded by you.** Meta appends it to the approved URL rather than substituting it, so `Gonçalves` has to arrive as `Gon%C3%A7alves`. ## Numbers and templates `GET /numbers` lists every WhatsApp number on your account, connected or not — the `phone_number_id` field names the one every other endpoint takes. A number that is not `"sendable"` still shows up rather than disappearing: `POST /messages` against one refuses with `WHATSAPP_NUMBER_NOT_SENDABLE`, and this is how you tell "wrong id" from "this number needs attention in the portal". `GET /templates` and `GET /templates/{id}` list your templates with their `variables` and `buttons` already computed — this is the intended way to learn a template's slot ids and button layout instead of reading them off the Templates screen. A template also shows up here regardless of status, the same "readable is not sendable" split as `/numbers`: a `PENDING` or `REJECTED` template is visible (with `rejected_reason` when there is one) so you can tell it apart from a typo'd id, but `POST /messages` still refuses anything that is not `APPROVED` with `TEMPLATE_NOT_SENDABLE`. An `AUTHENTICATION` template's `one_time_use_only` is `true` and its `variables` is always the single `code` slot — never the scanned `body.*` slots a template of that category would otherwise appear to have. ## Idempotency Send an `Idempotency-Key` header and a retry with the same key returns the original message's id instead of sending a second one. Without the header every request sends. Keys are scoped to your account, so they need only be unique to you. ## Rate limits Requests are limited per key per minute. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; a `429` adds `Retry-After`. Your plan also caps messages per month, day and hour — those refuse with `QUOTA_EXCEEDED`. ## Website leads `POST /leads` is the one exception to everything above: it uses a separate **lead key** (`wa_lead_…`), not your messaging API key, and it is the one endpoint that accepts requests from a browser. Create a lead key under **Developer → Website Leads**, optionally restricted to your site's own origin, and use it in your website's own form — never your `wa_live_…` key, which can send billed messages and must stay on your server. ## Servers | URL | Description | | --- | --- | | `https://api.waagent.in/v1` | WA AGENT API | ## Endpoints ### POST /messages **Send 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. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | no | Retrying with the same value returns the original message instead of sending again. | **Request** ```bash 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": "template", "template": { "name": "order_shipped", "language": { "code": "en" }, "variables": { "body.1": "Arjit", "button.0.1": "SR-4471" } } }' ``` **Request body** ```json { "type": "object", "required": [ "messaging_product", "phone_number_id", "to", "type", "template" ], "properties": { "messaging_product": { "type": "string", "enum": [ "whatsapp" ] }, "phone_number_id": { "type": "string", "description": "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": { "type": "string", "pattern": "^\\+[1-9]\\d{6,14}$", "description": "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.", "examples": [ "+919876543210" ] }, "type": { "type": "string", "enum": [ "template" ] }, "template": { "type": "object", "required": [ "variables" ], "properties": { "name": { "type": "string", "description": "The template's name at Meta. Requires `language`. Provide this or `id`, not both." }, "id": { "type": "string", "description": "The template id from the Templates screen. Unambiguous on its own, so `language` is not needed with it." }, "language": { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "examples": [ "en", "en_US" ] } }, "description": "Required with `name`. Must match the template exactly — `en` and `en_US` are two different templates at Meta." }, "variables": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 1024 }, "description": "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.", "examples": [ { "body.1": "Arjit", "body.2": "SR-4471", "button.0.1": "SR-4471" } ] }, "header_media": { "type": "object", "additionalProperties": false, "required": [ "link" ], "description": "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`).", "properties": { "link": { "type": "string", "format": "uri", "maxLength": 2048, "pattern": "^https://", "description": "A public `https` URL. Meta fetches it at send time; a dead or slow link fails the message with Meta's own `meta_code`." } } } } } } } ``` **Example — By template name** ```json { "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "order_shipped", "language": { "code": "en" }, "variables": { "body.1": "Arjit", "button.0.1": "SR-4471" } } } ``` **Example — A one-time code** An AUTHENTICATION template takes exactly one variable, `code`. Meta owns the wording of the message. The code is masked before it is written to our database, so it appears in no report and no log. ```json { "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "login_code", "language": { "code": "en" }, "variables": { "code": "483920" } } } ``` **Example — A media-header template** An IMAGE/VIDEO/DOCUMENT header needs no value from you — omit it from `variables` entirely. The platform attaches the file you uploaded when creating the template, resolving a fresh Meta media id if the cached one has expired. To send your own file instead, add `header_media` — see the next example. ```json { "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "shipping_update_with_photo", "language": { "code": "en" }, "variables": { "body.1": "Arjit" } } } ``` **Example — Media header + body + URL button — your own file** A DOCUMENT-header template with body variables and a URL-button variable, sending the caller's own PDF for this one message. The template is `Hi {{1}}, your invoice for order {{2}} is attached.` with a URL button `https://shop.example.com/orders/{{1}}`. `header_media` sits beside `variables`, not inside it. The link must be a public `https` URL whose file type matches the header (JPEG/PNG, MP4/3GPP, or PDF); Meta fetches it at send time. Required when `GET /templates` shows `header_media.has_stored_file: false`. ```json { "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "order_invoice", "language": { "code": "en" }, "variables": { "body.1": "Arjit", "body.2": "SR-4471", "button.0.1": "SR-4471" }, "header_media": { "link": "https://files.example.com/invoices/SR-4471.pdf" } } } ``` **Example — A location-header template** A LOCATION header takes one variable, at `header.location`, as a JSON **string** (not an object). A malformed value — not valid JSON, or missing `latitude`/`longitude` — is refused with `MESSAGE_VARIABLES_INCOMPLETE`, naming the slot. ```json { "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "store_nearby", "language": { "code": "en" }, "variables": { "header.location": "{\"latitude\":\"12.9716\",\"longitude\":\"77.5946\",\"name\":\"Store\"}" } } } ``` **Example — A URL button that isn't the template's first button** This template's buttons are, in order, a quick reply and then a URL button — so the URL button is at position **1**, not 0, and its variable is `button.1.1`. The number always counts every button on the template, never just the URL ones. ```json { "messaging_product": "whatsapp", "phone_number_id": "106540352242922", "to": "+919876543210", "type": "template", "template": { "name": "order_shipped_with_options", "language": { "code": "en" }, "variables": { "body.1": "Arjit", "button.1.1": "SR-4471" } } } ``` | 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`. | ### GET /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. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | A `wamid.…` or a `message_id`. | **Request** ```bash curl -X GET https://api.waagent.in/v1/messages/wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSN0QxRjhCMzk2NkE1RDcyRTVFAA== \ -H "Authorization: Bearer wa_live_YOUR_KEY" ``` | 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. | ### GET /numbers **List 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. **Request** ```bash curl -X GET https://api.waagent.in/v1/numbers \ -H "Authorization: Bearer wa_live_YOUR_KEY" ``` | 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. | ### GET /templates **List 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. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `status` | query | no | Only templates in this status — `APPROVED` for "what can I send". | | `waba_id` | query | no | Only templates on this WABA. From `GET /numbers`' `waba_id`. | | `page` | query | no | | | `page_size` | query | no | | **Request** ```bash curl -X GET https://api.waagent.in/v1/templates \ -H "Authorization: Bearer wa_live_YOUR_KEY" ``` | 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. | ### GET /templates/{id} **Look up a template** One template, by our own id (the `id` field `GET /templates` returns). | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | **Request** ```bash curl -X GET https://api.waagent.in/v1/templates/{id} \ -H "Authorization: Bearer wa_live_YOUR_KEY" ``` | 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. | ### POST /leads **Create 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** ```bash 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" }' ``` **Request body** ```json { "type": "object", "required": [ "name", "phone" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "phone": { "type": "string", "description": "Loosely accepted here and normalised on our side; an unusable number comes back as a 422 your form can show the visitor.", "examples": [ "9876543210" ] } } } ``` **Example — lead** ```json { "name": "Arjit", "phone": "9876543210" } ``` | 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. | ### GET /openapi.json **This document** The machine-readable spec. Import the URL into Postman or Insomnia. **Request** ```bash curl -X GET https://api.waagent.in/v1/openapi.json \ -H "Authorization: Bearer wa_live_YOUR_KEY" ``` | Status | Meaning | | --- | --- | | `200` | The OpenAPI document. |