API documentation

Every endpoint lives under https://wasel.tech and authenticates with the x-api-key header. A key belongs to a single project and is copied from that project's Keys page in the Wasel dashboard.

⚠️ Protecting the key

The key is for your server only. Putting it in a mobile app or a web page exposes it to anyone who unpacks the app, who could then send on the project's account and at its expense. If it leaks, generate a new key from the dashboard and the old one stops working immediately.

Which service do you need?

Start from the row that describes your case — it tells you which endpoint to call and what each message costs. Price follows the template category at Meta, and the gap between categories reaches four times.

Every message goes out from the project's own WhatsApp number — whether you trigger it from the dashboard or from the API. The dashboard is where the button is, not where the message comes from, and the sender your customer sees is your number. Verification codes are the only exception: a project with no number of its own sends them from Wasel's shared number.

Integration steps

From nothing to a delivered message. Steps marked “Project owner” are not done in code — if you are blocked on one, it is not your task.

  1. 1Project ownerLink a WhatsApp number to the project in the Wasel dashboard — required for broadcasts and notifications. Verification codes are the exception: a project with no number sends them from Wasel's shared number. Everything else returns 409.
  2. 2Project ownerCreate the templates you intend to send — booking confirmation, reminder, verification code — and wait for Meta to approve them. Review takes minutes to a day.
  3. 3Project ownerCopy the secret key from the Keys page and hand it to the developer over a private channel.
  4. 4DeveloperPut the key in a server-side environment variable. Never in the app bundle, never in the repository.
  5. 5DeveloperMake your first /api/send call to your own number — copy the example below and change the phone and template name.
  6. 6DeveloperStore the returned wamid against your own record. It is the only handle for tracking that message later.
  7. 7DeveloperPoll /api/status for delivery. Never infer delivery from a successful call.
  8. 8DeveloperHandle 409 opted_out in your code: that customer stopped messages. Do not retry — remove them from your list.
  9. 9DeveloperDo not blindly retry a request that failed on the network: we do not deduplicate, so the customer receives two messages.

Before you start

POST/api/send

Send a message using an approved template — booking confirmation, reminder, status update.

curl -X POST https://wasel.tech/api/send \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "96550001234",
    "template": "booking_confirmed",
    "variables": ["Mohammed", "12 Oct 7:30 PM"]
  }'

Response

{ "ok": true, "phone": "96550001234",
  "wamid": "wamid.HBgLOTY1NTAwMDEyMzQVAgAR...",
  "template": "booking_confirmed", "language": "ar" }

variables are positional — {{1}}, {{2}}… as they appear in the template. The count must match the template exactly or Meta rejects the message.
language is optional — if omitted we read the template's language from our records.
media is optional, for templates with a media header: "media": { "kind": "image", "link": "https://…" }

POST/api/otp

Send a verification code. The code is generated by us and returned in the response, and we never store it — your server keeps it and compares it.

curl -X POST https://wasel.tech/api/otp \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "96550001234" }'

POST/api/broadcast

Broadcast a marketing template to your contact list — one call handles the audience, the batching, and the campaign log. Opted-out contacts are excluded automatically, and groups narrows the audience to specific tags (omit it for the whole list).

curl -X POST https://wasel.tech/api/broadcast \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Autumn offers",
    "template": "autumn_offer",
    "language": "ar",
    "groups": ["vip"],
    "variables": [{ "from": "name", "fallback": "عميلنا" }]
  }'

Response — 202, before the broadcast finishes

{ "accepted": true, "status": "queued",
  "campaignId": "7f3a1c08-5e4b-4d19-9a62-0c1b8f2e7d44",
  "template": "autumn_offer", "language": "ar", "total": 1240,
  "poll": "/api/broadcast?id=7f3a1c08-5e4b-4d19-9a62-0c1b8f2e7d44" }

variables are in {{1}}, {{2}}… order, and each one resolves per contact: {"from":"name"} · {"from":"phone"} · {"from":"attr","key":"city"} · {"from":"text","value":"…"}. A fallback is required on each — Meta rejects an empty parameter with #132000, which drops that one customer's message.
🔴 Do not retry if it is slow or your connection drops: a retry creates a second campaign, so every customer gets two messages and you are billed twice. Poll the endpoint below instead.
⚠️ The template must be APPROVED, and a MARKETING category is billed per recipient.
⚠️ If your number's quality rating drops at Meta the broadcast stops itself to protect the number; the reason lands in the error field.

GET/api/broadcast

Broadcast progress: how many were sent, delivered, and failed — and why it stopped if it did.

curl "https://wasel.tech/api/broadcast?id=7f3a1c08-5e4b-4d19-9a62-0c1b8f2e7d44" \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY"
{ "status": "done", "total": 1240,
  "sent": 1238, "delivered": 1201, "read": 884, "failed": 2,
  "error": null, "finished_at": "2026-10-01T12:04:19.223Z" }

GET/api/status

The status of one message by its id, or the last twenty messages for a number.

curl "https://wasel.tech/api/status?phone=96550001234" \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY"

Statuses

queued — waitingsent — sentdelivered — deliveredread — readfailed — failed

Email channel

Same key, same contacts, same project — only the destination changes. Every endpoint here mirrors one on the WhatsApp side, so what you learned there works here. No templates in email: the text is free-form and sent as-is, because Meta's template rule does not exist for email.

POST/api/mail/otp

A verification code by email. Wasel generates it, sends it and returns it to you — the comparison, the expiry and the attempt count stay on your server, and the code is never stored here.

curl -X POST https://wasel.tech/api/mail/otp \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "customer@example.com" }'

Response

{ "ok": true, "id": "01a10681-8bcb-7e2a-8924-929c96f289e8",
  "code": "280892", "to": "customer@example.com",
  "from": "فرصة <Info@wasel.tech>" }

POST/api/mail/send

One transactional email: booking confirmation, receipt, appointment reminder. Pass lines and we wrap them, or html for your own design. It is delivered even to an unsubscribed recipient — opting out stops marketing only.

curl -X POST https://wasel.tech/api/mail/send \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "customer@example.com",
    "subject": "تأكيد حجزك",
    "lines": ["تم تأكيد حجزك", "الموعد: 12 أكتوبر 7:30 م"]
  }'

POST/api/mail/broadcast

A campaign to an audience of your contacts. Returns 202 at once and sending continues in the background, so poll progress with GET by id. An unsubscribe link is added per recipient automatically — and whoever clicks it does not click "spam".

curl -X POST https://wasel.tech/api/mail/broadcast \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Autumn offers",
    "subject": "عروض الخريف",
    "html": "<h1>عروضنا هذا الأسبوع</h1>",
    "groups": ["vip"]
  }'

Response — 202, before the broadcast finishes

{ "ok": true, "running": true, "total": 1240,
  "campaignId": "7f3a1c08-5e4b-4d19-9a62-0c1b8f2e7d44" }

GET/api/mail/status

Accepted is not delivered. ⚠️ There is no email webhook yet, so the status stays sent and delivered never arrives — know that before you wait for it.

curl "https://wasel.tech/api/mail/status?email=customer@example.com" \
  -H "x-api-key: wsl_YOUR_PROJECT_KEY"

SMTP gateway - when your code speaks SMTP

Your code already sends over SMTP and you would rather not touch it? Point it at the Wasel gateway: change three settings, leave every call site alone. The message travels through Wasel exactly as an API call would - it is logged, measured, and visible in the dashboard.

SMTP_HOST=smtp.wasel.tech
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USERNAME=your-project
SMTP_PASSWORD=wsl_YOUR_PROJECT_KEY

Node.js (nodemailer)

const transport = nodemailer.createTransport({
  host: 'smtp.wasel.tech',
  port: 465,
  secure: true,
  auth: { user: 'your-project', pass: 'wsl_YOUR_PROJECT_KEY' },
})

await transport.sendMail({
  from: 'Your Brand <you@your-domain.com>',
  to: 'customer@example.com',
  subject: 'Your booking is confirmed',
  html: '<h1>…your design…</h1>',
})

⚠️ Port 465, implicit TLS (secure: true). Unencrypted connections are refused. The username is only for your own logs; the password is your project API key. Attachments are not supported yet - text and HTML only.

Error codes