Cookies and analytics
We use analytics and marketing cookies to improve the site. They are only set with your consent. Cookie Policy.
The API lets you send and verify OTP codes via WhatsApp, as well as send marketing or utility messages from your templates. Each project has its own access credentials.
Log in to the portal to get your API URL and authentication keys.
Open Swagger UIInteractive specification (OpenAPI)
Complete interactive technical documentation of endpoints is available via Swagger UI: request/response schemas, models, error codes.
All requests to OTP endpoints require two keys sent in HTTP headers:
Your keys grant access to your project. Never put them in a mobile app, browser, Git repository or public message. If you suspect exposure, regenerate them from the project settings.
Sends a 6-digit OTP code to the specified WhatsApp number, or a marketing/utility message via a catalog template.
POST /v1/otp/send
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000"
}To send a non-OTP (utility/marketing) message with variables, specify a catalog template and its variables, in placeholder order:
{
"phone": "+22670000000",
"template": "order_update",
"template_language": "fr",
"variables": ["Awa", "#1042"]
}If the template has a dynamic URL button (configured in the portal), provide its suffix via "button_variable" - separate from body variables, button-specific numbering.
{
"phone": "+22670000000",
"template": "invite_friend",
"template_language": "fr",
"variables": ["Awa", "MonApp"],
"button_variable": "inv_abc123"
}You can provide your own OTP code through the "code" field (4 to 8 digits). Otherwise, the platform generates it automatically.
{
"phone": "+22670000000",
"code": "483920"
}| Field | Type | Description |
|---|---|---|
| phone | string | Phone in E.164 format (+226XXXXXXXX) |
| template | string · optional | Name of a catalog template. Omitted = project's default template. |
| template_language | string · optional | Template language (e.g. fr, en_US). Omitted = template's default language. |
| code | string · optional | Optional 4-8 digit OTP code. If omitted, the platform generates it. The code can then be verified through /otp/verify. |
| variables | string[] · optional | Body values in order of {{1}}, {{2}}, {{3}} placeholders. Required for utility/marketing templates. Auto-filled positions (generated OTP code, project name) must not be provided. |
| button_variable | string · optional | Dynamic URL button suffix. Required only if the template has a URL button configured in dynamic mode. |
{
"success": true,
"code": "OTP_SENT",
"message": "OTP sent",
"data": null
}| Code | Status | Cause |
|---|---|---|
| AUTH_REQUIRED | 401 | Missing X-API-Key and/or X-Auth-Key headers |
| AUTH_INVALID | 401 | Invalid keys or non-matching X-API-Key / X-Auth-Key pair - message specifies which key |
| PLAN_INACTIVE | 403 | Plan inactive or expired - renew from Billing section |
| INVALID_PHONE | 400 | Invalid number (E.164 format required) |
| TEMPLATE_NOT_FOUND | 400 | Template not found or not available in catalog |
| TEMPLATE_VARIABLES_MISMATCH | 400 | Wrong number of variables for this template |
| BUTTON_VARIABLE_REQUIRED | 400 | The dynamic URL button suffix ("button_variable") is required for this template |
| RATE_LIMITED | 429 | Plan hourly limit reached, or 3 messages/10 min for same number (then 15 min block) - message specifies limit and delay |
| QUOTA_EXCEEDED | 429 | Plan monthly quota exhausted - upgrade or wait for renewal |
| OTP_SEND_FAILED | 400 | WhatsApp send error (number not registered on WhatsApp) |
Verifies an OTP code. Code expires after 5 minutes. After 5 incorrect attempts, the number is blocked for 15 minutes.
POST /v1/otp/verify
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000",
"code": "482931"
}| Field | Type | Description |
|---|---|---|
| phone | string | E.164 number (same as used for sending) |
| code | string | Code received via WhatsApp (4 to 8 digits) |
{
"success": true,
"code": "OTP_VERIFIED",
"message": "OTP verified",
"data": null
}| Code | Status | Cause |
|---|---|---|
| OTP_INVALID | 400 | Incorrect code (message shows remaining attempts), expired, or already used |
| RATE_LIMITED | 429 | Too many incorrect attempts → number blocked 15 min (message shows remaining time) |
Send WhatsApp messages (text, image, document, audio, video, template) to any phone number. Each message consumes credits from the project quota.
Log in to the portal to get your API URL and authentication keys.
All requests require X-API-Key (project key) and X-Auth-Key (account key) headers, available in the portal Projects → Manage project → Configuration.
Free-form messages (text, image, document, audio and video) can only be sent during the 24 hours following a user message. Outside this window, use an approved WhatsApp template via /v1/otp/send or /v1/messages/template.
Choose the endpoint for your use case. All three use the same X-API-Key and X-Auth-Key headers.
| Endpoint | Use it for | Main features |
|---|---|---|
| POST /v1/otp/send | OTP with code verification | Automatically generated or user-provided code, then verification with /otp/verify |
| POST /v1/messages/template | An already configured WhatsApp template | Variables, dynamic button, coupon, text header or location |
| POST /v1/messages/send | Text, media or template through one endpoint | type text/image/document/audio/video/template and dynamic header_media |
| Endpoint | Description | Quota cost |
|---|---|---|
| POST /v1/messages/text | Simple text message (max 4096 characters) | 1 credit |
| POST /v1/messages/image | Image with public URL or Meta media_id + optional caption (max 1024 characters) | 2 credits |
| POST /v1/messages/document | Document (PDF, etc.) with public URL or Meta media_id + optional filename and caption | 2 credits |
| POST /v1/messages/audio | Audio file with public URL or Meta media_id | 2 credits |
| POST /v1/messages/video | Video with public URL or Meta media_id + optional caption (max 1024 characters) | 2 credits |
| POST /v1/messages/template | Sends a WhatsApp template (UTILITY/MARKETING) with body variables, dynamic URL button and copy_code button. The template must exist in the portal's visible catalog. | 1 credit |
| POST /v1/media/upload | Upload a file to Meta Cloud API via multipart/form-data and return the classic whatsapp_media_id plus the Resumable whatsapp_handle (4::...) when available. | - |
| GET /v1/media/:id | Retrieve metadata of an uploaded media | - |
| DELETE /v1/media/:id | Delete the media from Meta Cloud API and mark as DELETED in DB | - |
Simple text message (max 4096 characters)
POST /v1/messages/text
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000",
"text": "Bonjour, ceci est un message texte simple."
}Image with public URL or Meta media_id + optional caption (max 1024 characters)
POST /v1/messages/image
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000",
"image": { "url": "https://example.com/image.jpg" },
"caption": "Voici votre image"
}ℹ️ You can also provide a Meta media_id already uploaded via /v1/media/upload instead of a URL.
Document (PDF, etc.) with public URL or Meta media_id + optional filename and caption
POST /v1/messages/document
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000",
"document": { "url": "https://example.com/document.pdf" },
"filename": "facture.pdf",
"caption": "Votre facture"
}Audio file with public URL or Meta media_id
POST /v1/messages/audio
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000",
"audio": { "url": "https://example.com/audio.mp3" }
}Video with public URL or Meta media_id + optional caption (max 1024 characters)
POST /v1/messages/video
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000",
"video": { "url": "https://example.com/video.mp4" },
"caption": "Votre vidéo"
}Sends a WhatsApp template (UTILITY/MARKETING) with body variables, dynamic URL button and copy_code button. The template must exist in the portal's visible catalog.
POST /v1/messages/template
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"phone": "+22670000000",
"template": "confirmation_commande",
"template_language": "fr",
"variables": ["Awa", "#1042"],
"button_variable": "inv_abc123",
"copy_code_value": "PROMO20"
}Variables in order of {1}, {2}… placeholders - excluding auto positions (OTP code, project name). Dynamic URL button → button_variable required. Copy code button → copy_code_value required (15 max).
Each template area has its own field. Do not mix body variables with header or button variables.
| API field | Location | Usage |
|---|---|---|
variables | {{1}}, {{2}} | Values for placeholders in the message body, in position order. |
header_param | header {{1}} | Value for a dynamic text header. Only one header variable is allowed. |
button_variable | Suffix after the base URL | Value appended to a URL button configured as dynamic. |
copy_code_value | Copy-code button | Coupon code sent at delivery time, maximum 15 characters. |
For a dynamic IMAGE, VIDEO or DOCUMENT header, use POST /v1/messages/send with header_media. Provide the classic whatsapp_media_id for direct sending, or a public URL accessible by Meta.
POST /v1/messages/send
Content-Type: application/json
X-API-Key: sk_live_votre_cle_api
X-Auth-Key: au_live_votre_cle_auth
{
"type": "template",
"phone": "+22670000000",
"template": "promotion_client",
"template_language": "fr",
"variables": ["Awa", "20"],
"header_media": {
"type": "image",
"media_id": "123456789012345"
}
}Upload a file to Meta Cloud API via multipart/form-data and return the classic whatsapp_media_id plus the Resumable whatsapp_handle (4::...) when available.
curl -X POST https://api.exemple.com/v1/media/upload \ -H "X-API-Key: sk_live_votre_cle_api" \ -H "X-Auth-Key: au_live_votre_cle_auth" \ -F "file=@image.jpg"
Supported types: image (jpeg, png, webp), document (pdf), audio (mpeg, ogg, mp4), video (mp4, 3gpp). Max size: 100 MB.
Use whatsapp_media_id to send media. If whatsapp_handle is returned, use it as header_handle when creating a template. Never share these identifiers outside your integration.
{
"success": true,
"code": "MEDIA_UPLOADED",
"data": {
"media_id": "clmedia123",
"whatsapp_media_id": "123456789012345",
"whatsapp_handle": "4::aW1hZ2VfZnJhaXM",
"storage_url": "https://api.exemple.com/v1/media/files/clmedia123.jpg"
}
}// JavaScript (fetch)
const res = await fetch("https://api.exemple.com/v1/messages/text", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "sk_live_votre_cle_api",
"X-Auth-Key": "au_live_votre_cle_auth"
},
body: JSON.stringify({
phone: "+22670000000",
text: "Bonjour !"
})
});
const data = await res.json();| Field | Type | Description |
|---|---|---|
| phone | string | Recipient phone in E.164 format (e.g. +22670000000) |
| text | string | Message text content (max 4096 characters) |
image | object | Media source to provide with url or media_id |
document | object | Media source to provide with url or media_id |
audio | object | Media source to provide with url or media_id |
video | object | Media source to provide with url or media_id |
| template | string | Template name in the portal catalog |
| template_language | string | Template language (fr, en_US) |
| variables | string[] | Body variables in order of {1}, {2}… placeholders (excluding auto positions) |
| button_variable | string | Dynamic URL button suffix (required if template has a dynamic URL button) |
| copy_code_value | string | Coupon code for copy code button (required if template has one, 15 chars max.) |
| caption | string | Optional caption (max 1024 characters) |
| filename | string | Filename for documents (e.g. invoice.pdf) |
{
"success": true,
"code": "MESSAGE_QUEUED",
"message": "Message mis en file d'attente",
"data": null
}| Code | Status | Cause |
|---|---|---|
| AUTH_REQUIRED | 401 | Missing authentication headers (X-API-Key / X-Auth-Key) |
| AUTH_INVALID | 401 | Invalid keys or non-matching pair |
| INVALID_PHONE | 400 | Invalid number (E.164 format required) |
| INVALID_PAYLOAD | 400 | Invalid request body or missing field |
| TEMPLATE_NOT_FOUND | 400 | Template not found or not available |
| TEMPLATE_VARIABLES_MISMATCH | 400 | Wrong number of variables for this template |
| BUTTON_VARIABLE_REQUIRED | 400 | The dynamic URL button suffix is required |
| COPY_CODE_VALUE_REQUIRED | 400 | The copy code value (copy_code_value) is required for this template |
| QUOTA_EXCEEDED | 429 | Monthly quota exhausted - upgrade your plan |
| RATE_LIMITED | 429 | Hourly rate limit reached (1000 req/h/project) |
| MEDIA_UPLOAD_FAILED | 400 | Meta Cloud API upload failed |
| INVALID_MIME_TYPE | 400 | Unsupported MIME type |
| FILE_TOO_LARGE | 413 | File too large (> 100 MB) |
Receive an HTTP POST on your server as soon as an OTP is successfully verified. Ideal for triggering server-side actions (activate account, unlock payment…) without polling the API.
Set the callback URL in the portal: Projects → Manage project → Configuration → Verification webhook. Keep the signing key on your server and never publish it.
POST https://votre-serveur.com/otp-callback
Content-Type: application/json
X-OTP-Signature: sha256=<hmac>
{
"event": "otp.verified",
"project_id": "clx0a1b2c3...",
"phone": "+22670000000",
"phone_hash": "a3f9c8...",
"phone_masked": "+226****000",
"timestamp": "2026-07-23T10:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
| event | string | Event type (always "otp.verified") |
| project_id | string | Project ID concerned |
| phone | string | Full verified number (E.164 format) |
| phone_hash | string | Anonymized technical identifier for matching your events |
| phone_masked | string | Masked number for display |
| timestamp | string | ISO 8601 timestamp of verification |
Each call contains an X-OTP-Signature value. Verify it with your project key before processing the event to confirm its authenticity.
import crypto from "crypto";
// secret affiché dans le portail (Projet → Configuration → Webhook)
const expected = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(rawBody) // corps BRUT, non re-sérialisé
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers["x-otp-signature"] || "")
);
if (!ok) return res.status(401).end();These endpoints require an authenticated session via the portal (client_session cookie).
Global account quota.
{ "data": { "used": 42, "limit": 1000, "plan": { "name": "PRO" }, "reset_at": "2026-06-01T00:00:00.000Z" } }Optional period parameter: 7d, 30d (default), month or all.
{ "data": { "sent": 42, "verified": 38, "failed": 7, "failedDelivery": 4, "failedVerification": 3, "queued": 2, "successRate": 91, "period": "30d" } }failed is total failures; broken down into failedDelivery (message not sent) and failedVerification (incorrect or expired code entered by end user). successRate is delivery rate: sent / (sent + failedDelivery). Queued sends and verification failures are excluded, field is null until at least one send completes in the period.
{ "data": { "items": [ { "phone_masked": "+22670******000", "action": "SENT", "created_at": "..." } ], "total": 42 } }curl -X POST https://api.exemple.com/v1/otp/send \
-H "X-API-Key: sk_live_votre_cle_api" \
-H "X-Auth-Key: au_live_votre_cle_auth" \
-H "Content-Type: application/json" \
-d '{"phone": "+22670000000"}'curl -X POST https://api.exemple.com/v1/otp/verify \
-H "X-API-Key: sk_live_votre_cle_api" \
-H "X-Auth-Key: au_live_votre_cle_auth" \
-H "Content-Type: application/json" \
-d '{"phone": "+22670000000", "code": "482931"}'const API_URL = "https://api.exemple.com/v1";
const HEADERS = {
"Content-Type": "application/json",
"X-API-Key": "sk_live_...",
"X-Auth-Key": "au_live_..."
};
async function sendOtp(phone) {
const res = await fetch(`${API_URL}/otp/send`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ phone })
});
const data = await res.json();
if (!data.success) throw new Error(data.code);
return data;
}
async function verifyOtp(phone, code) {
const res = await fetch(`${API_URL}/otp/verify`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ phone, code })
});
const data = await res.json();
if (!data.success) throw new Error(data.code);
return data;
}
// Usage
await sendOtp("+22670000000");
// User enters the code received on WhatsApp
await verifyOtp("+22670000000", "482931");import requests
API_URL = "https://api.exemple.com/v1"
HEADERS = {
"Content-Type": "application/json",
"X-API-Key": "sk_live_...",
"X-Auth-Key": "au_live_..."
}
def send_otp(phone: str) -> dict:
res = requests.post(f"{API_URL}/otp/send", headers=HEADERS, json={"phone": phone})
data = res.json()
if not data.get("success"):
raise Exception(data.get("code"))
return data
def verify_otp(phone: str, code: str) -> dict:
res = requests.post(f"{API_URL}/otp/verify", headers=HEADERS, json={"phone": phone, "code": code})
data = res.json()
if not data.get("success"):
raise Exception(data.get("code"))
return data
# Usage
send_otp("+22670000000")
# User enters the code received on WhatsApp
verify_otp("+22670000000", "482931")| Limit | Value | Description |
|---|---|---|
| Verification attempts | 5 | 15 min block |
| OTP expiry | 5 min | Code invalid after delay |
| OTP sends | Per plan | Hourly rate per project, set by your plan (+ 3 OTP/10 min per number) |
| Global requests | 100/min | Per IP |
All errors follow the same format:
{
"success": false,
"code": "ERROR_CODE",
"message": "Description lisible",
"data": null
}| Code | HTTP Status | Cause |
|---|---|---|
| AUTH_REQUIRED | 401 | Missing authentication headers (X-API-Key / X-Auth-Key) |
| AUTH_INVALID | 401 | Invalid keys or non-matching pair - message specifies which key |
| PLAN_INACTIVE | 403 | Plan inactive or expired - renew from portal |
| INVALID_PAYLOAD | 400 | Invalid request body or missing field |
| INVALID_PHONE | 400 | Invalid phone number (E.164 format required) |
| OTP_INVALID | 400 | Incorrect, expired (5 min), or already used code - message specifies cause and remaining attempts |
| OTP_SEND_FAILED | 400 | WhatsApp send failed (number unreachable) |
| RATE_LIMITED | 429 | Too many requests or incorrect attempts - message specifies limit and unlock delay |
| QUOTA_EXCEEDED | 429 | Plan monthly quota exhausted - upgrade or renewal required |
| SERVER_ERROR | 500 | Internal error - retry or contact support |
When a WhatsApp OTP send fails because the number is not registered on WhatsApp, the SMS fallback automatically sends the code via SMS. This feature is disabled by default and must be enabled in each project's settings.
In the portal, go to Projects → Manage project → SMS Fallback → Enable the toggle. Country tariffs are visible in the same section.
The cost of each SMS fallback depends on the recipient's country. Tariffs are managed by the administrator and visible in the portal. If a country is not configured or its tariff is inactive, the SMS is not sent.
Send bulk SMS to your users. SMS campaigns are separate from WhatsApp campaigns and fully managed by the platform.
SMS campaign endpoints require an authenticated session via the portal (client_session cookie).
| Endpoint | Method | Description |
|---|---|---|
| /account/sms-campaigns | GET | List campaigns |
| /account/sms-campaigns | POST | Create a campaign |
| /account/sms-campaigns/:id/start | POST | Start a campaign |
| /account/sms-campaigns/:id/cancel | POST | Cancel a campaign |
| /account/sms-campaigns/:id/retry | POST | Retry failures |
| /account/sms-campaigns/:id | DELETE | Delete a campaign |
When creating, you can specify a future send date (scheduled_at). The campaign will automatically switch to RUNNING status at the scheduled time.
POST /account/sms-campaigns
Content-Type: application/json
{
"name": "Promo Black Friday",
"message": "Profitez de -50% sur toute la boutique !",
"phone_numbers": ["+22670000001", "+22670000002"],
"scheduled_at": "2026-11-24T08:00:00.000Z" // optionnel
}DRAFT - DRAFT - Created, not yet launched
SCHEDULED - SCHEDULED - Planned for a future date
RUNNING - RUNNING - Sending in progress
COMPLETED - COMPLETED - All SMS sent
PARTIAL - PARTIAL - Some SMS failed
CANCELLED - CANCELLED - Cancelled
SMS tariffs are defined by country and phone prefix. Each SMS sent via fallback or SMS campaign consumes the number of credits corresponding to the recipient's country.
The phone prefix is compared against configured tariffs. The number of credits deducted matches the country's tariff. If the country is not found or the tariff is inactive, the SMS is blocked.
Check the SMS pricing page in the portal to see the list of configured countries and their tariffs. Unconfigured countries block SMS sending.
| Country | Prefix | SMS cost |
|---|---|---|
| 🇧🇫 Burkina Faso | +226 | 3 credits |
| 🇨🇮 Côte d'Ivoire | +225 | 4 credits |
| 🇳🇪 Niger | +227 | 4 credits |
| 🇨🇲 Cameroun | +237 | 7 credits |
| 🇨🇩 RD Congo | +243 | 7 credits |
An SMS to a BF number (+226...) costs 3 credits. An SMS to a CM number (+237...) costs 10 credits. The credit is deducted from the project quota.
In the portal, open the SMS campaigns menu: your campaigns show the cost per send. In each project's settings, the SMS fallback section lists the rates by country.