API Reference

Send WhatsApp messages from your own backend over a simple REST API. All requests are JSON, over HTTPS, authenticated with an API key. Base URL:

https://api.sendwamessage.in

Get an API key from your dashboard under Developers. Every send is billed per message from your credit balance; a failed send is refunded automatically.

Authentication

Pass your API key in the x-api-key header on every request. Keys are shown once at creation — store them securely and never expose them in client-side code.

bash
curl https://api.sendwamessage.in/api/v1/auth/me \
  -H "x-api-key: wa_live_xxxxxxxx"

Auth for a custom app

Building your own frontend (to link numbers, take payments, etc.)? Authenticate the user with email + password and use the returned accessToken as a bearer token. API keys are for server-to-server sending only — user/tenant actions need a user session.

POST/api/v1/auth/login
bash
curl -X POST https://api.sendwamessage.in/api/v1/auth/login \
  -H "content-type: application/json" \
  -d '{ "email": "user@company.com", "password": "…" }'

Response (tokens are also set as cookies):

json
{ "data": {
    "user": { "id": "…", "email": "…", "roles": ["admin"] },
    "tenant": { "id": "…", "name": "…" },
    "accessToken": "eyJ…",   // send as: Authorization: Bearer <accessToken>
    "refreshToken": "eyJ…"    // POST /auth/refresh to rotate (15-min access)
} }
bash
curl https://api.sendwamessage.in/api/v1/sessions \
  -H "Authorization: Bearer eyJ…"
A browser app on a different domain must have its origin added to the API’s CORS allowlist — email support@sendwamessage.in with your app’s URL and we’ll enable it. Use the bearer token (not cookies) cross-origin.

Send a text message

POST/api/v1/messages/send

to accepts any format — bare national numbers get your workspace country code. Provide a body, a mediaId, or both.

bash
curl -X POST https://api.sendwamessage.in/api/v1/messages/send \
  -H "x-api-key: wa_live_xxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "to": "919876543210",
    "body": "Your order #4821 has shipped 🚚"
  }'

Response

json
{
  "success": true,
  "data": {
    "id": "019f8e0f-...",
    "status": "SENT",
    "toNumber": "919876543210",
    "waMessageId": "wamid.HBg...",
    "sentAt": "2026-07-23T09:24:11.000Z"
  }
}
Send an Idempotency-Key header (or idempotencyKey field) so a retried request never delivers twice.

Send media (image / PDF / video)

Two steps: upload the file to get a `mediaId`, then send it (optionally with a caption).

POST/api/v1/media
bash
curl -X POST https://api.sendwamessage.in/api/v1/media \
  -H "x-api-key: wa_live_xxxxxxxx" \
  -F "file=@invoice.pdf"
# → { "data": { "id": "019f8df1-...", "type": "DOCUMENT" } }
POST/api/v1/messages/send
bash
curl -X POST https://api.sendwamessage.in/api/v1/messages/send \
  -H "x-api-key: wa_live_xxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "to": "919876543210",
    "mediaId": "019f8df1-...",
    "body": "Invoice attached"
  }'

Bulk send (up to 100 recipients)

POST/api/v1/messages/bulk

Send the same message to up to 100 numbers. Returns immediately with a job id; messages are paced out in the background (default one every 4s) to protect the number. Invalid numbers are dropped and reported.

bash
curl -X POST https://api.sendwamessage.in/api/v1/messages/bulk \
  -H "x-api-key: wa_live_xxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "to": ["919876543210", "919812345678"],
    "body": "Flat 20% off this weekend!",
    "throttleMs": 4000
  }'
# → 202 { "data": { "jobId": "019f...", "total": 2, "invalid": [] } }
GET/api/v1/messages/bulk/:id

Poll for progress:

json
{ "data": { "status": "RUNNING", "totalRecipients": 2,
           "sentCount": 1, "failedCount": 0, "processedCount": 1 } }

Schedule a message

Add a future scheduledAt (ISO 8601) to any send or bulk send. The message is queued and delivered at that time — and only billed when it actually goes out.

bash
curl -X POST https://api.sendwamessage.in/api/v1/messages/send \
  -H "x-api-key: wa_live_xxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "to": "919876543210",
    "body": "Happy New Year! 🎉",
    "scheduledAt": "2027-01-01T00:00:00.000Z"
  }'

Link a WhatsApp number (QR)

Connect a number by scanning a QR: create a session, start it, then poll for the QR (a data-URI image) until the status becomes CONNECTED. Requires a user bearer token (see “Auth for a custom app”).

POST/api/v1/sessions
bash
curl -X POST https://api.sendwamessage.in/api/v1/sessions \
  -H "Authorization: Bearer eyJ…" -H "content-type: application/json" \
  -d '{ "name": "Main number" }'
# → 201 { "data": { "id": "sess_…", "status": "INITIALIZING" } }
POST/api/v1/sessions/:id/connect
GET/api/v1/sessions/:id/qr

Start it, then poll the QR every ~3s and render qr (a data:image/png URI) for the user to scan:

json
// GET /sessions/:id/qr  →
{ "data": {
    "status": "QR_PENDING",       // → AUTHENTICATED → CONNECTED once scanned
    "qr": "data:image/png;base64,iVBOR…",
    "qrExpiresAt": "2026-07-23T10:00:20Z",
    "phoneNumber": null
} }
When status is CONNECTED, stop polling — the number is linked and you can send. POST /sessions/:id/disconnect, /logout, or DELETE /sessions/:id to unlink.

Subscriptions & payments

Payments go through Razorpay Checkout. Create an order on your backend, open Checkout in the browser, then let our webhook activate the plan (the webhook is the single source of truth — the browser callback is only for UX).

GET/api/v1/plans
POST/api/v1/billing/checkout
bash
curl -X POST https://api.sendwamessage.in/api/v1/billing/checkout \
  -H "Authorization: Bearer eyJ…" -H "content-type: application/json" \
  -d '{ "planId": "019f…" }'
# → { "data": { "orderId": "order_…", "amountMinor": 20000,
#               "currency": "INR", "keyId": "rzp_live_…" } }

Open Razorpay Checkout with that order:

javascript
const rzp = new Razorpay({
  key: order.keyId,
  order_id: order.orderId,
  amount: order.amountMinor,
  currency: order.currency,
  name: 'Sendwamessage',
  handler: async (r) => {
    // Optional UX confirmation — the webhook is what actually activates the plan.
    await fetch(API + '/api/v1/billing/verify', {
      method: 'POST',
      headers: { authorization: 'Bearer ' + accessToken, 'content-type': 'application/json' },
      body: JSON.stringify(r), // { razorpay_order_id, razorpay_payment_id, razorpay_signature }
    });
  },
});
rzp.open();

Buy extra credits without changing plan (same Checkout flow):

bash
curl https://api.sendwamessage.in/api/v1/billing/topup-options -H "Authorization: Bearer eyJ…"
curl -X POST https://api.sendwamessage.in/api/v1/billing/topup \
  -H "Authorization: Bearer eyJ…" -H "content-type: application/json" \
  -d '{ "credits": 5000 }'   # → a Razorpay order, same as checkout
GET/api/v1/billing

Current subscription, balance, invoices and payment history for the workspace.

Subscribe to the PAYMENT_SUCCEEDED / BULK_COMPLETED webhooks (or poll GET /billing) to reflect activation — never trust the browser callback alone.

Message status

GET/api/v1/messages/:id

Status moves SENT → DELIVERED → READ as WhatsApp confirms, or FAILED (auto-refunded). Use webhooks to receive these in real time rather than polling.

Webhooks

Subscribe an HTTPS endpoint to delivery events. We POST a signed JSON payload; failed deliveries retry with exponential backoff.

POST/api/v1/webhooks
bash
curl -X POST https://api.sendwamessage.in/api/v1/webhooks \
  -H "x-api-key: wa_live_xxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/wa",
    "events": ["MESSAGE_SENT", "MESSAGE_DELIVERED", "MESSAGE_READ", "MESSAGE_FAILED"]
  }'
# → 201 { "data": { "id": "...", "secret": "whsec_..." } }   ← shown ONCE

Payload we POST to your URL:

json
{
  "event": "MESSAGE_DELIVERED",
  "data": { "messageId": "019f8e0f-..." },
  "timestamp": "2026-07-23T09:24:30.000Z"
}

Verify the X-Signature header (HMAC-SHA256 of the raw body with your secret):

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

function verify(rawBody, header, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(header), b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}
Available events: MESSAGE_SENT, MESSAGE_DELIVERED, MESSAGE_READ, MESSAGE_FAILED, BULK_COMPLETED. An endpoint that fails repeatedly is auto-disabled; re-enable it from the dashboard.

Errors & rate limits

Every error is JSON with a stable code:

json
{ "success": false,
  "error": { "code": "INSUFFICIENT_CREDITS",
             "message": "Not enough credits to send." } }
  • 401 — missing/invalid API key
  • 402 INSUFFICIENT_CREDITS — top up your balance
  • 422 SESSION_NOT_CONNECTED — link a WhatsApp number first
  • 429 RATE_LIMITED — slow down; a Retry-After header tells you how long

Need help integrating? Email support@sendwamessage.in.