Documentation

Send and receive WhatsApp messages over a REST API. Connect a number by scanning a QR code, then send from your own code. Interactive endpoint reference →

1. Getting started

Three steps to your first message:

  1. Sign in to the console with your email and password.
  2. Click + Add number, enter a name and the phone number, then Connect and scan the QR code with that phone.
  3. Expand that number with +, create its API key, copy it, and send a message from your code.

The number you enter is the only number that may link to that session. If a different number scans the QR, it is unlinked immediately and its credentials destroyed.

2. Authentication

Two separate credentials, deliberately. API keys are for machines and cannot sign into the console. Email and password is for people and cannot send messages. Revoking one never breaks the other.

Every API request carries a bearer key:

curl https://wa.adaminnovations.in/v1/sessions \
  -H "Authorization: Bearer wa_live_..."

Each number has its own key. Open a number on the console board, expand it with +, and create a key under API keys for this number. That key is scoped to that one number and is rejected on every other, so a leak is contained to a single line — and you can revoke one integration without touching the rest.

Keys are shown once at creation and stored hashed, so we cannot recover one for you. If you lose it, press Regenerate: that issues a replacement and revokes the old key in the same step, so update your integration before you do it.

3. Connecting a number

Create the session, then request a QR code:

curl -X POST https://wa.adaminnovations.in/v1/sessions \
  -H "Authorization: Bearer wa_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Support line", "phone_number": "96550001234"}'
curl -X POST https://wa.adaminnovations.in/v1/sessions/SESSION_ID/connect \
  -H "Authorization: Bearer wa_live_..."

The response contains a PNG data URI you can render directly, plus an 8-character pairing code you can type into the phone instead of scanning. Scan it in WhatsApp → Settings → Linked Devices.

QR codes rotate roughly every 20 seconds. Poll GET /v1/sessions/{id}/qr or subscribe to the session.qr webhook rather than showing one static image.

Statuses: provisionedqr_pendingconnecting connected. Sending works only in connected.

4. Sending messages

curl -X POST https://wa.adaminnovations.in/v1/messages \
  -H "Authorization: Bearer wa_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1234-confirmation" \
  -d '{
    "session_id": "...",
    "type": "text",
    "to": "96550009999",
    "text": "Your order has shipped."
  }'

Supported types: text, image, video, audio, document, sticker, location, contact, poll, interactive.

Idempotency-Key makes retries safe. Sending the same key twice returns the original result rather than delivering the message twice — worth using for anything triggered by a webhook or a job queue.

Media is sent by URL, which we fetch server-side:

{
  "type": "image",
  "to": "96550009999",
  "media": { "url": "https://example.com/receipt.png" },
  "caption": "Your receipt"
}

5. Receiving messages

Register an endpoint and we POST every inbound message and status change to it:

curl -X POST https://wa.adaminnovations.in/v1/webhooks \
  -H "Authorization: Bearer wa_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.com/whatsapp"}'

The response includes a secret, shown once. You need it to verify that deliveries genuinely came from us.

Every delivery carries X-WA-Signature: t=<unix>,v1=<hmac-sha256> over ${t}.${rawBody}:

import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))

  // Reject stale deliveries — the timestamp is signed, so it cannot be moved.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false

  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')

  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

Verify against the raw request body, not re-serialised JSON. Key order and whitespace are not preserved through a parse, and any difference breaks the signature.

Failed deliveries retry at 1m, 5m, 30m, 2h and 6h, then dead-letter. Client errors other than 429 are not retried. Inspect attempts at GET /v1/webhooks/{id}/deliveries.

Events: message.received, message.status, session.qr, session.connected, session.disconnected, session.logged_out.

6. Buttons and lists

Send buttons using the same shape Meta’s official API accepts:

{
  "type": "interactive",
  "to": "96550009999",
  "interactive": {
    "type": "button",
    "body": "How can we help?",
    "action": {
      "buttons": [
        { "id": "sales",   "title": "Sales" },
        { "id": "support", "title": "Support" }
      ]
    }
  }
}

On QR-connected numbers, buttons do not render. WhatsApp removed interactive message support from the protocol these numbers use. Nothing can restore it — not this API, not any other unofficial provider.

Instead, the message above is delivered as a numbered menu:

How can we help?

1. Sales
2. Support

Reply with a number (1-2).

When the recipient replies 1, your webhook receives:

{
  "interactive": {
    "button_reply": { "id": "sales", "title": "Sales" }
  }
}

That is exactly what the official Cloud API sends. Your code reads button_reply.id either way, so moving a number to the official API later changes nothing except that the buttons become tappable.

Replies match on 1, 1., 1) or the option title, case-insensitively. Out-of-range numbers and ordinary conversation are ignored, and each menu is consumed once so a repeated reply cannot double-fire.

7. Rate limits

Two independent limits:

LimitDefaultWhy
Per API key600 req/minOrdinary API protection
Per number20 msg/minBan avoidance

The per-number limit also enforces a minimum gap between sends with random jitter. Machine-regular timing is a strong signal to WhatsApp’s abuse detection, which is why sends are not evenly spaced. This cannot be disabled. Exceeding it returns 429 send_rate_limit_exceeded.

8. Errors

Every error uses the same shape:

{
  "error": {
    "code": "session_not_connected",
    "type": "session",
    "message": "Session is not connected. Connect it and scan the QR code first.",
    "request_id": "req_5cb3cf5245794d5888658936"
  }
}

Branch on code, never on the message — messages may be reworded. Quote request_id when reporting a problem.

The complete list is at /error-codes. The ones you will actually meet:

  • session_not_connected — the number is not linked. Connect and scan.
  • session_logged_out — unlinked from the phone. Needs a fresh QR scan.
  • session_wrong_number — a different number scanned the QR. Credentials were wiped.
  • send_rate_limit_exceeded — slow down. Protects the number.
  • recipient_not_on_whatsapp — that number has no WhatsApp account.

9. Limitations

Worth knowing before you build:

  • Buttons and lists do not render on QR-connected numbers. They arrive as numbered menus — see section 6.
  • Accounts can be banned. This uses the unofficial WhatsApp protocol. Rate limits reduce the risk but cannot remove it. Do not use it for cold bulk outreach, and warm new numbers up gradually.
  • No message templates, Flows, or the green tick. Those are official Cloud API features.
  • The phone must link once, but need not stay online afterwards.

If you need guaranteed delivery, working buttons, or a support SLA, the official WhatsApp Cloud API is the right choice. This API supports it as an alternative engine — the request and webhook shapes are identical, so switching does not change your integration.