WhatsApp API

Send Messages

Send WhatsApp template, text, image, video, audio, document and interactive messages with POST /api/v1/messages: fields, limits, responses and delivery status.

One endpoint sends every kind of WhatsApp message:

POST https://waba-api.startmessaging.com/api/v1/messages

It needs the messages:send scope. It sends one message to one number and returns once WhatsApp has accepted it.

The 24-hour rule

WhatsApp decides what you may send based on the customer service window: the 24 hours after the customer’s last message to you.

TypeOutside the windowInside the window
template (approved)YesYes
textNoYes
image, video, audio, documentNoYes
interactiveNoYes

A free-form message outside the window is refused with 403 MESSAGE_WINDOW_CLOSED. Send an approved template instead. GET /api/v1/conversations reports windowOpen for each conversation (see Conversations).

Request body

FieldTypeRequiredDescription
tostringYesRecipient phone number. E.164 (+919876543210) is recommended; a national number (9876543210) is read in your workspace’s country. Up to 32 characters. Only phone numbers are accepted.
typestringYesOne of template, text, image, video, audio, document, interactive
templateobjectWhen type is templateSee Template messages
textobjectWhen type is textSee Text messages
mediaobjectWhen type is image, video, audio or documentSee Media messages
interactiveobjectWhen type is interactiveSee Interactive messages
clientReferencestringNoYour own reference for this message (see clientReference)

Send exactly the object that matches type and no other. A body with type: "text" plus a template object is a 400 VALIDATION_ERROR, not a guess.

A number that is not in your contacts yet is added automatically (source api), and the message appears in its conversation in the shared team inbox.

Some customers use a WhatsApp username and haven’t shared their number with you. Their contact has phone: null and a waUserId instead (see Contacts). The API can’t send to a waUserId, so reply to these customers from the team inbox until they share their number.

Headers

HeaderRequiredDescription
AuthorizationYesBearer sm_live_your_api_key
Content-TypeYesapplication/json (UTF-8, at most 100 KB)
Idempotency-KeyRecommendedA unique key per message so a retry never sends twice. See Pagination & Idempotency.

Template messages

Approved templates are the only messages that can start a conversation or reach a customer outside the 24-hour window. Templates are created in the dashboard and reviewed by Meta; see Message Templates.

FieldTypeRequiredDescription
template.namestringYesThe template’s name exactly as Meta approved it (for example order_confirmation), not the display name your team sees in the dashboard
template.languagestringYesLanguage code, for example en_US, en_GB or hi. The same name in another language is a different template.
template.parametersarrayWhen the body has variablesBody variables in order. Each value is a string of up to 1,024 characters.
template.headerMediaUrlstringWhen the header is an image, video or documentA public https URL of the file. WhatsApp downloads it.
template.buttonParametersarrayWhen a button needs a valueSend-time values for dynamic buttons (see below)
template.carouselCardsarrayFor a carousel templateOne entry per card, in the template’s card order (see Carousel templates)
template.offerExpiresAtstringFor a limited-time offer that shows its end timeWhen the offer ends: an ISO 8601 date-time in the future (see Limited-time offers)

Body variables: positional or named

For a template with positional variables ({{1}}, {{2}}), send the values in order:

"parameters": [{ "text": "Aarti" }, { "text": "#1042" }]

For a template with named variables ({{first_name}}, {{order_id}}), send each value with its name:

"parameters": [
  { "parameter_name": "first_name", "text": "Aarti" },
  { "parameter_name": "order_id", "text": "#1042" }
]

The template’s Send via API panel in the dashboard shows exactly which values a template needs and a request you can copy. Carousel templates are the exception: the panel’s request has no cards, and the panel still says carousels can’t be sent through the API. The API does send them; see Carousel templates.

Meta keeps the variable style (named or numbered) a template was created with. A template that Meta registered without its variables can’t be filled in, so a send answers 422 TEMPLATE_VARIABLES_NOT_REGISTERED. Duplicate the template in the dashboard and send the copy once Meta approves it.

Button parameters

Buttons that are fully static need nothing at send time. Buttons with a send-time value need an entry in buttonParameters:

FieldDescription
indexPosition of the button in the template, counting from 0 (0–9)
subTypeurl for a link button with a variable (and for authentication codes), copy_code for a coupon button, flow for a WhatsApp Form button
textThe value for the URL variable, the one-time code, or the flow token for a flow button
couponCodeThe coupon code a copy_code button copies, up to 15 characters (WhatsApp’s limit)
"buttonParameters": [
  { "index": 0, "subType": "url", "text": "ORD-1042" },
  { "index": 1, "subType": "copy_code", "couponCode": "DIWALI25" }
]

If a button needs a value and you leave it out, the API answers 422 TEMPLATE_BUTTON_PARAM_REQUIRED and details lists each missing button’s index and buttonType. A template with a media header and no headerMediaUrl answers 422 TEMPLATE_HEADER_MEDIA_REQUIRED.

For one-time passwords, see the WhatsApp OTP recipe: the code goes in both parameters and buttonParameters.

Example

curl -X POST https://waba-api.startmessaging.com/api/v1/messages \
  -H "Authorization: Bearer sm_live_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirmation" \
  -d '{
    "to": "+919876543210",
    "type": "template",
    "template": {
      "name": "order_confirmation",
      "language": "en_US",
      "parameters": [{ "text": "Aarti" }, { "text": "#1042" }],
      "headerMediaUrl": "https://cdn.example.com/orders/1042.jpg"
    },
    "clientReference": "order-1042"
  }'
const res = await fetch('https://waba-api.startmessaging.com/api/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SM_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1042-confirmation',
  },
  body: JSON.stringify({
    to: '+919876543210',
    type: 'template',
    template: {
      name: 'order_confirmation',
      language: 'en_US',
      parameters: [{ text: 'Aarti' }, { text: '#1042' }],
    },
    clientReference: 'order-1042',
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
console.log(body.data.id);
import os, requests

res = requests.post(
    "https://waba-api.startmessaging.com/api/v1/messages",
    headers={
        "Authorization": f"Bearer {os.environ['SM_API_KEY']}",
        "Idempotency-Key": "order-1042-confirmation",
    },
    json={
        "to": "+919876543210",
        "type": "template",
        "template": {
            "name": "order_confirmation",
            "language": "en_US",
            "parameters": [{"text": "Aarti"}, {"text": "#1042"}],
        },
        "clientReference": "order-1042",
    },
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["data"]["id"])
<?php
$payload = [
    'to' => '+919876543210',
    'type' => 'template',
    'template' => [
        'name' => 'order_confirmation',
        'language' => 'en_US',
        'parameters' => [['text' => 'Aarti'], ['text' => '#1042']],
    ],
    'clientReference' => 'order-1042',
];

$ch = curl_init('https://waba-api.startmessaging.com/api/v1/messages');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('SM_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: order-1042-confirmation',
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = json_decode(curl_exec($ch), true);
echo $response['data']['id'];

A carousel template sends 2 to 10 cards that scroll sideways. A carousel always sends every card it was created with, so template.carouselCards needs one entry per card, in the same order as the template:

FieldRequiredDescription
headerMediaUrlYesThe card’s image or video (whichever the template’s cards were created with), as a public https URL without credentials, up to 2,048 characters. Required on every send: WhatsApp has no default.
parametersWhen the card text has variablesThe card text’s variables, in order, exactly like template.parameters for the message body
buttonParametersWhen a card button is a link with a variableUp to 2 entries, each { "index", "subType": "url", "text" }. index counts the card’s own buttons from 0. Quick-reply and phone buttons need nothing.

template.parameters still fills the message body above the cards, if it has variables.

If a card is missing something, or you send a different number of cards, the API answers 422 TEMPLATE_CAROUSEL_CARDS_INCOMPLETE before anything is sent. details.expected is the template’s card count, details.received is how many you sent, and details.problems lists each gap as { "card", "missing" }, where card counts from 1 and missing is media, variables or button (with buttonIndex).

curl -X POST https://waba-api.startmessaging.com/api/v1/messages \
  -H "Authorization: Bearer sm_live_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: diwali-carousel-919876543210" \
  -d '{
    "to": "+919876543210",
    "type": "template",
    "template": {
      "name": "diwali_collection",
      "language": "en_US",
      "parameters": [{ "text": "Aarti" }],
      "carouselCards": [
        {
          "headerMediaUrl": "https://cdn.example.com/products/kurta-blue.jpg",
          "parameters": [{ "text": "₹1,299" }],
          "buttonParameters": [{ "index": 0, "subType": "url", "text": "kurta-blue" }]
        },
        {
          "headerMediaUrl": "https://cdn.example.com/products/saree-red.jpg",
          "parameters": [{ "text": "₹2,499" }],
          "buttonParameters": [{ "index": 0, "subType": "url", "text": "saree-red" }]
        }
      ]
    }
  }'
const res = await fetch('https://waba-api.startmessaging.com/api/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SM_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'diwali-carousel-919876543210',
  },
  body: JSON.stringify({
    to: '+919876543210',
    type: 'template',
    template: {
      name: 'diwali_collection',
      language: 'en_US',
      parameters: [{ text: 'Aarti' }],
      carouselCards: products.map((p) => ({
        headerMediaUrl: p.imageUrl,
        parameters: [{ text: p.price }],
        buttonParameters: [{ index: 0, subType: 'url', text: p.slug }],
      })),
    },
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
import os, requests

res = requests.post(
    "https://waba-api.startmessaging.com/api/v1/messages",
    headers={
        "Authorization": f"Bearer {os.environ['SM_API_KEY']}",
        "Idempotency-Key": "diwali-carousel-919876543210",
    },
    json={
        "to": "+919876543210",
        "type": "template",
        "template": {
            "name": "diwali_collection",
            "language": "en_US",
            "parameters": [{"text": "Aarti"}],
            "carouselCards": [
                {
                    "headerMediaUrl": p["image_url"],
                    "parameters": [{"text": p["price"]}],
                    "buttonParameters": [{"index": 0, "subType": "url", "text": p["slug"]}],
                }
                for p in products  # one per card, in the template's order
            ],
        },
    },
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")

Limited-time offers

A limited-time offer template can show a countdown to the end of the offer. If it does, every send needs template.offerExpiresAt: when the offer ends, as an ISO 8601 date-time (for example 2026-10-20T18:30:00Z) that is still in the future. WhatsApp counts the offer down to it.

  • No offerExpiresAt on a template that shows the end time: 422 TEMPLATE_OFFER_EXPIRY_REQUIRED.
  • An end time that has already passed: 422 TEMPLATE_OFFER_EXPIRY_INVALID.

Offer templates usually also have a coupon button (subType: "copy_code", couponCode up to 15 characters) and a link button.

curl -X POST https://waba-api.startmessaging.com/api/v1/messages \
  -H "Authorization: Bearer sm_live_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: diwali-offer-919876543210" \
  -d '{
    "to": "+919876543210",
    "type": "template",
    "template": {
      "name": "diwali_flash_sale",
      "language": "en_US",
      "parameters": [{ "text": "Aarti" }],
      "headerMediaUrl": "https://cdn.example.com/diwali-sale.jpg",
      "offerExpiresAt": "2026-10-20T18:30:00Z",
      "buttonParameters": [
        { "index": 0, "subType": "copy_code", "couponCode": "DIWALI25" },
        { "index": 1, "subType": "url", "text": "diwali" }
      ]
    }
  }'
const endsAt = new Date(Date.now() + 48 * 60 * 60 * 1000); // 48 hours from now

const res = await fetch('https://waba-api.startmessaging.com/api/v1/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SM_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'diwali-offer-919876543210',
  },
  body: JSON.stringify({
    to: '+919876543210',
    type: 'template',
    template: {
      name: 'diwali_flash_sale',
      language: 'en_US',
      parameters: [{ text: 'Aarti' }],
      headerMediaUrl: 'https://cdn.example.com/diwali-sale.jpg',
      offerExpiresAt: endsAt.toISOString(),
      buttonParameters: [
        { index: 0, subType: 'copy_code', couponCode: 'DIWALI25' },
        { index: 1, subType: 'url', text: 'diwali' },
      ],
    },
  }),
});
import os, requests
from datetime import datetime, timedelta, timezone

ends_at = datetime.now(timezone.utc) + timedelta(hours=48)

res = requests.post(
    "https://waba-api.startmessaging.com/api/v1/messages",
    headers={
        "Authorization": f"Bearer {os.environ['SM_API_KEY']}",
        "Idempotency-Key": "diwali-offer-919876543210",
    },
    json={
        "to": "+919876543210",
        "type": "template",
        "template": {
            "name": "diwali_flash_sale",
            "language": "en_US",
            "parameters": [{"text": "Aarti"}],
            "headerMediaUrl": "https://cdn.example.com/diwali-sale.jpg",
            "offerExpiresAt": ends_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
            "buttonParameters": [
                {"index": 0, "subType": "copy_code", "couponCode": "DIWALI25"},
                {"index": 1, "subType": "url", "text": "diwali"},
            ],
        },
    },
)

Text messages

FieldTypeRequiredDescription
text.bodystringYesThe message text, 1 to 4,096 characters

Text messages need an open 24-hour window.

curl -X POST https://waba-api.startmessaging.com/api/v1/messages \
  -H "Authorization: Bearer sm_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "type": "text",
    "text": { "body": "Hi Aarti, your order #1042 has shipped and arrives on Friday." }
  }'

Media messages

Set type to image, video, audio or document and describe the file in media. Media messages need an open 24-hour window.

FieldTypeRequiredDescription
media.urlstringYesA public https URL, without credentials, up to 2,048 characters
media.captionstringNoText shown under an image, video or document, up to 1,024 characters. Not allowed on audio.
media.filenamestringNoDocuments only: the file name the recipient sees, up to 240 characters

Media is sent by URL only. There is no upload endpoint: WhatsApp downloads the file from your link when it delivers the message, and StartMessaging never fetches it. Make sure the URL is publicly reachable over HTTPS and serves the right Content-Type. If WhatsApp can’t fetch the file, the send is refused (422 MESSAGE_REJECTED) or arrives later as a message.failed webhook.

WhatsApp’s own limits apply to the file:

TypeCommon formatsMax size
imageJPEG, PNG5 MB
videoMP4 (H.264 video, AAC audio), 3GPP16 MB
audioAAC, MP3, OGG (Opus), AMR, M4A16 MB
documentPDF, DOC(X), XLS(X), PPT(X), TXT and other files100 MB

Image

{
  "to": "+919876543210",
  "type": "image",
  "media": {
    "url": "https://cdn.example.com/products/kurta-blue.jpg",
    "caption": "Back in stock in your size"
  }
}

Document

curl -X POST https://waba-api.startmessaging.com/api/v1/messages \
  -H "Authorization: Bearer sm_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "type": "document",
    "media": {
      "url": "https://cdn.example.com/invoices/1042.pdf",
      "filename": "invoice-1042.pdf",
      "caption": "Your invoice for order #1042"
    }
  }'

Audio

{
  "to": "+919876543210",
  "type": "audio",
  "media": { "url": "https://cdn.example.com/voice/welcome.mp3" }
}

Sending media.caption with type: "audio" is a 400 VALIDATION_ERROR, because WhatsApp audio messages have no caption.

Interactive messages

Interactive messages show reply buttons or a list menu. They need an open 24-hour window and the Interactive Messages plan feature, which Team, Business, Enterprise and the free trial include and no Developer plan does (Developer Free included); a plan without it answers 403 PLAN_FEATURE_REQUIRED.

FieldTypeRequiredDescription
interactive.interactiveTypestringYesbutton (up to 3 reply buttons) or list (a menu of rows)
interactive.bodystringYesThe message text, up to 1,024 characters
interactive.headerobjectNotype: text, image, video or document. text (up to 60 characters) for a text header, mediaUrl (public https) for a media header.
interactive.footerstringNoSmall print under the body, up to 60 characters
interactive.buttonsarrayFor button1 to 3 buttons, each { id, title }. id up to 256 characters, title up to 20.
interactive.buttonLabelstringFor listLabel of the button that opens the list, up to 20 characters
interactive.sectionsarrayFor listSections, each with an optional title (up to 24 characters; required by WhatsApp when there is more than one section) and rows

Each list row is { id, title, description }: id up to 200 characters, title up to 24, optional description up to 72. WhatsApp allows 10 rows in total across all sections.

When the customer taps a button or row, you receive a message.received webhook with type: "interactive" and reply: { kind, id, title }. Branch on id (the id you set), not on title.

Reply buttons

curl -X POST https://waba-api.startmessaging.com/api/v1/messages \
  -H "Authorization: Bearer sm_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919876543210",
    "type": "interactive",
    "interactive": {
      "interactiveType": "button",
      "header": { "type": "text", "text": "Order #1042" },
      "body": "Would you like to reorder your last purchase?",
      "footer": "Reply STOP to opt out",
      "buttons": [
        { "id": "reorder", "title": "Reorder" },
        { "id": "later", "title": "Not now" }
      ]
    }
  }'

List

{
  "to": "+919876543210",
  "type": "interactive",
  "interactive": {
    "interactiveType": "list",
    "body": "Which size would you like?",
    "buttonLabel": "Choose a size",
    "sections": [
      {
        "title": "Sizes",
        "rows": [
          { "id": "size-s", "title": "Small", "description": "Chest 36–38 in" },
          { "id": "size-m", "title": "Medium", "description": "Chest 38–40 in" },
          { "id": "size-l", "title": "Large", "description": "Chest 40–42 in" }
        ]
      }
    ]
  }
}

Location and contact-card messages can’t be sent through the API.

Your own reference: clientReference

Send clientReference with a message to tie it to your own record: an order number, a login attempt id, a ticket. It is never shown to the customer. It comes back on:

  • the send response,
  • GET /api/v1/messages/{id},
  • the conversation’s message list,
  • every message.sent, message.delivered, message.read and message.failed webhook for that message,
  • message.received webhooks when the customer replies to that message (context.clientReference).

So you can match a receipt or a reply to your record without storing our message id. It is a string of 1 to 256 printable ASCII characters; spaces are allowed, but not only spaces.

clientReference is not an idempotency key: two sends with the same reference are two messages. Use the Idempotency-Key header to make retries safe.

Response

201 Created means WhatsApp accepted the message.

{
  "data": {
    "id": "3f6c2a1e-8b1d-4c53-9e7a-2b5d8f0c4a11",
    "status": "sent",
    "conversationId": "8a1b9c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
    "contactId": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    "to": "+919876543210",
    "type": "template",
    "clientReference": "order-1042",
    "createdAt": "2026-10-02T10:00:00.000Z"
  },
  "meta": {
    "requestId": "b7c1e0d2-5a4f-4c3b-9e8d-1f2a3b4c5d6e",
    "timestamp": "2026-10-02T10:00:00.000Z"
  }
}
FieldDescription
idMessage id. Look it up with GET /api/v1/messages/{id}.
statussent once WhatsApp accepted it
conversationIdThe inbox conversation the message is in
contactIdThe recipient contact
toThe recipient, normalised to E.164
typeWhat was sent
clientReferenceThe reference you sent, or null
createdAtWhen WhatsApp accepted it

Common send errors

HTTPCodeWhat to do
400VALIDATION_ERRORFix the body; error.message names the field
400MOBILE_INVALIDto is not a valid mobile number
400WABA_NOT_CONNECTEDReconnect WhatsApp on the Connect page
402PLAN_MESSAGE_LIMIT_REACHEDThis month’s API message allowance is used up (see below)
402SUBSCRIPTION_REQUIREDThe workspace is read-only because Razorpay gave up on a subscription renewal; fix billing in the dashboard. It can also appear briefly when a trial or cancelled subscription has just ended, until the workspace moves to Developer Free.
402META_PAYMENT_REQUIREDAdd or fix the payment method in WhatsApp Manager
403MESSAGE_WINDOW_CLOSEDSend an approved template instead
404TEMPLATE_NOT_FOUNDCheck template.name and template.language
422TEMPLATE_NOT_APPROVEDWait for Meta’s approval; details.status shows the current status
422TEMPLATE_CAROUSEL_CARDS_INCOMPLETESend every card with its media and values; details.problems names each gap
422TEMPLATE_OFFER_EXPIRY_REQUIRED, TEMPLATE_OFFER_EXPIRY_INVALIDSend template.offerExpiresAt, in the future
422TEMPLATE_VARIABLES_NOT_REGISTEREDDuplicate the template in the dashboard and send the copy
422CONTACT_OPTED_OUTThe customer replied STOP; they must opt back in first
422MESSAGE_REJECTEDWhatsApp refused the request itself; details.metaCode is Meta’s error number
429RATE_LIMIT_EXCEEDEDWait for Retry-After seconds
502WABA_CONNECT_FAILEDTransient WhatsApp failure; retry later with the same Idempotency-Key

The full list is on Errors & Limits.

Monthly API message allowance

Each plan includes a number of messages a month sent through the API (POST /api/v1/messages and POST /api/v1/trigger/send). Only sends that WhatsApp accepted count: a send that is refused, by us or by WhatsApp, does not use up the allowance. The month follows India time and starts again on the 1st.

PlanMessages a monthWhere sends stop
Free trial1,000At 1,000, authentication templates included
Developer Free100At 100, authentication templates included
Developer 1K, 10K, 100K1,000, 10,000, 100,000At 3× the allowance. Authentication templates are never stopped.
Team, Business, EnterpriseUnlimited (fair use)Never

When sends stop, the API answers 402 PLAN_MESSAGE_LIMIT_REACHED:

{
  "statusCode": 402,
  "error": {
    "code": "PLAN_MESSAGE_LIMIT_REACHED",
    "message": "This workspace has sent the 100 messages its plan includes through the API this month. …",
    "details": {
      "limit": 100,
      "stopAt": 100,
      "sent": 100,
      "period": "2026-10",
      "resetsAt": "2026-10-31T18:30:00.000Z"
    }
  },
  "meta": { "requestId": "…", "timestamp": "2026-10-20T10:00:00.000Z", "path": "/api/v1/messages" }
}

details.sent is this month’s count, details.stopAt is where sends stop, and details.resetsAt is when the count starts again (midnight India time on the 1st). Nothing upgrades or charges by itself: choose a bigger Developer size or another plan to send more. To warn your own users before this happens, watch messages in GET /api/v1/me.

Get a message

GET /api/v1/messages/{id} looks up a message’s delivery status. Needs the messages:read scope. For real-time updates, subscribe to the message.* webhooks instead of polling.

curl https://waba-api.startmessaging.com/api/v1/messages/3f6c2a1e-8b1d-4c53-9e7a-2b5d8f0c4a11 \
  -H "Authorization: Bearer sm_live_your_api_key"
{
  "data": {
    "id": "3f6c2a1e-8b1d-4c53-9e7a-2b5d8f0c4a11",
    "conversationId": "8a1b9c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
    "direction": "outbound",
    "type": "template",
    "status": "delivered",
    "to": "+919876543210",
    "from": null,
    "text": "Hi Aarti, your order #1042 is confirmed.",
    "templateName": "order_confirmation",
    "failureCode": null,
    "failureReason": null,
    "pricing": { "category": "utility", "type": "regular" },
    "apiKeyId": "0e9d8c7b-6a5f-4e3d-2c1b-0a9f8e7d6c5b",
    "clientReference": "order-1042",
    "form": null,
    "timestamp": "2026-10-02T10:00:00.000Z"
  },
  "meta": { "requestId": "…", "timestamp": "2026-10-02T10:00:05.000Z" }
}
FieldDescription
directionoutbound (from you) or inbound (from the customer)
typetext, template, image, video, audio, document, sticker or interactive. Kinds the API does not model (a location, a contact card, a reaction) read as text.
statusDelivery status (see below)
to / fromThe customer’s number: to on outbound messages, from on inbound
textThe words: the text, a template body with its variables filled in, a caption, or the label of a tapped option. null when there are none.
templateNameThe template sent, for a template message
failureCode / failureReasonWhatsApp’s error code (for example 131049) and reason when the message failed
pricingHow Meta classified the message for billing: category and type (see below). null for inbound messages and until WhatsApp’s first receipt arrives.
apiKeyIdThe API key that sent it; null for messages sent another way
clientReferenceThe reference sent with it through the API; null otherwise
formThe answers when the customer submitted a WhatsApp Form; null otherwise

A message of another workspace answers 404 MESSAGE_NOT_FOUND, exactly like a missing one.

Pricing

pricing records what WhatsApp’s delivery receipts say about how Meta billed an outbound message. Meta bills your WhatsApp Business Account directly; it is never a StartMessaging charge.

FieldValues
pricing.categorymarketing, utility, authentication, authentication-international, service, or referral_conversion (inside a Click-to-WhatsApp ad’s free window)
pricing.typeregular (Meta charged the message), free_customer_service (a service reply inside the monthly free allowance) or free_entry_point (inside a Click-to-WhatsApp ad’s free window)

Meta may add new values; treat an unknown one as information, not an error.

Message statuses

StatusMeaning
queuedRecorded but not yet accepted by WhatsApp. It is part of the status list, but a message normally starts at sent.
sentWhatsApp accepted the message
deliveredIt reached the customer’s phone
readThe customer opened it (only if they have read receipts on)
failedWhatsApp could not deliver it; see failureCode and failureReason

Outbound messages move sent → delivered → read, or end in failed. Inbound messages read delivered. Each change also fires the matching message.* webhook.

List your WhatsApp numbers

GET /api/v1/phone-numbers returns the sender numbers of the connected WhatsApp Business Account with WhatsApp’s quality rating and messaging limit. Needs no scope. All numbers come back in one page (nextCursor is always null).

curl https://waba-api.startmessaging.com/api/v1/phone-numbers \
  -H "Authorization: Bearer sm_live_your_api_key"
{
  "data": {
    "items": [
      {
        "id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
        "displayPhoneNumber": "+918010012345",
        "verifiedName": "Meera Boutique",
        "qualityRating": "green",
        "messagingLimitPerDay": 1000,
        "status": "active"
      }
    ],
    "nextCursor": null
  },
  "meta": { "requestId": "…", "timestamp": "2026-10-02T10:00:00.000Z" }
}
FieldDescription
qualityRatinggreen, yellow, red, or unknown (not rated yet, which is not the same as healthy)
messagingLimitPerDayHow many customers you may start conversations with per rolling 24 hours. Meta sets it per business portfolio, so every number in the portfolio shares it. -1 is unlimited; null is unknown.
statuspending, active, flagged or banned. Only an active number can send.

FAQ

Can I upload a file and reuse a media id? No. The API sends media by URL only. Host the file on a public HTTPS URL; WhatsApp downloads it each time you send.

Why did my text message fail with MESSAGE_WINDOW_CLOSED? The customer has not messaged you in the last 24 hours. Outside that window WhatsApp delivers only approved templates. Send a template, and switch to free-form messages once the customer replies.

Do I need to create the contact before sending? No. A number that is not in your contacts is added automatically with source api. On plans with a contact cap, a send to a new number over the cap answers 403 PLAN_LIMIT_REACHED; numbers you already have can still be messaged.

Does StartMessaging charge per message? No. Meta bills message charges directly to your WhatsApp Business Account. StartMessaging charges only the subscription. Each plan includes a monthly number of API messages (see Monthly API message allowance), and each message’s pricing shows how Meta classified it.

Can I send carousel or limited-time offer templates? Yes. Send each carousel card’s media and values in template.carouselCards, and an offer’s end time in template.offerExpiresAt. Campaigns can’t send these templates, so the API (or the inbox) is the way to send them. For a carousel, build the request from Carousel templates rather than the dashboard’s Send via API panel, which has no cards and still says carousels can’t be sent through the API.

Can I send location or contact-card messages? Not through the API. Supported types are template, text, image, video, audio, document and interactive.