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.

Base URL
https://usemarsal.cloud/api/v1
Get your API key →

Prefer not to read? Jump to Tools & SDKs for the OpenAPI description, the Postman collection and the PHP client.

All examples in:

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.

Authorization: Bearer msk_live_xxxxxxxxxxxxxxxxxxxx

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

POST https://usemarsal.cloud/api/v1/messages

Free text can only be sent inside the 24‑hour customer‑service window. Outside it, use a template.

FieldTypeDescription
tostringRecipient, international digits only. Required.
typestringtext (default) or template.
bodystringMessage 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

POST https://usemarsal.cloud/api/v1/messages

Approved templates can be sent any time. Pass the name, language, and body variables in order.

FieldTypeDescription
typestringMust be template.
templatestringApproved template name.
languagestringe.g. ar.
paramsarrayBody 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

GET https://usemarsal.cloud/api/v1/conversations?status=open&limit=50

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

GET https://usemarsal.cloud/api/v1/conversations/{id}/messages?after_id=0

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

POST https://usemarsal.cloud/api/v1/conversations/{id}/messages

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

POST https://usemarsal.cloud/api/v1/conversations/{id}/read
POST https://usemarsal.cloud/api/v1/conversations/{id}/ai   { "enabled": 0|1 }
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

GET https://usemarsal.cloud/api/v1/contacts?q=ahmed&limit=100
curl -X GET "https://usemarsal.cloud/api/v1/contacts?limit=100" \
  -H "Authorization: Bearer msk_live_xxxx"

Add a contact POST

POST https://usemarsal.cloud/api/v1/contacts
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

POST https://usemarsal.cloud/api/v1/otp/send

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

POST https://usemarsal.cloud/api/v1/otp/verify

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

GET https://usemarsal.cloud/api/v1/templates

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

GET https://usemarsal.cloud/api/v1/numbers

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

GET https://usemarsal.cloud/api/v1/campaigns
curl -X GET "https://usemarsal.cloud/api/v1/campaigns" \
  -H "Authorization: Bearer msk_live_xxxx"

Dashboard stats GET

GET https://usemarsal.cloud/api/v1/dashboard

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

GET https://usemarsal.cloud/api/v1/updates?since_id=0

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);

Webhooks

Polling /updates is fine for a script. For a production integration, let Marsal call you: register an endpoint and we POST a signed JSON body the moment something happens — a message arrives, a send is delivered, a chat is assigned or closed, a campaign finishes, Meta approves a template.

Register an endpoint

Workspace → Developers · API → Webhooks. Paste your https URL, tick the events you want, and a signing secret is generated for you. Webhooks are part of the Enterprise plan and above.

Open the webhooks panel →

The envelope

Every event uses the same wrapper; only data changes per event.

{
  "event": "message.received",
  "tenant_id": 12,
  "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "ts": 1758326400,
  "data": { ... }
}

Headers

HeaderValue
X-Marsal-EventThe event name, e.g. message.received.
X-Marsal-Delivery32-hex id, stable across retries — use it to deduplicate.
X-Marsal-Attempt1 on the first try, then 2, 3…
X-Marsal-TimestampUnix seconds at which this attempt was signed.
X-Marsal-Signature-V2Authoritative. t=<unix>,v1=<hex> where hex = HMAC-SHA256 of "<t>.<raw body>" with your secret.
X-Marsal-SignatureLegacy. HMAC-SHA256 of the raw body alone — no timestamp, so it cannot detect a replay. Still sent so existing integrations keep working; verify V2 in anything new.

Event reference

EventFires when
message.receivedA customer sent you a WhatsApp message.
message.sentYour workspace sent a message (agent, API, bot, auto-reply or scheduled send).
message.statusWhatsApp reported sent / delivered / read / failed for one of your messages.
conversation.assignedA chat was handed to an agent, moved between agents, or released.
conversation.closedA chat was closed from the inbox, the API or after a service rating.
csat.receivedA customer replied with a 1–5 satisfaction score.
contact.createdA contact was added to your workspace for the first time.
campaign.finishedA broadcast campaign stopped sending (completed or aborted).
template.status_changedMeta approved, rejected or deleted one of your message templates.

Broadcast campaigns do not emit message.sent per recipient — a 50,000-contact campaign would mean 50,000 deliveries. Subscribe to campaign.finished for the totals, and to message.status if you need per-recipient delivery state (each of those carries campaign_id).

message.received

A customer sent you a WhatsApp message.

{
    "event": "message.received",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "conversation_id": 8421,
        "human_reply_at": null,
        "contact": {
            "id": 5107,
            "wa_id": "966550521672",
            "name": "Ahmed"
        },
        "type": "text",
        "text": "هل المنتج متوفر؟"
    }
}

message.sent

Your workspace sent a message (agent, API, bot, auto-reply or scheduled send).

{
    "event": "message.sent",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "message_id": 91733,
        "conversation_id": 8421,
        "contact_id": 5107,
        "type": "text",
        "text": "أهلاً بيك، أيوه متوفر ✅",
        "wa_message_id": "wamid.HBgMOTY2NTUwNTIxNjcy",
        "status": "sent",
        "sender_user_id": 42,
        "template_id": null
    }
}

message.status

WhatsApp reported sent / delivered / read / failed for one of your messages.

{
    "event": "message.status",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "wa_message_id": "wamid.HBgMOTY2NTUwNTIxNjcy",
        "status": "delivered",
        "error": null,
        "campaign_id": null,
        "recipient_wa_id": "966550521672"
    }
}

conversation.assigned

A chat was handed to an agent, moved between agents, or released.

{
    "event": "conversation.assigned",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "conversation_id": 8421,
        "from_user_id": null,
        "to_user_id": 42,
        "reason": "manual",
        "contact_id": 5107
    }
}

conversation.closed

A chat was closed from the inbox, the API or after a service rating.

{
    "event": "conversation.closed",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "conversation_id": 8421,
        "contact_id": 5107,
        "source": "inbox",
        "assigned_user_id": 42
    }
}

csat.received

A customer replied with a 1–5 satisfaction score.

{
    "event": "csat.received",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "conversation_id": 8421,
        "contact_id": 5107,
        "rating": 5
    }
}

contact.created

A contact was added to your workspace for the first time.

{
    "event": "contact.created",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "contact_id": 5107,
        "wa_id": "966550521672",
        "name": "Ahmed",
        "email": null,
        "source": "inbound"
    }
}

campaign.finished

A broadcast campaign stopped sending (completed or aborted).

{
    "event": "campaign.finished",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "campaign_id": 311,
        "name": "عرض رمضان",
        "status": "sent",
        "total": 1200,
        "sent": 1187,
        "delivered": 1102,
        "read": 840,
        "failed": 13
    }
}

template.status_changed

Meta approved, rejected or deleted one of your message templates.

{
    "event": "template.status_changed",
    "tenant_id": 12,
    "delivery_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "ts": 1758326400,
    "data": {
        "template_id": 77,
        "name": "order_confirmation",
        "language": "ar",
        "category": "UTILITY",
        "old_status": "PENDING",
        "status": "APPROVED"
    }
}

Verify a signature

Compute the HMAC over "<timestamp>.<raw request body>" and compare it to v1 in constant time. Use the raw body exactly as received — parsing and re-encoding the JSON changes the bytes and the signature will not match. Reject anything older than five minutes so a captured delivery cannot be replayed at you later.

PHP

<?php
$secret = 'your_webhook_secret';           // from the Developers panel
$raw    = file_get_contents('php://input');
$hdr    = $_SERVER['HTTP_X_MARSAL_SIGNATURE_V2'] ?? '';

$t = 0; $v1 = '';
foreach (explode(',', $hdr) as $part) {
    [$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
    if ($k === 't')  $t  = (int) $v;
    if ($k === 'v1') $v1 = $v;
}

$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);

if ($v1 === '' || !hash_equals($expected, $v1)) { http_response_code(401); exit('bad signature'); }
if (abs(time() - $t) > 300)                    { http_response_code(401); exit('stale'); }

$event = json_decode($raw, true);
// Deduplicate on the delivery id — a retry repeats it.
// if (already_seen($_SERVER['HTTP_X_MARSAL_DELIVERY'])) { http_response_code(200); exit; }

error_log($event['event'] . ' ' . json_encode($event['data']));
http_response_code(200);                   // anything outside 2xx is retried
echo 'ok';

Node.js (Express)

const express = require("express");
const crypto  = require("crypto");

const app    = express();
const SECRET = process.env.MARSAL_WEBHOOK_SECRET;

// express.json() would hand you a parsed object; keep the raw bytes for the HMAC.
app.post("/marsal-webhook", express.raw({ type: "*/*" }), (req, res) => {
  const header = req.get("X-Marsal-Signature-V2") || "";
  const parts  = Object.fromEntries(header.split(",").map(p => p.trim().split("=")));
  const t  = parseInt(parts.t, 10);
  const v1 = parts.v1 || "";

  const expected = crypto.createHmac("sha256", SECRET)
                         .update(`${t}.${req.body.toString("utf8")}`)
                         .digest("hex");

  const a = Buffer.from(expected, "utf8"), b = Buffer.from(v1, "utf8");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).send("bad signature");
  if (!t || Math.abs(Date.now() / 1000 - t) > 300)            return res.status(401).send("stale");

  const evt = JSON.parse(req.body.toString("utf8"));
  console.log(req.get("X-Marsal-Delivery"), evt.event, evt.data);

  res.status(200).send("ok");   // reply fast; do the slow work after
});

app.listen(3000);

Retries, timeouts & endpoint requirements

RuleValue
SuccessAny 2xx. Everything else — including 3xx — counts as a failure.
Timeout5s total, 3s to connect. Acknowledge first, process afterwards.
RetriesUp to 6 attempts. The gap doubles each time (30s, 60s, 2m, 4m, 8m…, capped at 1h) with ±20% jitter so a recovering endpoint is not hit by a thundering herd.
OrderingNot guaranteed. A retried event can arrive after a newer one — use ts and the ids in data, not arrival order.
At-least-onceA delivery can repeat (we retried something your server did receive). Deduplicate on X-Marsal-Delivery.
Body sizeCapped at 256 KB. An oversized payload arrives with "_truncated": true and its scalar fields only — fetch the detail over the REST API.
Response sizeWe read at most 64 KB of your reply, and only to show it in the delivery log.

What your endpoint must be

RequirementWhy
https only, port 443The payload carries customer messages and phone numbers; plain http is refused at registration and at send time.
Valid TLS certificateThe chain and host name are verified. Self-signed certificates fail.
Publicly routable addressLoopback, private (RFC1918), link-local, CGNAT and reserved ranges are refused — for IPv4 and IPv6, and for every address the name resolves to. The address is re-checked on every attempt and pinned for the request, so a name that later points somewhere private stops being called.
No redirectsA 3xx is not followed and is treated as a failure. Register the final URL.
No credentials in the URLhttps://user:pass@host/ is rejected. Put the secret in the signature check, not the URL.

How to test

Open Developers · API, expand a webhook and use Send test event: it POSTs a fully signed sample of whichever event you pick and shows you the HTTP status, the round-trip time and the first 480 characters of your response. The attempt is written to the delivery log like any other.

Building the receiver first? Point the webhook at a public request-bin service to see the exact bytes and headers, then swap in your own URL. A local http://localhost or ngrok-over-http endpoint will be refused — expose it over https.

When something is wrong, the Delivery log on the same page shows every attempt with its status, try count, response code, round-trip time and the error or response snippet, plus a Retry now button that replays the exact stored body.

Tools & SDKs

Everything on this page is also available as a machine-readable description, so you can generate a client, mock the API, or click through it in Postman instead of copying cURL commands by hand.

OpenAPI 3.1 description

Every endpoint, parameter, response shape and error code above — plus the nine webhook events and their signature headers — in one file. Import it into Postman, Insomnia, Bruno or Stoplight, or run it through an OpenAPI generator to build a client in your own language.

curl -O https://usemarsal.cloud/openapi.yaml

# generate a client in any language the generator supports
npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g python -o ./marsal-client

Postman collection

Generated from the description above, with a folder per resource and a working example on every request. Import both files, pick the Marsal Cloud environment, and set api_key to a key from Developers · API — every request inherits the bearer token from the collection, so nothing else needs configuring.

FileWhat it is
marsal.postman_collection.jsonThe collection — 15 requests, schema v2.1.
marsal.postman_environment.jsonThe environment — base_url, api_key, conversation_id.

Firing a request from Postman sends a real WhatsApp message from your workspace and is billed like any other send. The GET requests and /otp/verify are safe to explore with.

PHP SDK

A small, dependency-free client for PHP 8.0+ — cURL and nothing else. It covers every endpoint on this page, throws a typed exception per error, retries 429s and 5xx with backoff, and ships a webhook verifier that checks the V2 signature and the timestamp window for you.

Download the PHP SDK (.zip) →

MIT licensed. Unzip it anywhere and require src/autoload.php — no Composer needed.

<?php
require __DIR__ . '/marsal-php-sdk/src/autoload.php';

$marsal = new \Marsal\Api\Client(getenv('MARSAL_API_KEY'));

$marsal->sendText('9665xxxxxxxx', 'أهلاً بيك');
$marsal->sendTemplate('9665xxxxxxxx', 'order_confirmation', 'ar', ['Ahmed', '#9402']);

// verifying a webhook, including the replay window
$event = (new \Marsal\Api\WebhookVerifier(getenv('MARSAL_WEBHOOK_SECRET')))->verifyRequest();
echo $event->name;   // message.received

Other languages. PHP is the only client library we publish today — there is no official Node.js, Python, C#, Java or Go SDK yet. The examples throughout this page are plain HTTP in each language and work as they stand, and the OpenAPI description above will generate a typed client for any language your generator supports.

Endpoint index

MethodPathDescription
POST/messagesSend text or template
POST/otp/sendSend an OTP
POST/otp/verifyVerify an OTP
GET/conversationsList conversations
GET/conversations/{id}/messagesGet messages
POST/conversations/{id}/messagesReply in a chat
POST/conversations/{id}/readMark read
POST/conversations/{id}/aiToggle AI bot
GET/contactsList contacts
POST/contactsAdd a contact
GET/templatesList templates
GET/numbersList numbers
GET/campaignsList campaigns
GET/dashboardWorkspace stats
GET/updatesPoll new inbound

Errors

HTTPerrorMeaning
401unauthenticatedMissing or invalid API key.
403no_workspaceKey not linked to a workspace.
404conversation_not_foundNo such conversation in your workspace.
409no_whatsapp_number / session_closedNo connected number, or the 24h window is closed.
422to_required / body_requiredA required field is missing.
502send_failedWhatsApp rejected the message (see detail).

Ready to build?

Grab your API key and send your first WhatsApp message in minutes.

Get your API key →