API v1

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.

base url
https://senderapi.digitodia.com

Quickstart

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.

bash
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.

http
X-Api-Key: wag_live_qwlSv1TUSppFfx6vQ1TXoZyD1vSRrVH

Keys 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.

ScopeGrants
messages:sendPOST /v1/messages
messages:readGET /v1/messages/:id
numbers:readReading linked number status

Send a message

POST/v1/messages

Queues 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.

FieldTypeNotes
tostringRequired. International format.
bodystringRequired unless template is set. Max 4000 characters.
templatestringName of one of your templates. Required unless body is set.
variablesobjectValues for template placeholders.

Look up a message

GET/v1/messages/:id

Returns 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.

bash
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.

json
{
  "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.

EventFires when
message.statusA message is sent, delivered, read or fails
message.receivedSomebody replies to your number
number.disconnectedA linked number drops or is banned
number.quarantinedSafety 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.

javascript
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.

json
{
  "statusCode": 403,
  "error": "FORBIDDEN",
  "message": "Monthly quota of 1000 messages reached for 2026-08",
  "path": "/v1/messages",
  "timestamp": "2026-08-11T00:31:04.812Z"
}
StatusMeaning
400Validation failed. Check the message array.
401Missing, unknown, revoked or expired API key.
403Key lacks the scope, account is suspended, or quota is spent.
404No matching message or resource.
429Rate 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.