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.inGet 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.
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.
/api/v1/auth/logincurl -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):
{ "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)
} }curl https://api.sendwamessage.in/api/v1/sessions \
-H "Authorization: Bearer eyJ…"Send a text message
/api/v1/messages/sendto accepts any format — bare national numbers get your workspace country code. Provide a body, a mediaId, or both.
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
{
"success": true,
"data": {
"id": "019f8e0f-...",
"status": "SENT",
"toNumber": "919876543210",
"waMessageId": "wamid.HBg...",
"sentAt": "2026-07-23T09:24:11.000Z"
}
}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).
/api/v1/mediacurl -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" } }/api/v1/messages/sendcurl -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)
/api/v1/messages/bulkSend 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.
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": [] } }/api/v1/messages/bulk/:idPoll for progress:
{ "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.
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”).
/api/v1/sessionscurl -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" } }/api/v1/sessions/:id/connect/api/v1/sessions/:id/qrStart it, then poll the QR every ~3s and render qr (a data:image/png URI) for the user to scan:
// 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
} }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).
/api/v1/plans/api/v1/billing/checkoutcurl -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:
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):
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/api/v1/billingCurrent subscription, balance, invoices and payment history for the workspace.
PAYMENT_SUCCEEDED / BULK_COMPLETED webhooks (or poll GET /billing) to reflect activation — never trust the browser callback alone.Message status
/api/v1/messages/:idStatus 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.
/api/v1/webhookscurl -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 ONCEPayload we POST to your URL:
{
"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):
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);
}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:
{ "success": false,
"error": { "code": "INSUFFICIENT_CREDITS",
"message": "Not enough credits to send." } }401— missing/invalid API key402 INSUFFICIENT_CREDITS— top up your balance422 SESSION_NOT_CONNECTED— link a WhatsApp number first429 RATE_LIMITED— slow down; aRetry-Afterheader tells you how long
Need help integrating? Email support@sendwamessage.in.