Developer documentation
Send WhatsApp messages from a number you have linked in the dashboard. One send endpoint, an API key, and signed webhooks for delivery.
https://senderapi.digitodia.comQuickstart
Link a WhatsApp number in the dashboard, create an API key, then make your first call. The response comes back as soon as the send is accepted, not when it lands on the handset.
curl -X POST https://senderapi.digitodia.com/v1/messages \
-H "X-Api-Key: $WA_GATEWAY_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "+201234567890", "body": "Your appointment is tomorrow at 3pm." }'
# {
# "id": "b71d...",
# "to": "201234567890",
# "status": "queued",
# "createdAt": "2026-08-11T00:36:04.812Z"
# }Authentication
Every request to /v1 carries your API key in the X-Api-Key header. Keys are shown once at creation and stored only as a hash, so save it somewhere safe.
X-Api-Key: wag_live_qwlSv1TUSppFfx6vQ1TXoZyD1vSRrVHKeys carry scopes. A key scoped only to messages:send cannot read history, which lets you keep a send-only key on a less trusted service. You can also restrict a key to specific source IPs and give it its own per-minute rate limit.
| Scope | Grants |
|---|---|
messages:send | POST /v1/messages |
messages:read | GET /v1/messages/:id |
numbers:read | Reading linked number status |
Send a message
/v1/messagesQueues a message for delivery on the healthiest available number. Pass a body string, or the name of a template saved in the dashboard. The response comes back as soon as the send is accepted — subscribe to message.status if you need delivery confirmation.
| Field | Type | Notes |
|---|---|---|
to | string | Required. International format. |
body | string | Required unless template is set. Max 4000 characters. |
template | string | Name of one of your templates. Required unless body is set. |
variables | object | Values for template placeholders. |
Look up a message
/v1/messages/:idReturns the stored message, including the latest delivery status. Use this when you already have the id from the send response.
Idempotency
Send an Idempotency-Key header and the first response is replayed for 24 hours. A retry after a network timeout will not send a second message.
curl -X POST https://senderapi.digitodia.com/v1/messages \
-H "X-Api-Key: $WA_GATEWAY_KEY" \
-H "Idempotency-Key: booking-8c21f0c4" \
-H "Content-Type: application/json" \
-d '{ "to": "+201234567890", "body": "Your appointment is tomorrow at 3pm." }'Templates
Templates are message bodies with {{variable}} placeholders, managed in the dashboard. Reference one by name when sending and pass the values in variables.
{
"to": "+201234567890",
"template": "appointment-reminder",
"variables": { "name": "Alex", "date": "Tuesday", "time": "3pm" }
}
// template body:
// "Hi {{name}}, your appointment is on {{date}} at {{time}}."Webhooks
Register an endpoint in the dashboard to receive delivery updates, inbound replies and number health changes. Every request is signed so you can prove it came from us.
| Event | Fires when |
|---|---|
message.status | A message is sent, delivered, read or fails |
message.received | Somebody replies to your number |
number.disconnected | A linked number drops or is banned |
number.quarantined | Safety checks pull a number from the pool |
The signature covers the timestamp and the raw body, so a captured request cannot be replayed later. Reject anything older than a few minutes.
import { createHmac, timingSafeEqual } from "crypto";
export function verify(header, rawBody, secret) {
const parts = Object.fromEntries(
header.split(",").map((piece) => piece.split("=")),
);
// Reject stale timestamps so a captured request cannot be replayed.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}Delivery is retried six times with exponential backoff. A 4xx other than 429 is treated as permanent and not retried. Every attempt is recorded in the delivery log in your dashboard.
Errors
Errors share one shape, so a client can parse failures without special cases.
{
"statusCode": 403,
"error": "FORBIDDEN",
"message": "Monthly quota of 1000 messages reached for 2026-08",
"path": "/v1/messages",
"timestamp": "2026-08-11T00:31:04.812Z"
}| Status | Meaning |
|---|---|
400 | Validation failed. Check the message array. |
401 | Missing, unknown, revoked or expired API key. |
403 | Key lacks the scope, account is suspended, or quota is spent. |
404 | No matching message or resource. |
429 | Rate limit exceeded. Back off and retry. |
Rate limits
Limits exist on two axes. Your API key has a request ceiling, and each linked number has its own send pacing that protects it from looking automated.
A newly linked number ramps from a low daily allowance toward full volume over about two weeks. During warmup you may see a retryable error telling you the daily allowance is spent — that is the safety layer doing its job, not a fault. Link more numbers to raise your ceiling; traffic is pooled across them by health.
The same recipient cannot receive two messages within 60 seconds, and there is a daily cap per recipient. Both are deliberate: hammering one person is what produces the spam reports that get numbers banned.