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 with the Templates API, or in the Arsel Dashboard under Templates > WhatsApp.
Your organization also needs a connected WhatsApp Business account — set up in the dashboard under Integration > WhatsApp — and available WhatsApp balance.
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. |
template_name | 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. |
503 | 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 $ARSEL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp/123" \
-d '{
"to": "+966512345678",
"template_name": "verification_code",
"language": "en",
"parameters": {
"code": "483920"
}
}'
const response = await fetch("https://api.arsel.sa/v1/whatsapp/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ARSEL_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "otp/123",
},
body: JSON.stringify({
to: "+966512345678",
template_name: "verification_code",
language: "en",
parameters: {
code: "483920",
},
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.name}: ${data.message}`);
}
console.log(data);
import os
import requests
response = requests.post(
"https://api.arsel.sa/v1/whatsapp/send",
headers={
"Authorization": f"Bearer {os.environ['ARSEL_API_KEY']}",
"Idempotency-Key": "otp/123",
},
json={
"to": "+966512345678",
"template_name": "verification_code",
"language": "en",
"parameters": {
"code": "483920",
},
},
)
response.raise_for_status()
print(response.json())
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient(); // share one instance across requests
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("ARSEL_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.arsel.sa/v1/whatsapp/send")
{
Content = new StringContent(
"""
{
"to": "+966512345678",
"template_name": "verification_code",
"language": "en",
"parameters": {
"code": "483920"
}
}
""",
Encoding.UTF8,
"application/json"),
};
request.Headers.Add("Idempotency-Key", "otp/123");
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)response.StatusCode}: {body}");
Console.WriteLine(body);
<?php
$ch = curl_init("https://api.arsel.sa/v1/whatsapp/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ARSEL_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: otp/123",
],
CURLOPT_POSTFIELDS => json_encode([
"to" => "+966512345678",
"template_name" => "verification_code",
"language" => "en",
"parameters" => [
"code" => "483920",
],
]),
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
throw new Exception("{$data['name']}: {$data['message']}");
}
print_r($data);
Utility template with placeholders
- cURL
- JavaScript
- Python
- C#
- PHP
curl -X POST "https://api.arsel.sa/v1/whatsapp/send" \
-H "Authorization: Bearer $ARSEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+966512345678",
"template_name": "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: {
Authorization: `Bearer ${process.env.ARSEL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+966512345678",
template_name: "order_shipped",
language: "ar",
parameters: {
firstName: "سارة",
orderId: "A-1029",
carrier: "SMSA",
},
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.name}: ${data.message}`);
}
console.log(data);
import os
import requests
response = requests.post(
"https://api.arsel.sa/v1/whatsapp/send",
headers={"Authorization": f"Bearer {os.environ['ARSEL_API_KEY']}"},
json={
"to": "+966512345678",
"template_name": "order_shipped",
"language": "ar",
"parameters": {
"firstName": "سارة",
"orderId": "A-1029",
"carrier": "SMSA",
},
},
)
response.raise_for_status()
print(response.json())
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient(); // share one instance across requests
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("ARSEL_API_KEY"));
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.arsel.sa/v1/whatsapp/send")
{
Content = new StringContent(
"""
{
"to": "+966512345678",
"template_name": "order_shipped",
"language": "ar",
"parameters": {
"firstName": "سارة",
"orderId": "A-1029",
"carrier": "SMSA"
}
}
""",
Encoding.UTF8,
"application/json"),
};
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)response.StatusCode}: {body}");
Console.WriteLine(body);
<?php
$ch = curl_init("https://api.arsel.sa/v1/whatsapp/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ARSEL_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"to" => "+966512345678",
"template_name" => "order_shipped",
"language" => "ar",
"parameters" => [
"firstName" => "سارة",
"orderId" => "A-1029",
"carrier" => "SMSA",
],
]),
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
throw new Exception("{$data['name']}: {$data['message']}");
}
print_r($data);
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, 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, or a request with the same Idempotency-Key is still processing. The message was not sent. | Yes |
- 403 Forbidden
- 409 Conflict
- 503 Unavailable
- 404 Not Found
- 401 Unauthorized
{
"status_code": 403,
"name": "forbidden",
"message": "Insufficient WhatsApp balance"
}
{
"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."
}
{
"status_code": 404,
"name": "not_found",
"message": "Template not found"
}
{
"status_code": 401,
"name": "unauthorized",
"message": "Invalid API Key"
}