Send WhatsApp
Send a one-off transactional WhatsApp message from an approved template — an OTP, an order update, a receipt.
Endpoint
POST /whatsapp/send
Returns: 202 Accepted
Prerequisites
WhatsApp does not allow business-initiated free-text messages. You must create a template, wait for Meta to approve it, then send it by name. Create and track templates in the Arsel Dashboard under WhatsApp > Templates.
Your organization also needs a connected WhatsApp Business account and available WhatsApp balance. Both are set up in the dashboard under WhatsApp > Settings.
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <your-api-key> | Yes |
Content-Type | application/json | Yes |
Idempotency-Key | Any unique string, max 256 chars | No — see Idempotency |
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient in E.164 format, including the country code. |
templateName | string | Yes | Name of an approved template in your account. Max 512 characters. |
language | string | No | Template language code, e.g. en or ar. Omit when exactly one approved template carries this name. |
parameters | object | No | Values for the template placeholders, keyed by placeholder name. |
Filling in parameters
What goes in parameters depends on the template's category:
| Category | What to pass |
|---|---|
AUTHENTICATION | The one-time code as code. It is repeated automatically into the copy-code button when the template has one. |
UTILITY | One entry per {{placeholder}} in the template body, keyed by placeholder name. |
MARKETING | Same as UTILITY. Marketing templates are subject to the recipient's marketing opt-out. |
language is requiredIf the same template name is approved in more than one language, omitting language returns 409 Conflict. Send the language code explicitly.
Response
{
"id": "01957e3a-4b5c-7d8e-9f0a-1b2c3d4e5f6a"
}
| Field | Type | Description |
|---|---|---|
id | string | Unique message ID. Use it to track delivery via GET /whatsapp/:id. |
202 Accepted means the provider accepted the message, not that it was delivered. Final delivery state arrives asynchronously — poll GET /whatsapp/:id.
Billing
Each message is charged at the rate for the destination country and category. The charge is held when the message is accepted and settled from the delivery receipt. A message the provider rejects is not charged.
Idempotency
Sending the same WhatsApp message twice — for example because a network timeout made you retry — could send the message twice — and bill you for both. To make a retry safe, send an Idempotency-Key header whose value is unique to that WhatsApp message (for example otp/123):
- The first request with a given key is processed normally and its response is stored.
- Any later request with the same key (within 24 hours) is not reprocessed — it returns the original response unchanged, plus an
Idempotent-Replayed: trueresponse header. The side effect happens only once. - After 24 hours the key expires and may be reused.
The key can be any unique string up to 256 characters. A UUID works, but a value derived from the entity the request is about — like otp/123 — is easier to regenerate identically on a retry. Keys are scoped per endpoint, so the same value can be reused independently across the send-email, send-SMS, and send-event endpoints without colliding.
Reuse a key with a different request body and the request is rejected with 409 invalid_idempotent_request, which protects you from receiving the wrong cached response. Use a fresh key for a genuinely different request.
Idempotency error responses
| Status | name | When |
|---|---|---|
400 | invalid_idempotency_key | The key is empty or longer than 256 characters. |
409 | invalid_idempotent_request | The key was already used with a different request body. |
409 | concurrent_idempotent_requests | An earlier request with the same key is still being processed — retry shortly. |
A retry that sends a second one-time code invalidates the first, and the user is left staring at a code that no longer works. Always send an Idempotency-Key with authentication templates.
Examples
Authentication template (OTP)
- cURL
- JavaScript
- Python
- C#
- PHP
curl -X POST "https://api.arsel.sa/v1/whatsapp/send" \
-H "Authorization: Bearer be_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp/123" \
-d '{
"to": "+966512345678",
"templateName": "verification_code",
"language": "en",
"parameters": {
"code": "483920"
}
}'
const response = await fetch("https://api.arsel.sa/v1/whatsapp/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer be_your_api_key",
"Idempotency-Key": "otp/123",
},
body: JSON.stringify({
to: "+966512345678",
templateName: "verification_code",
language: "en",
parameters: { code: "483920" },
}),
});
const { id } = await response.json();
console.log(id); // Use this to track delivery
import requests
response = requests.post(
"https://api.arsel.sa/v1/whatsapp/send",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer be_your_api_key",
"Idempotency-Key": "otp/123",
},
json={
"to": "+966512345678",
"templateName": "verification_code",
"language": "en",
"parameters": {"code": "483920"},
},
)
data = response.json()
print(data["id"]) # Use this to track delivery
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer be_your_api_key");
client.DefaultRequestHeaders.Add("Idempotency-Key", "otp/123");
var payload = new
{
to = "+966512345678",
templateName = "verification_code",
language = "en",
parameters = new { code = "483920" }
};
var json = System.Text.Json.JsonSerializer.Serialize(payload);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.arsel.sa/v1/whatsapp/send", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
<?php
$ch = curl_init("https://api.arsel.sa/v1/whatsapp/send");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer be_your_api_key",
"Idempotency-Key: otp/123"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"to" => "+966512345678",
"templateName" => "verification_code",
"language" => "en",
"parameters" => ["code" => "483920"]
]));
$response = curl_exec($ch);
echo $response;
curl_close($ch);
Utility template with placeholders
- cURL
- JavaScript
- Python
- C#
- PHP
curl -X POST "https://api.arsel.sa/v1/whatsapp/send" \
-H "Authorization: Bearer be_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"to": "+966512345678",
"templateName": "order_shipped",
"language": "ar",
"parameters": {
"firstName": "سارة",
"orderId": "A-1029",
"carrier": "SMSA"
}
}'
const response = await fetch("https://api.arsel.sa/v1/whatsapp/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer be_your_api_key",
},
body: JSON.stringify({
to: "+966512345678",
templateName: "order_shipped",
language: "ar",
parameters: {
firstName: "سارة",
orderId: "A-1029",
carrier: "SMSA",
},
}),
});
const { id } = await response.json();
console.log(id);
import requests
response = requests.post(
"https://api.arsel.sa/v1/whatsapp/send",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer be_your_api_key",
},
json={
"to": "+966512345678",
"templateName": "order_shipped",
"language": "ar",
"parameters": {
"firstName": "سارة",
"orderId": "A-1029",
"carrier": "SMSA",
},
},
)
print(response.json())
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer be_your_api_key");
var payload = new
{
to = "+966512345678",
templateName = "order_shipped",
language = "ar",
parameters = new
{
firstName = "سارة",
orderId = "A-1029",
carrier = "SMSA"
}
};
var json = System.Text.Json.JsonSerializer.Serialize(payload);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.arsel.sa/v1/whatsapp/send", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
<?php
$ch = curl_init("https://api.arsel.sa/v1/whatsapp/send");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer be_your_api_key"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"to" => "+966512345678",
"templateName" => "order_shipped",
"language" => "ar",
"parameters" => [
"firstName" => "سارة",
"orderId" => "A-1029",
"carrier" => "SMSA"
]
]));
$response = curl_exec($ch);
echo $response;
curl_close($ch);
Error Responses
| Status | Meaning | Retry? |
|---|---|---|
400 | The number, the template, or your WhatsApp setup was rejected. Deterministic. | No — the same request fails the same way |
401 | Invalid or missing API key | No |
403 | Recipient opted out, monthly limit reached, or insufficient WhatsApp balance | No |
404 | Template not found | No |
409 | Idempotency-Key reused with a different payload, a request with that key is still processing, or the template name exists in more than one language and language was omitted | No |
422 | Validation error | No |
429 | Rate limit exceeded | Yes — after retry-after |
503 | The provider is unreachable or is rate limiting the account. The message was not sent. | Yes |
- 403 Forbidden
- 404 Not Found
- 409 Conflict
- 503 Unavailable
{
"status_code": 403,
"name": "forbidden",
"message": "Insufficient WhatsApp balance"
}
{
"status_code": 404,
"name": "not_found",
"message": "Template not found"
}
{
"status_code": 409,
"name": "conflict",
"message": "Template name is approved in more than one language — specify `language`"
}
{
"status_code": 503,
"name": "service_unavailable",
"message": "The WhatsApp provider is unavailable. The message was not sent — retry it."
}