Send WhatsApp Business messages from your own software: approved templates to start a conversation, and text, images and documents to reply.
POST one message per call to https://api.everymessage.com/v1/whatsapp/messages. To contact someone first, use an approved template. Once they message you, you can send free-form text and media for 24 hours.
Before you start: WhatsApp must be switched on for your account and your business number connected. Templates are created in the portal and approved by WhatsApp. Read about WhatsApp or contact us to set it up.
Method
https://api.everymessage.com/v1/whatsapp/messagesSend Content-Type: application/json and authenticate with HTTP Basic (details).
The WhatsApp API uses HTTP status codes. Unlike /v1/submit, it does not use the response envelope: 202 means accepted, and errors return a 4xx status with {"error": "…"}.
Request
Template
{
"from": "447700900000",
"to": "447700900123",
"type": "template",
"template": {
"name": "order_update",
"language": "en_GB",
"body": [
"Sam",
"A1042"
]
},
"reference": "order-A1042"
}Template with image and button
{
"from": "447700900000",
"to": "447700900123",
"type": "template",
"template": {
"name": "appointment_card",
"language": "en_GB",
"header": {
"type": "image",
"link": "https://example.com/img/clinic.png"
},
"body": [
"Sam",
"Tuesday 14 October",
"10:30"
],
"buttons": [
{
"index": 0,
"type": "url",
"value": "A1042"
}
]
}
}Text reply
{
"from": "447700900000",
"to": "447700900123",
"type": "text",
"text": {
"body": "Thanks Sam, we've moved your appointment to 11:15."
}
}Image
{
"from": "447700900000",
"to": "447700900123",
"type": "image",
"image": {
"link": "https://example.com/img/map.png",
"caption": "How to find us"
}
}Document
{
"from": "447700900000",
"to": "447700900123",
"type": "document",
"document": {
"link": "https://example.com/files/invoice-A1042.pdf",
"filename": "invoice-A1042.pdf",
"caption": "Your invoice"
}
}Request body
| Field | Type | Description |
|---|---|---|
from required | string | Your WhatsApp business number, for example 447700900000. |
to required | string | The recipient’s WhatsApp number in international format, for example 447700900123. One recipient per call. |
type required | string | template, text, image, document, video or audio. Then supply the matching object below. |
template | object | For type: template. See Template. |
text | object | For type: text: {"body": "…"}. Up to 4,096 characters. |
image, document, video, audio | object | For media types. See Media. |
reference | string | Your own reference. It comes back on the delivery receipt. |
replyUrl | string | Where to send delivery receipts by webhook. See receipts and replies. |
Template
| Field | Type | Description |
|---|---|---|
name required | string | The template name, exactly as approved. |
language required | string | The template language, for example en_GB. |
body | array | Values for the template’s placeholders, in order: the first fills {{1}}, the second {{2}}, and so on. Must match the number in the template. |
header | object | Only if the template header has a variable. Text: {"type": "text", "text": "…"}. Media: {"type": "image", "link": "https://…"} (or document / video). |
buttons | array | Only for buttons that take a value: {"index": 0, "value": "…"}, where index is the button’s position starting at 0. For a website button, the value is added to the end of its URL; for a copy-code button it is the code. |
We check the values against the approved template before sending, so a wrong number of values is rejected with 400 instead of failing later.
Media
| Field | Type | Description |
|---|---|---|
link | string | A public HTTPS link to the file. Either link or id is required. |
id | string | A WhatsApp media id, if you have already uploaded the file. |
caption | string | Text shown with an image, video or document. |
filename | string | The file name shown for a document. |
24-hour window
WhatsApp only allows free-form messages (text and media) within 24 hours of the customer’s last message to you. Outside that window, start the conversation with an approved template. If you send free-form outside the window, the API returns 409 and nothing is sent or charged. A common pattern is to try a text reply and, on 409, send a template instead.
Each message is charged by its WhatsApp category: marketing, utility or authentication for templates, and service for free-form replies. The category in the response shows which. See WhatsApp message types and pricing for current costs.
Response
HTTP 202 Accepted:
{
"status": "accepted",
"channel": "WHATSAPP",
"to": "447700900123",
"type": "template",
"category": "UTILITY",
"messageId": 48240,
"conversationId": "K7Q2M4XW9PLD3T6B",
"charged": 0.03,
"transactionId": 4830
}
| Field | Type | Description |
|---|---|---|
status | string | accepted: queued for delivery. |
to | string | The recipient. |
type | string | The message type sent. |
category | string | The template’s category (MARKETING, UTILITY or AUTHENTICATION). Left out for free-form messages, which are charged as service messages. |
messageId | integer | Our id for the message. Delivery receipts refer to it. |
conversationId | string | Identifies the conversation with this person. |
charged | number | Cost of the message. |
transactionId | integer | Our id for the request. |
Errors
| HTTP status | Meaning |
|---|---|
400 | Missing or invalid fields, or the template values don’t match the approved template. |
401 | Wrong api_key or secret_key. |
402 | Not enough credit on a prepay account. |
403 | WhatsApp is not enabled for your account, or from is not one of your WhatsApp numbers. |
404 | No template with that name and language. |
409 | The template is not approved yet, or a free-form message was sent outside the 24-hour window. |
429 | Your daily sending limit has been reached. |
500 | Something went wrong on our side. Safe to retry after a short wait. |
List templates
https://api.everymessage.com/v1/whatsapp/templatesLists your templates with their approval status and how many values each one needs. Filter with ?status=APPROVED or ?category=UTILITY.
{
"templates": [
{
"id": 12,
"companyId": 2,
"wabaId": "104512345678901",
"name": "order_update",
"language": "en_GB",
"category": "UTILITY",
"status": "APPROVED",
"metaTemplateId": "998877665544332",
"bodyParamCount": 2,
"headerFormat": null,
"headerParamCount": 0,
"buttonCount": 0,
"rejectedReason": null
}
]
}
bodyParamCount is how many values to send in template.body; headerFormat and buttonCount tell you whether a header or button values are needed. Templates can also be created by API with POST /v1/whatsapp/templates, though most customers use the portal’s template designer.
Receipts and replies
Delivery receipts arrive in the same format as SMS receipts: by webhook to replyUrl (or your account’s default receipt URL), or by collecting them. A message that is delivered or read is reported as DELIVERED; one WhatsApp could not deliver is FAILED. See delivery receipts.
Messages from your customers are forwarded to the webhook URL (or workflow) set up for your WhatsApp number. Each reply also opens a new 24-hour window for free-form messages.
Code examples
Send a template message
Both examples read the keys from EM_API_KEY and EM_SECRET_KEY. C# needs .NET 8 or later; Python needs 3.10 or later and pip install httpx.
C#
// Send a WhatsApp template message with the everymessage v1 API (.NET 8 or later)
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
var apiKey = Environment.GetEnvironmentVariable("EM_API_KEY")!;
var secretKey = Environment.GetEnvironmentVariable("EM_SECRET_KEY")!;
using var http = new HttpClient { BaseAddress = new Uri("https://api.everymessage.com/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:{secretKey}")));
var json = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
// One recipient per call. "from" is your WhatsApp business number.
var request = new WhatsAppMessage(
From: "447700900000",
To: "447700900123",
Type: "template",
Template: new Template(
Name: "order_update",
Language: "en_GB",
Body: ["Sam", "A1042"]), // fills {{1}} and {{2}} in the approved template
Reference: "order-A1042");
using var response = await http.PostAsJsonAsync("v1/whatsapp/messages", request, json);
// The WhatsApp API uses HTTP status codes: 202 means accepted.
// 409 means the template isn't approved, or a free-form message is outside the 24-hour window.
if (!response.IsSuccessStatusCode)
throw new InvalidOperationException(
$"WhatsApp send failed ({(int)response.StatusCode}): {await response.Content.ReadAsStringAsync()}");
var result = await response.Content.ReadFromJsonAsync<SendResult>(json)
?? throw new InvalidOperationException("Empty response from everymessage");
Console.WriteLine($"{result.To}: {result.Status} (message {result.MessageId}, {result.Category}, charged {result.Charged})");
record WhatsAppMessage(string From, string To, string Type, Template? Template = null,
TextBody? Text = null, string? Reference = null);
record Template(string Name, string Language, IReadOnlyList<string>? Body = null);
record TextBody(string Body);
record SendResult(string Status, string To, string Type, string? Category, long? MessageId, decimal Charged);
Python
"""Send a WhatsApp template message with the everymessage v1 API (Python 3.10+, pip install httpx)."""
import os
import httpx
# One recipient per call. "from" is your WhatsApp business number.
payload = {
"from": "447700900000",
"to": "447700900123",
"type": "template",
"template": {
"name": "order_update",
"language": "en_GB",
"body": ["Sam", "A1042"], # fills {{1}} and {{2}} in the approved template
},
"reference": "order-A1042",
}
with httpx.Client(
base_url="https://api.everymessage.com",
auth=(os.environ["EM_API_KEY"], os.environ["EM_SECRET_KEY"]),
timeout=30,
) as client:
response = client.post("/v1/whatsapp/messages", json=payload)
# The WhatsApp API uses HTTP status codes: 202 means accepted.
# 409 means the template isn't approved, or a free-form message is outside the 24-hour window.
if response.is_error:
raise RuntimeError(f"WhatsApp send failed ({response.status_code}): {response.text}")
r = response.json()
print(f"{r['to']}: {r['status']} (message {r.get('messageId')}, {r.get('category')}, charged {r['charged']})")
List approved templates
C#
// List your approved WhatsApp templates with the everymessage v1 API (.NET 8 or later)
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
var apiKey = Environment.GetEnvironmentVariable("EM_API_KEY")!;
var secretKey = Environment.GetEnvironmentVariable("EM_SECRET_KEY")!;
using var http = new HttpClient { BaseAddress = new Uri("https://api.everymessage.com/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:{secretKey}")));
using var response = await http.GetAsync("v1/whatsapp/templates?status=APPROVED");
if (!response.IsSuccessStatusCode)
throw new InvalidOperationException(
$"List failed ({(int)response.StatusCode}): {await response.Content.ReadAsStringAsync()}");
var result = await response.Content.ReadFromJsonAsync<TemplateList>()
?? throw new InvalidOperationException("Empty response from everymessage");
foreach (var t in result.Templates)
Console.WriteLine($"{t.Name} ({t.Language}, {t.Category}): {t.BodyParamCount} body parameter(s)" +
(t.HeaderFormat is null ? "" : $", {t.HeaderFormat} header"));
record TemplateList(IReadOnlyList<TemplateInfo> Templates);
record TemplateInfo(string Name, string Language, string Category, string Status,
int BodyParamCount, string? HeaderFormat, int ButtonCount);
Python
"""List your approved WhatsApp templates with the everymessage v1 API (Python 3.10+, pip install httpx)."""
import os
import httpx
with httpx.Client(
base_url="https://api.everymessage.com",
auth=(os.environ["EM_API_KEY"], os.environ["EM_SECRET_KEY"]),
timeout=30,
) as client:
response = client.get("/v1/whatsapp/templates", params={"status": "APPROVED"})
if response.is_error:
raise RuntimeError(f"List failed ({response.status_code}): {response.text}")
for t in response.json()["templates"]:
header = f", {t['headerFormat']} header" if t.get("headerFormat") else ""
print(f"{t['name']} ({t['language']}, {t['category']}): "
f"{t['bodyParamCount']} body parameter(s){header}")
