SSendTo · Developer docs

Connect your systems to official WhatsApp messaging

A simple REST API with one key and signed events delivered in real time — start in a sandbox whose messages reach no one.

Base URL
https://whatsapp.nawa-tech.com/api/v1
Authentication
Every request carries the x-api-key header with your key from the portal — rotate it under “API & webhooks”.
Environment
Every response carries environment: sandbox (simulated — reaches no one) or live.

Start in three steps

  1. Create a free sandbox account in the portal and save your API key and signing secret. Open the portal
  2. Send your first message (simulated in the sandbox).
  3. Receive events on your URL and verify their signature — then request live sending.
curl -X POST https://whatsapp.nawa-tech.com/api/v1/messages/text \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"clientMessageId":"order-10045","to":"9665XXXXXXXX","templateName":"order_ready","templateLang":"ar","templateParams":["Ahmed","#10045"]}'
const r = await fetch('https://whatsapp.nawa-tech.com/api/v1/messages/text', {
  method: 'POST',
  headers: { 'x-api-key': process.env.SENDTO_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ clientMessageId: 'order-10045', to: '9665XXXXXXXX',
    templateName: 'order_ready', templateLang: 'ar', templateParams: ['Ahmed', '#10045'] }),
});
const out = await r.json(); // { success, environment, data: { status, messageId, ... } }

Per-minute request limit per account (429 with Retry-After) · batches up to 1000 text and 50 media messages · media up to 5MB · requests up to 20MB.

Getting started & status

GET/api/v1/healthNo key

Health

No key — is the service up.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/health
Response
{
  "success": true,
  "service": "sendto-cloudapi-mini",
  "now": "2026-09-26T10:00:00.000Z"
}
GET/api/v1/readinessx-api-key

Readiness

Whether the invitation template is approved and your numbers are ready.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/readiness \
  -H "x-api-key: sk_..."
GET/api/v1/limitsx-api-key

Today's capacity

Your numbers' capacity today and the reset time.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/limits \
  -H "x-api-key: sk_..."
GET/api/v1/sender/healthx-api-key

Sender health

overall, readyForSending, aggregateCapacity, qualityRating and your account caps (fromApplication).

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/sender/health \
  -H "x-api-key: sk_..."
GET/api/v1/templatesx-api-key

Templates

Your templates: status, category, variable count, header, buttons, usable.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/templates \
  -H "x-api-key: sk_..."

Sending

POST/api/v1/messages/textx-api-key

Send one message

200 with the result · 202 when scheduled or deferred by a limit (reason sent as message.deferred) · a repeated clientMessageId returns the original result (duplicate: true).

FieldTypeRequiredDescription
clientMessageIdstringyesYour message ID (8–128 chars) — used for matching and 48h de-duplication
tostringyesRecipient number in international digits (8–20)
templateNamestringApproved template — required outside the 24h conversation window
templateLangstringTemplate language (ar, en …)
templateParamsstring[]Template variables in order
messagestringFree text — only inside the conversation window
kind"notification" | "invitation"Default notification · invitations need eventId or expiresAt
eventIdstringA registered event (POST /events)
expiresAtISO-8601Invitation expiry
scheduledForISO-8601Not sent before this time (up to 1 year)
deadlineISO-8601After this it expires unsent and is refunded
mediaBase64 / mediaIdstringjpeg/png image or PDF up to 5MB (or a previous upload ID) with type: image|document
sentFrom · fromPolicystring · "prefer"|"strict"|"auto"Preferred sending number and its policy
metaobject ≤ 4KBEchoed back verbatim in every event
buttonUrlhttps URLYour link for the template button (wallet card, booking page) — we store it, the guest opens our link which redirects to it, and we count opens · only for templates whose button is on our domain
buttonPayloadsstring[] (1–10 · كلٌّ 1–128)Quick-reply button payloads in template order — echoed back verbatim in message.received (raw accounts; default: clientMessageId)
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/messages/text \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"clientMessageId":"order-10045","to":"9665XXXXXXXX","templateName":"order_ready","templateLang":"ar","templateParams":["Ahmed","#10045"]}'
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "clientMessageId": "order-10045",
    "messageId": "wamid.HBgM…",
    "status": "sent",
    "sentFrom": "9665XXXXXXXX",
    "fallbackUsed": false,
    "sentAt": "2026-09-26T10:00:01.000Z",
    "duplicate": false
  }
}
Error codes
POST/api/v1/messages/otpx-api-key

One-time passcode

Sent with a Meta authentication template (fixed Meta text) and a copy-code button — top priority · not blocked by marketing opt-out · if not sent within its TTL (10 minutes by default) it expires instead of arriving late · the code is erased from our records afterwards.

FieldTypeRequiredDescription
tostringyesInternational digits
codestring (1–15)yesThe code — no spaces or links (Meta limit)
clientMessageId · ttlSeconds · metastring · integer (60–3600) · objectOptional — ttlSeconds defaults to 600
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/messages/otp \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"to":"9665XXXXXXXX","code":"482913"}'
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "clientMessageId": "otp_…",
    "messageId": "wamid.HBgM…",
    "status": "sent",
    "sentFrom": "9665XXXXXXXX",
    "duplicate": false
  }
}
Error codes
POST/api/v1/messages/bulkx-api-key

Bulk batch

Up to 1000 text and 50 media messages · 202 with five lists: accepted, rejected (per-message code), duplicates (48h), held, atRisk — plus a conservative estimatedCompletionAt. Message fields on the envelope return FIELD_NOT_ALLOWED_IN_BULK.

FieldTypeRequiredDescription
messagesMessage[]yesEach item has the fields of "Send one message"
batchNamestringBatch name
eventIdstringBatch event
fromPolicystringPolicy for the whole batch
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/messages/bulk \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"batchName":"reminders-sep","messages":[{"clientMessageId":"rem-0001","to":"9665XXXXXXXX","templateName":"order_ready","templateLang":"ar","templateParams":["Sara","#1"]}]}'
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "batchId": "b1f…",
    "batchName": "reminders-sep",
    "eventId": null,
    "total": 1,
    "accepted": [
      {
        "clientMessageId": "rem-0001"
      }
    ],
    "held": [],
    "atRisk": [],
    "rejected": [],
    "duplicates": [],
    "estimatedCompletionAt": "2026-09-26T10:05:00.000Z"
  }
}
Error codes
POST/api/v1/messages/richx-api-key

Rich message

Header image + text + up to 3 reply buttons (or one URL button). Inside the conversation window, or with an approved template outside it. Call buttons and copyCode are not available (400 unsupported).

FieldTypeRequiredDescription
clientMessageIdstringyesYour message ID (8–128 chars) — used for matching and 48h de-duplication
tostringyesRecipient number in international digits (8–20)
messagestring ≤ 1024yesBody text
buttons{id,text}[] | [{text,url}]yesButton text ≤ 20 chars
headerImagebase64 | data:Header image
title · footerstring ≤ 60Title and footer
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/messages/rich \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"clientMessageId":"card-2201","to":"9665XXXXXXXX","message":"تذكرتك جاهزة","buttons":[{"id":"confirm","text":"تأكيد"},{"id":"decline","text":"اعتذار"}]}'
Error codes
POST/api/v1/messages/uploadx-api-key

Upload media

Upload once and reuse the mediaId across many messages (content is de-duplicated).

FieldTypeRequiredDescription
mediaBase64stringyesjpeg/png/pdf up to 5MB
mimetype · filenamestringOptional
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/messages/upload \
  -H "x-api-key: sk_..."
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "mediaId": "ast_…",
    "mimetype": "image/png",
    "bytes": 48211
  }
}
Error codes
POST/api/v1/feedback/sendx-api-key

Send a feedback form

A feedback form you built in the portal, approved by us and published at Meta — the guest gets a button that opens it inside WhatsApp. **Once per number** (ALREADY_SENT) · **only the first answer counts** and reaches you as feedback.submitted with the question texts · sandbox accounts send a ready form in simulation.

FieldTypeRequiredDescription
formIdstringyesForm id (any version — the latest published one is sent)
tostringyesGuest number in international digits
clientMessageIdstringYour id — repeating it returns the original result (duplicate:true)
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/feedback/send \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"formId":"6f1c…","to":"9665XXXXXXXX","clientMessageId":"visit-10045-rating"}'
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "sendId": "a3b…",
    "jobId": "9e2…",
    "formId": "6f1c…",
    "version": 1,
    "to": "9665XXXXXXXX",
    "status": "queued",
    "duplicate": false
  }
}
Error codes

Tracking & cancelling

GET/api/v1/messages/:cidx-api-key

Message status

timeline, invitation, attempts, error, meta — by your clientMessageId.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/messages/{cid} \
  -H "x-api-key: sk_..."
Error codes
POST/api/v1/linksx-api-key

Create a redirect link

A link on our domain that redirects to yours and counts opens — https and a public host.

FieldTypeRequiredDescription
urlhttps URLyesDestination
eventId · clientMessageId · expiresAtstring · ISO-8601Optional
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/links \
  -H "x-api-key: sk_..."
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "code": "Ab3kP9xQr2",
    "shortUrl": "https://whatsapp.nawa-tech.com/l/Ab3kP9xQr2",
    "url": "https://…",
    "clicks": 0,
    "active": true
  }
}
Error codes
GET/api/v1/links/:codex-api-key

Link opens

clicks, firstClickAt, lastClickAt, active.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/links/{code} \
  -H "x-api-key: sk_..."
Error codes
POST/api/v1/batches/:batchId/cancelx-api-key

Cancel a batch

Cancels whatever has not been sent (refunded) — messages mid-send are counted in inFlight.

Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/batches/{batchId}/cancel \
  -H "x-api-key: sk_..."

Events & replies

POST/api/v1/eventsx-api-key

Register an event

Invitation deadlines, reply texts and the callback for its events. 201 new · 200 duplicate · 409 EVENT_LOCKED when changing the date after an invitation went out (unless confirmLockedChange: true).

FieldTypeRequiredDescription
eventIdstring (4–64)yesYour event ID
eventDateISO-8601yesEvent date
callbackUrlhttps URLyesWebhook URL for this event
texts.invalidReply · texts.nudge · texts.expiredstringyesTexts sent to guests
defaultsobjectInvitation deadlines and undeliveredTimeoutMinutes
defaults.sentFromstringDefault sending number for the event (one of yours) — each new guest starts on it, guests already messaged keep their number · null ⇒ automatic · editable after lock
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/events \
  -H "x-api-key: sk_..."
Error codes
GET/api/v1/events/:eventIdx-api-key

Get an event

Its settings + sentInvitations.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/events/{eventId} \
  -H "x-api-key: sk_..."
Error codes

Recipients & opt-outs

POST/api/v1/numbers/checkx-api-key

Check numbers

Up to 500 — true if we delivered to it before · null otherwise (Meta offers no pre-check — never false).

FieldTypeRequiredDescription
numbersstring[]yesNumbers
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/numbers/check \
  -H "x-api-key: sk_..."
Error codes
POST/api/v1/optouts/checkx-api-key

Check opt-outs

Who asked to stop your messages.

FieldTypeRequiredDescription
numbersstring[]yesNumbers
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/optouts/check \
  -H "x-api-key: sk_..."
DELETE/api/v1/optouts/:phoneNumberx-api-key

Remove an opt-out

Only at the number owner's request.

Request
curl -X DELETE https://whatsapp.nawa-tech.com/api/v1/optouts/{phoneNumber} \
  -H "x-api-key: sk_..."

Webhooks we send you

We POST every event as JSON to your URL (or the event callbackUrl). Any 2xx within 10 seconds is success — otherwise we retry: immediately, then after 10s, 1m, 5m, 30m, 2h (six attempts), then it appears in GET /api/v1/webhooks/failed.

Headers
X-Timestamp: 1790000000
X-Signature: sha256=hex(HMAC_SHA256(secret, "<X-Timestamp>.<rawBody>"))
X-Webhook-Id: whk_…   X-Delivery-Attempt: 1
Verify the signature — Node.js
const crypto = require('crypto');
// rawBody: the exact request body string · ts: X-Timestamp · sig: X-Signature
function verify(rawBody, ts, sig, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Verify the signature — PHP
<?php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_TIMESTAMP'];
$sig = $_SERVER['HTTP_X_SIGNATURE'];
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
$ok = abs(time() - (int)$ts) < 300 && hash_equals($expected, $sig);
Events
message.statussent, delivered, read, failed (with code, explanation, action), expired (neverDelivered)
message.deferredDeferred once per reason: send window, number limits, INVOICE_OVERDUE, SENDING_PAUSED …
message.receivedraw accounts: every reply verbatim + repliedTo attribution (button_payload, quoted_message, inferred last_outbound, none) with clientMessageId and meta, lastOutbound, lastOutboundByKind, windowExpiresAt — interpretation is yours
guest.confirmed · guest.declined · guest.replied_invalid · guest.unresolved · guest.replied_late · guest.messageGuest replies to invitations (answer, matchConfidence, slotMatch)
guest.opted_out · guest.opted_inOpt-out and opt-in
batch.progress · batch.completedBatch progress (every 5% or 60s) and completion once
sender.healthNumber status or quality change at Meta
feedback.submittedA guest rating — first one only: answers with question texts (rating 1–5, single, multi, comment), formId, version, to, clientMessageId, meta
GET/api/v1/webhooks/failedx-api-key

Failed webhooks

Events whose six attempts were exhausted — with their payload.

FieldTypeRequiredDescription
limit · sincenumber · ISO-8601Optional
Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/webhooks/failed \
  -H "x-api-key: sk_..."
POST/api/v1/webhooks/failed/:webhookId/retryx-api-key

Retry a webhook

Re-queued with six new attempts and the same ID.

Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/webhooks/failed/{webhookId}/retry \
  -H "x-api-key: sk_..."
GET/api/v1/media/inbound/:wamidNo key

Guest media file

Delivered ready-made as mediaUrl in guest.* events — signed, so it works without a key (even in <img>) · valid 7 days from receipt (Meta retention) then 410 · we do not store the file: download it within that window.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/media/inbound/{wamid}
Error codes

Balance & invoices

A unique message is the first one to a number in your subscription; a second one to the same number is a follow-up. Anything not delivered is refunded. An invoice is valid until its due time; after that new messages get 402 INVOICE_OVERDUE and the queue waits — everything resumes by itself once paid.

GET/api/v1/balancex-api-key

Balance

Package, billing type (PACKAGE, MONTHLY, USAGE), quota (unique/follow-up), pay-as-you-go money, suspended and open invoices — or partner capacity.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/balance \
  -H "x-api-key: sk_..."
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "billingMode": "ENFORCED",
    "suspended": false,
    "invoices": [],
    "subscription": {
      "package": "الأعمال",
      "billingType": "PACKAGE",
      "state": "ACTIVE",
      "unique": {
        "quota": 5000,
        "used": 1200,
        "remaining": 3800
      },
      "followup": {
        "quota": 1000,
        "used": 40,
        "remaining": 960
      }
    }
  }
}
GET/api/v1/invoicesx-api-key

Invoices

Each invoice is valid until dueAt; after that sending pauses (INVOICE_OVERDUE) and resumes by itself once paid in the portal.

FieldTypeRequiredDescription
statusOPEN|OVERDUE|PAID|VOIDOptional
Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/invoices \
  -H "x-api-key: sk_..."
Response
{
  "success": true,
  "environment": "live",
  "data": [
    {
      "number": "INV-2026-000001",
      "kind": "USAGE",
      "status": "OPEN",
      "subtotal": "500.00",
      "vatPct": 0,
      "vatAmount": "0.00",
      "total": "500.00",
      "currency": "SAR",
      "issuedAt": "2026-09-26T10:00:00.000Z",
      "dueAt": "2026-09-27T10:00:00.000Z"
    }
  ]
}
GET/api/v1/invoices/:numberx-api-key

Get an invoice

Its lines, amounts and status.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/invoices/{number} \
  -H "x-api-key: sk_..."
Error codes

Partner API

GET/api/v1/partner/accountx-api-key of a partner account

Partner account

Capacity (total, used, remaining), balance owed, credit limit, available credit, maximum top-up now, expiry, pending transfers.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/partner/account \
  -H "x-api-key: sk_..."
Response
{
  "success": true,
  "environment": "live",
  "data": {
    "currency": "SAR",
    "ratePerMessage": "0.3700",
    "capacity": {
      "total": 20000,
      "used": 5120,
      "remaining": 14880,
      "pct": 26
    },
    "debt": "4000.00",
    "creditLimit": "10000.00",
    "creditAvailable": "6000.00",
    "maxTopupMessages": 16216,
    "expiresAt": null,
    "expired": false,
    "pendingTransfers": []
  }
}
Error codes
POST/api/v1/partner/topupsx-api-key of a partner account

Top up capacity

Activated instantly while the balance after it stays within the credit limit — otherwise 402 CREDIT_LIMIT_EXCEEDED with the numbers and the maximum you can request now.

FieldTypeRequiredDescription
messagesintegeryesNumber of messages
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/partner/topups \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"messages":20000}'
Error codes
POST/api/v1/partner/transfersx-api-key of a partner account

Report a transfer

Does not change your balance until we confirm receipt — the amount actually received is applied.

FieldTypeRequiredDescription
amountnumberyesAmount
referencestringyesTransfer reference — unique
transferDate · notedate · stringOptional
Request
curl -X POST https://whatsapp.nawa-tech.com/api/v1/partner/transfers \
  -H "x-api-key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{"amount":5000,"reference":"TRX-88213","transferDate":"2026-09-26"}'
Error codes
GET/api/v1/partner/ledgerx-api-key of a partner account

Ledger

Every entry with the balance after it: TOPUP, TRANSFER_REPORTED, TRANSFER_CONFIRMED, TRANSFER_REJECTED, ADJUSTMENT.

Request
curl -X GET https://whatsapp.nawa-tech.com/api/v1/partner/ledger \
  -H "x-api-key: sk_..."

All error codes

Every error has the shape { success: false, error: { code, message, … } } — the message names the cause.

CodeHTTPDescription
UNAUTHORIZED401x-api-key missing
INVALID_API_KEY401Unknown key
APP_INACTIVE403Account disabled
RATE_LIMIT_EXCEEDED429Per-minute request limit — see Retry-After (seconds)
VALIDATION_ERROR400Missing or invalid field — the message names it
INVALID_PHONE400Number is not 8–20 digits
FORM_NOT_FOUND404No feedback form with this id in your account
FORM_NOT_SENDABLE409The form is not published yet (sandbox: not approved yet)
ALREADY_SENT409This form was already sent to this number — rated once (previousSendAt)
DUPLICATE_CLIENT_MESSAGE_ID409clientMessageId already used for another message in this account
RECIPIENT_NOT_ALLOWLISTED403Recipient not on the allowed list
BATCH_TOO_LARGE400Batch above the limit
FIELD_NOT_ALLOWED_IN_BULK400Message field on the batch envelope
INVALID_FROM_POLICY400fromPolicy: prefer, strict, auto
SERVICE_WINDOW_CLOSED400Free text outside the conversation window — use a template
MEDIA_TOO_LARGE400Media above 5MB
INVALID_MEDIA400Not jpeg/png/pdf
MESSAGE_NOT_FOUND404No message with this ID
TEMPLATE_REQUIRED400templateName is required for invitations on your account — no default template
INVALID_OTP400Code must be 1–15 characters, no spaces or links
INVALID_URL400Invalid URL — https and a public host
BUTTON_URL_UNSUPPORTED400Template button is not on our domain — buttonUrl not accepted
INVALID_BUTTON_PAYLOAD400buttonPayloads: 1–10 strings, each 1–128 chars — never silently truncated
INVALID_SENDER400Not one of your numbers
INVALID_SIGNATURE403Invalid link signature
MEDIA_EXPIRED410Media expired at Meta (7 days)
MEDIA_NOT_FOUND404No media for this message
META_ERROR502Download from Meta failed — its text is in the message
EVENT_NOT_FOUND404No event with this ID
EVENT_LOCKED409Date change after invitations went out — confirm with confirmLockedChange
NOT_FOUND404Not found
NO_ACTIVE_SUBSCRIPTION402No active subscription
SUBSCRIPTION_EXPIRED402Subscription expired
QUOTA_EXHAUSTED402Unique messages used up
FOLLOWUP_QUOTA_EXHAUSTED402A second message to the same number needs follow-up credit
INVOICE_OVERDUE402An invoice is overdue — sending is paused until paid (includes its number and amount)
USAGE_CREDIT_LIMIT402Pay as you go: balance reached its limit
PARTNER_CAPACITY_EXHAUSTED402Partner capacity used up — request a top-up
PARTNER_CAPACITY_EXPIRED402Partner agreement ended
CREDIT_LIMIT_EXCEEDED402Top-up exceeds the credit limit
INVALID_QUANTITY400Messages from 1 to 10,000,000
REFERENCE_REQUIRED400Transfer reference required
DUPLICATE_REFERENCE409Reference already reported
INVALID_AMOUNT400Invalid amount
NOT_A_PARTNER403Partner accounts only

SendTo — a Nawa Tech service