API reference
Notifications API
The recommended surface: send an event and let the platform choose the template.
Six endpoints. You send an event key and named variables; the platform picks the approved template, maps your names onto the positional {{1}}…{{n}} parameters, builds the components, including the extra copy-code button authentication templates require, and handles the 24-hour window.
Use this for anything you tell a customer about their order. The low-level messages API makes it easy to build a send that Meta silently refuses.
Before your first send
Nothing sends until Meta approves your templates. On a new account, every send returns 409 TEMPLATE_NOT_APPROVED until you have been through this once:
Submit the catalog
POST /v1/notifications/templates/submitwith an empty body{}submits all 18 events under your number's WABA.Poll until they are approved
GET /v1/notifications/events?refresh=trueuntilreadyis18. Authentication templates usually clear in seconds; the rest take minutes to a few hours.Send
POST /v1/notifications/send. It works whether or not the customer has ever messaged you, because it goes out as an approved template.
Event catalog
Returns the full event catalog. No phone number is needed, so you can wire an integration up before a number is linked, useful for generating your own field mappings at build time.
Parameters: none.
Response 200, array of
| Field | Required | Type | Description |
|---|---|---|---|
| event | Yes | string | The event key you pass to /send.Example: order_shipped |
| title | Yes | string | Human-readable name. |
| description | Yes | string | When the event is sent. |
| group | Yes | string | Authentication, Orders, Shipping, Payments & Returns or Marketing. |
| category | Yes | string | MARKETING, UTILITY or AUTHENTICATION. This decides the price. |
| templateName | Yes | string | The Meta template name behind the event. Identical to event for every event except order_status, whose template is order_status_update. |
| language | Yes | string | Template language. en_US throughout the shipped catalog. |
| variables | Yes | string[] | Variable names in positional order, index 0 is {{1}}. |
| variableDocs | Yes | object | { variableName: description }. |
| preview | Yes | string | The exact template body, with {{n}} placeholders still in it. |
[
{
"event": "order_shipped",
"title": "Order Shipped",
"description": "Sent when an order is dispatched with a tracking / AWB code.",
"group": "Shipping",
"category": "UTILITY",
"templateName": "order_shipped",
"language": "en_US",
"variables": ["orderId", "awbCode", "trackingLink"],
"variableDocs": {
"orderId": "Human-readable order id",
"awbCode": "Carrier AWB / tracking number",
"trackingLink": "Carrier tracking link"
},
"preview": "🚚 Good news, your order *{{1}}* has shipped!\n\nTracking number: {{2}}\nTrack your shipment: {{3}}\n\nIt is on its way to you."
}
]Errors: 401 and 429 only.
The same content, laid out for reading, is on the event catalog page.
Readiness
Which events can actually be delivered right now on a given number, plus any blocker that applies to the number itself. Check this first when notifications are not arriving.
Query parameters
| Field | Required | Type | Description |
|---|---|---|---|
| phoneNumberId | No | string | Meta phone number id. Omit to use the account's oldest number that has an access token. |
| refresh | No | string | Pass true to re-read approval status from Meta before answering. Any other value is ignored. |
curl "https://whatsapp-api.growcord.in/api/v1/notifications/events?phoneNumberId=1030974986765331&refresh=true" \
-H "Authorization: Bearer sk_live_xxx"Response 200
| Field | Required | Type | Description |
|---|---|---|---|
| phoneNumberId | Yes | string | The number this readiness applies to. |
| total | Yes | integer | Events in the catalog. Always 18. |
| ready | Yes | integer | How many have an APPROVED template. |
| health.canSend | Yes | boolean | false when the number itself cannot send at all, whatever the template state. |
| health.blockers | Yes | string[] | One sentence per blocker, each naming the fix. |
| events[] | Yes | array | Every catalog field, plus the three below. |
| events[].status | Yes | string | APPROVED, PENDING, REJECTED or NOT_CREATED. |
| events[].statusReason | No | string | null | Meta's rejection reason, when there is one. |
| events[].ready | Yes | boolean | true only when status is APPROVED. |
{
"phoneNumberId": "1030974986765331",
"total": 18,
"ready": 17,
"health": { "canSend": true, "blockers": [] },
"events": [
{
"event": "order_confirmation",
"title": "Order Confirmation",
"description": "Sent immediately after a customer places an order.",
"group": "Orders",
"category": "UTILITY",
"templateName": "order_confirmation",
"language": "en_US",
"variables": ["customerName", "orderId", "orderTotal", "trackingLink"],
"variableDocs": {
"customerName": "Customer name",
"orderId": "Human-readable order id",
"orderTotal": "Order total, formatted (e.g. 1,499.00)",
"trackingLink": "Link where the customer can track the order"
},
"preview": "Hi {{1}}, your order *{{2}}* is confirmed! ✅…",
"status": "APPROVED",
"statusReason": null,
"ready": true
}
]
}{
"health": {
"canSend": false,
"blockers": [
"This number is not registered on the WhatsApp Cloud API. Register it with a 6-digit two-step PIN (Phone Numbers → Register), until then Meta rejects every send with error 133010."
]
}
}Errors. 404 when no number matched, or the account has none configured:
{ "error": "No configured WhatsApp number on this account. Link one in the dashboard first." }{ "error": "Phone number not found or not configured" }Submitting the catalog
Submits catalog templates to Meta for approval. Every field is optional, an empty body {} submits the whole catalog under the account's default number.
Body parameters
| Field | Required | Type | Description |
|---|---|---|---|
| phoneNumberId | No | string | Which number's WABA to submit under. Default: the account's oldest configured number |
| events | No | string[] | Restrict the run to these event keys. Default: the whole catalog |
| onlyMissing | No | boolean | true skips events already APPROVED or PENDING; rejected ones are re-submitted. false re-submits everything.Default: true |
curl -X POST https://whatsapp-api.growcord.in/api/v1/notifications/templates/submit \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "1030974986765331",
"events": ["order_confirmation", "order_shipped"],
"onlyMissing": true
}'Response 200
| Field | Required | Type | Description |
|---|---|---|---|
| success | Yes | boolean | Always true when the call itself completed. It does not mean Meta approved anything. |
| phoneNumberId | Yes | string | The number submitted under. |
| submitted | Yes | integer | Newly submitted to Meta. |
| existing | Yes | integer | Meta already had a template with that name. |
| failed | Yes | integer | Meta rejected the submission. |
| results[] | Yes | array | One entry per template attempted. |
| results[].event | Yes | string | Event key. |
| results[].templateName | Yes | string | Meta template name. |
| results[].category | Yes | string | Template category. |
| results[].state | Yes | string | submitted, already_exists or failed. |
| results[].status | No | string | Meta's status. Present on submitted, usually PENDING. |
| results[].metaTemplateId | No | string | Meta's template id. Present on submitted. |
| results[].error | No | string | Meta's message. Present on failed. |
| note | Yes | string | A reminder that Meta's review is asynchronous. |
{
"success": true,
"phoneNumberId": "1030974986765331",
"results": [
{
"event": "order_confirmation",
"templateName": "order_confirmation",
"category": "UTILITY",
"state": "submitted",
"status": "PENDING",
"metaTemplateId": "1234567890123456"
},
{
"event": "order_shipped",
"templateName": "order_shipped",
"category": "UTILITY",
"state": "already_exists"
}
],
"submitted": 1,
"existing": 1,
"failed": 0,
"note": "Templates are reviewed by Meta, usually within minutes to a few hours. Poll GET /v1/notifications/events?refresh=true for approval status."
}Submissions run sequentially because Meta rate-limits template creation per WABA, so a full 18-template run takes several seconds. Hold the connection open.
Errors. 400 when an event key is not in the catalog:
{ "error": "Unknown events: order_packed, shipping_delayed" }400 when Meta refused, or the number has no access token:
{ "error": "Phone number has no access token configured" }404 when no number is configured, the same two bodies as readiness.
Sending one notification
Sends one transactional notification to one recipient.
Body parameters
| Field | Required | Type | Description |
|---|---|---|---|
| to | Yes | string | Recipient phone number, minimum 5 characters. Use E.164 with the country code. Non-digits are stripped and nothing else is validated here, so a national number is delivered to a different subscriber entirely. Example: +919876543210 |
| event | Yes | string | One of the 18 catalog event keys. Example: order_shipped |
| variables | No | object | { variableName: value }. Values are coerced to strings. Optional to the schema, required in practice, see the warning below.Default: {} |
| phoneNumberId | No | string | Which of your numbers to send from. Default: the account's oldest configured number |
| languageCode | No | string | Locale to look for. en_US also matches a template Meta approved as en.Default: the template's language (en_US) |
| allowTextFallback | No | boolean | When no approved template exists, send the rendered body as plain text instead, but only if the 24-hour window is open. Ignored when a template is approved. Default: false |
Newlines and tabs inside a value are collapsed to a single space and runs of four or more spaces are shortened, because Meta rejects such parameters with error 132000. Values are then trimmed.
curl -X POST https://whatsapp-api.growcord.in/api/v1/notifications/send \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+919876543210",
"event": "order_shipped",
"variables": {
"orderId": "#ORD-9001",
"awbCode": "AWB123456789",
"trackingLink": "https://yourstore.com/track/AWB123456789"
},
"phoneNumberId": "1030974986765331"
}'Response 200
| Field | Required | Type | Description |
|---|---|---|---|
| success | Yes | boolean | Always true. |
| messageId | Yes | string | Meta message id ( wamid.…). Pass it to /v1/notifications/status/:messageId. |
| channel | Yes | string | template normally; text when it fell back. |
| event | Yes | string | Echo of the event you sent. |
| template | Yes | string | null | The template used, or null on a text fallback. |
| missingVariables | No | string[] | Present only when non-empty. The variables that arrived blank and were replaced with -. |
{
"success": true,
"messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSN0Yx...",
"channel": "template",
"event": "order_shipped",
"template": "order_shipped",
"missingVariables": ["awbCode"]
}A missingVariables array means the message was delivered with gaps: the send succeeded, the credit was spent, and the customer saw a - where the tracking number should have been. Log it as an error even though the status is 200.
Errors
400, the body failed validation. error is an object:
{
"error": {
"formErrors": [],
"fieldErrors": { "to": ["String must contain at least 5 character(s)"] }
}
}400, the event key is not in the catalog:
{
"error": "Unknown event \"order_packed\".",
"code": "UNKNOWN_EVENT",
"supportedEvents": ["otp_verification", "password_reset", "…"]
}402, insufficient credits or wallet balance. Nothing was sent and nothing was spent; see Errors and retries for both body shapes.
404, no usable number on the account, or the phoneNumberId you named is not yours:
{ "error": "No configured WhatsApp number on this account", "code": "NO_PHONE" }409, no approved template for this event on this number. The most common failure on a new account:
{
"error": "No approved WhatsApp template named \"order_shipped\" for this number. Submit it for verification (POST /v1/notifications/templates/submit) and wait for Meta approval, business-initiated messages cannot be sent as plain text.",
"code": "TEMPLATE_NOT_APPROVED",
"details": { "event": "order_shipped", "templateName": "order_shipped", "language": "en_US" }
}429, the key rate limit, or the account's sending cap. They need opposite handling; tell them apart by the body.
502, Meta rejected the send. The credit is refunded automatically:
{
"error": "(#133010) Account is not registered",
"code": "SEND_FAILED",
"details": { "event": "order_shipped", "template": "order_shipped" }
}Sending to many recipients
One event, up to 500 recipients, in a single call, for back-in-stock alerts and similar fan-outs. Each recipient is billed and limit-checked individually, and the whole call costs one request against your rate limit.
Body parameters
| Field | Required | Type | Description |
|---|---|---|---|
| event | Yes | string | Catalog event key, applied to every recipient. Example: back_in_stock |
| recipients | Yes | array | 1 to 500 entries. Fewer or more is a 400. |
| recipients[].to | Yes | string | Recipient number, minimum 5 characters. |
| recipients[].variables | No | object | Per-recipient variables. The same “required in practice” rule as /send.Default: {} |
| phoneNumberId | No | string | Number to send from. Default: the account's oldest configured number |
| languageCode | No | string | Locale to look for. Default: the template's language |
allowTextFallback is not part of this schema and has no effect here.
{
"event": "back_in_stock",
"recipients": [
{
"to": "+919876543210",
"variables": {
"productName": "Arnica Montana 30CH",
"productLink": "https://yourstore.com/product/arnica-30ch"
}
},
{
"to": "+919812345678",
"variables": {
"productName": "Arnica Montana 30CH",
"productLink": "https://yourstore.com/product/arnica-30ch"
}
}
],
"phoneNumberId": "1030974986765331",
"languageCode": "en_US"
}Response 200
| Field | Required | Type | Description |
|---|---|---|---|
| success | Yes | boolean | Always true. It reports that the batch ran, not that anything was delivered. |
| sent | Yes | integer | Recipients with ok: true. |
| failed | Yes | integer | Recipients with ok: false. |
| results[] | Yes | array | One entry per recipient that was processed, which is not necessarily every recipient you sent. |
| results[].to | Yes | string | The recipient exactly as you supplied it. |
| results[].ok | Yes | boolean | Whether this one was delivered. |
| results[].messageId | No | string | Present when ok: true. |
| results[].channel | No | string | template. Present when ok: true. |
| results[].error | No | string | Present when ok: false. |
| results[].code | No | string | Present when ok: false. One of UNKNOWN_EVENT, NO_PHONE, TEMPLATE_NOT_APPROVED, SEND_FAILED, LIMIT_REACHED, INSUFFICIENT_CREDITS, INSUFFICIENT_BALANCE. |
{
"success": true,
"sent": 1,
"failed": 1,
"results": [
{ "to": "+919876543210", "ok": true, "messageId": "wamid.HBgM...", "channel": "template" },
{ "to": "+919812345678", "ok": false, "error": "Insufficient balance", "code": "INSUFFICIENT_CREDITS" }
]
}const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
const data = await res.json();
const processed = data.results.map((r) => r.to);
const untouched = payload.recipients.filter((r) => !processed.includes(r.to));
if (untouched.length) {
"/v1/notifications/catalog"
await requeue(untouched);
}Because billing happens per recipient inside the loop, this endpoint returns none of the X-Message-* headers. The per-recipient outcome is in results.
Errors
400, validation failure, with error as a Zod object. Common causes: recipients empty, more than 500 entries, or a to shorter than 5 characters.
413, the body exceeded the 1 MB request limit. 500 recipients carrying long URLs or product names can reach it. Split the batch.
There is no 402 or 409 at the top level. Per-recipient failures appear inside results.
Delivery status
Delivery status for a message this account sent.
Path parameters
| Field | Required | Type | Description |
|---|---|---|---|
| messageId | Yes | string | The wamid.… returned by /send. |
curl "https://whatsapp-api.growcord.in/api/v1/notifications/status/wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSN0Yx" \
-H "Authorization: Bearer sk_live_xxx"Response 200
| Field | Required | Type | Description |
|---|---|---|---|
| metaMessageId | Yes | string | The wamid you asked for. |
| status | No | string | null | sent → delivered → read, or failed. |
| statusTimestamp | No | string | null | ISO 8601 timestamp of the last status update. |
| messageType | Yes | string | template or text. |
| recipientWaId | Yes | string | Recipient, digits only, no +. |
| errorCode | No | string | null | Reserved, see the note below. |
| errorMessage | No | string | null | Reserved, see the note below. |
| createdAt | Yes | string | ISO 8601 timestamp of the send. |
| event | No | string | null | The event key this message was sent for. |
{
"metaMessageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSN0Yx...",
"status": "delivered",
"statusTimestamp": "2026-08-05T10:00:12.000Z",
"messageType": "template",
"recipientWaId": "919876543210",
"errorCode": null,
"errorMessage": null,
"createdAt": "2026-08-05T10:00:04.000Z",
"event": "order_shipped"
}Status updates arrive asynchronously from Meta, so a message read back immediately after sending usually still shows sent. Poll, or read the delivery state in bulk from GET /v1/messages.
Errors. 404 when no message with that id belongs to this account:
{ "error": "Message not found" }