Marsal WhatsApp API
A simple REST API to send WhatsApp messages and templates, run OTP verification, and manage your conversations, contacts and campaigns — from any programming language.
Authentication
Send your API key as a Bearer token on every request. Create keys in Developers · API. A key is scoped to your workspace and grants full access — keep it secret.
Phone numbers
Always send recipient numbers in international format, digits only (no +, spaces or dashes). Example: a Saudi number becomes 9665xxxxxxxx, an Egyptian number 2010xxxxxxxx.
Response format
Every response is JSON. Success returns "ok": true; a business error returns "ok": false with an error string and a matching HTTP status.
{ "ok": true, "message_id": 4210, "wamid": "wamid.HBg...", "to": "9665xxxxxxxx" }
Send a text message POST
Free text can only be sent inside the 24‑hour customer‑service window. Outside it, use a template.
| Field | Type | Description |
|---|---|---|
to | string | Recipient, international digits only. Required. |
type | string | text (default) or template. |
body | string | Message text. |
curl -X POST "https://usemarsal.cloud/api/v1/messages" \
-H "Authorization: Bearer msk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "to": "9665xxxxxxxx", "type": "text", "body": "أهلاً بيك" }'
const r = await fetch("https://usemarsal.cloud/api/v1/messages", {
method: "POST",
headers: { Authorization: "Bearer msk_live_xxxx", "Content-Type": "application/json" },
body: JSON.stringify({ to: "9665xxxxxxxx", type: "text", body: "أهلاً بيك" })
});
console.log(await r.json());
import requests
r = requests.post("https://usemarsal.cloud/api/v1/messages",
headers={"Authorization": "Bearer msk_live_xxxx"},
json={"to": "9665xxxxxxxx", "type": "text", "body": "أهلاً بيك"})
print(r.json())
$ch = curl_init("https://usemarsal.cloud/api/v1/messages");
curl_setopt_array($ch, [CURLOPT_POST=>true, CURLOPT_RETURNTRANSFER=>true,
CURLOPT_HTTPHEADER=>["Authorization: Bearer msk_live_xxxx","Content-Type: application/json"],
CURLOPT_POSTFIELDS=>json_encode(["to"=>"9665xxxxxxxx","type"=>"text","body"=>"أهلاً بيك"])]);
$res = json_decode(curl_exec($ch), true);
Send a template POST
Approved templates can be sent any time. Pass the name, language, and body variables in order.
| Field | Type | Description |
|---|---|---|
type | string | Must be template. |
template | string | Approved template name. |
language | string | e.g. ar. |
params | array | Body variables, in order. |
curl -X POST "https://usemarsal.cloud/api/v1/messages" \
-H "Authorization: Bearer msk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "to": "9665xxxxxxxx", "type": "template", "template": "order_confirmation", "language": "ar", "params": ["Ahmed", "#9402"] }'
await fetch("https://usemarsal.cloud/api/v1/messages", { method:"POST",
headers:{ Authorization:"Bearer msk_live_xxxx","Content-Type":"application/json" },
body: JSON.stringify({ to:"9665xxxxxxxx", type:"template",
template:"order_confirmation", language:"ar", params:["Ahmed","#9402"] }) });
requests.post("https://usemarsal.cloud/api/v1/messages",
headers={"Authorization":"Bearer msk_live_xxxx"},
json={"to":"9665xxxxxxxx","type":"template",
"template":"order_confirmation","language":"ar","params":["Ahmed","#9402"]})
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ "to"=>"9665xxxxxxxx","type"=>"template", "template"=>"order_confirmation","language"=>"ar","params"=>["Ahmed","#9402"]]));
List conversations GET
Optional status = all|open|pending|closed. Returns each chat with the contact, unread count, and whether the 24h window is open.
curl -X GET "https://usemarsal.cloud/api/v1/conversations?limit=50" \ -H "Authorization: Bearer msk_live_xxxx"
const r = await fetch("https://usemarsal.cloud/api/v1/conversations?limit=50",
{ headers: { Authorization: "Bearer msk_live_xxxx" } });
requests.get("https://usemarsal.cloud/api/v1/conversations?limit=50",
headers={"Authorization":"Bearer msk_live_xxxx"})
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer msk_live_xxxx"]);
Get a conversation's messages GET
Returns messages (in/out) for a conversation. Pass after_id to fetch only newer ones.
curl -X GET "https://usemarsal.cloud/api/v1/conversations/123/messages" \ -H "Authorization: Bearer msk_live_xxxx"
Reply inside a chat POST
Sends a free‑text reply in an existing conversation (inside the 24h window). Pauses the AI bot for that chat (human takeover), same as the inbox.
curl -X POST "https://usemarsal.cloud/api/v1/conversations/123/messages" \
-H "Authorization: Bearer msk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "body": "تمام هجهزلك الطلب" }'
await fetch("https://usemarsal.cloud/api/v1/conversations/123/messages", { method:"POST",
headers:{ Authorization:"Bearer msk_live_xxxx","Content-Type":"application/json" },
body: JSON.stringify({ body:"تمام هجهزلك الطلب" }) });
requests.post("https://usemarsal.cloud/api/v1/conversations/123/messages",
headers={"Authorization":"Bearer msk_live_xxxx"}, json={"body":"تمام هجهزلك الطلب"})
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["body"=>"تمام هجهزلك الطلب"]));
Mark read & toggle AI POST
curl -X POST "https://usemarsal.cloud/api/v1/conversations/123/ai" \
-H "Authorization: Bearer msk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "enabled": 0 }'
List contacts GET
curl -X GET "https://usemarsal.cloud/api/v1/contacts?limit=100" \ -H "Authorization: Bearer msk_live_xxxx"
Add a contact POST
curl -X POST "https://usemarsal.cloud/api/v1/contacts" \
-H "Authorization: Bearer msk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "wa_id": "9665xxxxxxxx", "name": "Ahmed", "email": "a@x.com" }'
await fetch("https://usemarsal.cloud/api/v1/contacts", { method:"POST",
headers:{ Authorization:"Bearer msk_live_xxxx","Content-Type":"application/json" },
body: JSON.stringify({ wa_id:"9665xxxxxxxx", name:"Ahmed", email:"a@x.com" }) });
requests.post("https://usemarsal.cloud/api/v1/contacts",
headers={"Authorization":"Bearer msk_live_xxxx"},
json={"wa_id":"9665xxxxxxxx","name":"Ahmed","email":"a@x.com"})
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["wa_id"=>"9665xxxxxxxx","name"=>"Ahmed"]));
Send an OTP POST
Marsal generates a 6‑digit code, sends it, and stores only its hash. Pass a template to deliver any time; omit it for plain text inside the 24h window.
curl -X POST "https://usemarsal.cloud/api/v1/otp/send" \
-H "Authorization: Bearer msk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "to": "9665xxxxxxxx", "template": "otp_code", "ttl": 300 }'
await fetch("https://usemarsal.cloud/api/v1/otp/send", { method:"POST",
headers:{ Authorization:"Bearer msk_live_xxxx","Content-Type":"application/json" },
body: JSON.stringify({ to:"9665xxxxxxxx", template:"otp_code", ttl:300 }) });
requests.post("https://usemarsal.cloud/api/v1/otp/send",
headers={"Authorization":"Bearer msk_live_xxxx"},
json={"to":"9665xxxxxxxx","template":"otp_code","ttl":300})
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["to"=>"9665xxxxxxxx","template"=>"otp_code","ttl"=>300]));
Verify an OTP POST
Returns { "ok": true, "verified": true|false }. Codes expire after the TTL and lock after 5 wrong attempts.
curl -X POST "https://usemarsal.cloud/api/v1/otp/verify" \
-H "Authorization: Bearer msk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "to": "9665xxxxxxxx", "code": "123456" }'
const { verified } = await (await fetch("https://usemarsal.cloud/api/v1/otp/verify", { method:"POST",
headers:{ Authorization:"Bearer msk_live_xxxx","Content-Type":"application/json" },
body: JSON.stringify({ to:"9665xxxxxxxx", code:"123456" }) })).json();
r = requests.post("https://usemarsal.cloud/api/v1/otp/verify",
headers={"Authorization":"Bearer msk_live_xxxx"}, json={"to":"9665xxxxxxxx","code":"123456"})
print(r.json()["verified"])
$verified = json_decode(curl_exec($ch), true)["verified"];
Templates GET
Your approved message templates (name, language, category, status).
curl -X GET "https://usemarsal.cloud/api/v1/templates" \ -H "Authorization: Bearer msk_live_xxxx"
Numbers GET
Your connected WhatsApp numbers, quality rating and status.
curl -X GET "https://usemarsal.cloud/api/v1/numbers" \ -H "Authorization: Bearer msk_live_xxxx"
Campaigns GET
curl -X GET "https://usemarsal.cloud/api/v1/campaigns" \ -H "Authorization: Bearer msk_live_xxxx"
Dashboard stats GET
Counts for contacts, open conversations, unread, messages today, and campaigns.
curl -X GET "https://usemarsal.cloud/api/v1/dashboard" \ -H "Authorization: Bearer msk_live_xxxx"
Poll for new messages GET
Returns inbound messages newer than since_id, plus the latest id — poll it every few seconds to build a live inbox or trigger your own automations.
curl -X GET "https://usemarsal.cloud/api/v1/updates?since_id=4200" \ -H "Authorization: Bearer msk_live_xxxx"
let since = 0;
setInterval(async () => {
const r = await fetch(`https://usemarsal.cloud/api/v1/updates?since_id=${since}`,
{ headers: { Authorization: "Bearer msk_live_xxxx" } });
const data = await r.json();
since = data.last_id;
data.messages.forEach(m => console.log(m.from, m.body));
}, 5000);
import time, requests
since = 0
while True:
d = requests.get(f"https://usemarsal.cloud/api/v1/updates?since_id={since}",
headers={"Authorization":"Bearer msk_live_xxxx"}).json()
since = d["last_id"]
for m in d["messages"]: print(m["from"], m["body"])
time.sleep(5)
$d = json_decode(file_get_contents("https://usemarsal.cloud/api/v1/updates?since_id=$since"), true);
Endpoint index
| Method | Path | Description |
|---|---|---|
| POST | /messages | Send text or template |
| POST | /otp/send | Send an OTP |
| POST | /otp/verify | Verify an OTP |
| GET | /conversations | List conversations |
| GET | /conversations/{id}/messages | Get messages |
| POST | /conversations/{id}/messages | Reply in a chat |
| POST | /conversations/{id}/read | Mark read |
| POST | /conversations/{id}/ai | Toggle AI bot |
| GET | /contacts | List contacts |
| POST | /contacts | Add a contact |
| GET | /templates | List templates |
| GET | /numbers | List numbers |
| GET | /campaigns | List campaigns |
| GET | /dashboard | Workspace stats |
| GET | /updates | Poll new inbound |
Errors
| HTTP | error | Meaning |
|---|---|---|
| 401 | unauthenticated | Missing or invalid API key. |
| 403 | no_workspace | Key not linked to a workspace. |
| 404 | conversation_not_found | No such conversation in your workspace. |
| 409 | no_whatsapp_number / session_closed | No connected number, or the 24h window is closed. |
| 422 | to_required / body_required | A required field is missing. |
| 502 | send_failed | WhatsApp rejected the message (see detail). |
Ready to build?
Grab your API key and send your first WhatsApp message in minutes.
Get your API key →