Base URL: https://wagate.opendrap.website/v1. All requests and responses are JSON. Only message people who agreed to hear from you, and respect rate limits to protect your numbers.
Send your organization token as a bearer token. Keep it on your server.
Authorization: Bearer YOUR_TOKEN # .env WA_API_URL=https://wagate.opendrap.website/v1 WA_API_TOKEN=YOUR_TOKEN
Create a client, for example one of your customers. Use the returned id in the next step — never guess it.
curl -X POST https://wagate.opendrap.website/v1/clients -H "Authorization: Bearer $WA_API_TOKEN" \
-d '{"name":"Acme Store","contact":"owner@acme.example"}'201 Created
{ "id": "cl_xxxx", "name": "Acme Store", "contact": "owner@acme.example", "created_at": "2026-10-14T09:30:00Z" }
# save it: CLIENT_ID=$(curl ... | jq -r .id)curl -X POST https://wagate.opendrap.website/v1/clients -H "Authorization: Bearer %WA_API_TOKEN%" -H "Content-Type: application/json" -d "{\"name\":\"Acme Store\",\"contact\":\"owner@acme.example\"}"
REM no backslash-newline in Windows CMD. Use PowerShell for multi-line, or keep it on one line.List your clients to get real IDs (dynamic — copy the id you need).
curl https://wagate.opendrap.website/v1/clients -H "Authorization: Bearer $WA_API_TOKEN"
{ "clients": [ { "id": "cl_xxxx", "name": "Acme Store", ... } ] }List your devices to get real IDs.
curl https://wagate.opendrap.website/v1/devices -H "Authorization: Bearer $WA_API_TOKEN"
{ "devices": [ { "id": "dev_xxxx", "name": "Acme Store", "status": "pending", ... } ] }Create a device for a client. {id} accepts a client ID or client name — if the name does not exist it is auto-created, so you never get client_not_found for a fresh name. Fails with device_limit_reached when your plan is full.
# dynamic: full flow, no hardcoded IDs
CLIENT_ID=$(curl -s -X POST https://wagate.opendrap.website/v1/clients -H "Authorization: Bearer $WA_API_TOKEN" \
-d '{"name":"Acme Store"}' | jq -r .id)
curl -X POST https://wagate.opendrap.website/v1/clients/$CLIENT_ID/devices -H "Authorization: Bearer $WA_API_TOKEN" \
-d '{"name":"Acme Store"}'
# or shortcut: use the name directly in the path (auto-creates client)
curl -X POST "https://wagate.opendrap.website/v1/clients/Acme Store/devices" -H "Authorization: Bearer $WA_API_TOKEN" \
-d '{"name":"Acme Store"}'
201 Created
{ "id": "dev_xxxx", "client_id": "cl_xxxx", "name": "Acme Store", "status": "pending" }Returns the current QR image as a data URI. It changes about every 60 seconds, so poll it while the client is scanning. state is waiting or connected.
{ "qr": "data:image/...", "state": "waiting", "expires_in": 42 }Get an 8-character pairing code for a phone number (with country code). {id} accepts a device ID or device name (e.g. Acme Store). The client types it into WhatsApp, Linked Devices, Link with phone number.
curl -X POST https://wagate.opendrap.website/v1/devices/DEV_ID/pair -H "Authorization: Bearer $WA_API_TOKEN" -d '{"phone":"919876543210"}'
{ "code": "K7QP-4MXA" }{id} accepts device ID or name. status is pending, pairing, connected or offline.
{ "id": "dev_xxxx", "status": "connected", "phone": "919876543210", "last_seen": "2026-10-14T09:31:00Z" }Log out the WhatsApp link and remove the device. Accepts ID or name.
{ "deleted": true }Send a message. {id} accepts device ID or name. Dynamic body — any of these shapes work (mirrors sendHandler / sendTemplateHandler in your ref):
| Field | Type | Notes |
|---|---|---|
to / number | string | Required for DM. Number with country code (or full JID). |
text / message / body | string | Free text. |
group_jid | string | Send to a group instead (...@g.us). |
template | string | Template name or ID. |
variables | object | Values for the template's variables. |
header_text, footer, buttons | string,object | Hydrated template buttons (quick_reply / visit_site). |
# DEV_ID from GET /v1/devices — or use the device name directly
curl -X POST https://wagate.opendrap.website/v1/devices/$DEV_ID/messages \
-H "Authorization: Bearer $WA_API_TOKEN" -H "Idempotency-Key: appt-8841" \
-d '{"to":"919876543210","template":"appointment_reminder",
"variables":{"name":"Ravi","place":"Acme Store","date":"15 Oct","time":"10:30 AM"}}'r = requests.post(f"{URL}/devices/{DEV_ID}/messages",
headers={"Authorization": f"Bearer {TOKEN}", "Idempotency-Key": "appt-8841"},
json={"to": "919876543210", "text": "Your order has shipped."})const r = await fetch(`${URL}/devices/${DEV_ID}/messages`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Idempotency-Key": "appt-8841" },
body: JSON.stringify({ to: "919876543210", text: "Your order has shipped." }) });202 Accepted
{ "id": "msg_xxxx", "device_id": "dev_xxxx", "to": "919876543210", "status": "queued" }Send to a group (mirrors sendGroupHandler). {id} accepts device ID or name.
curl -X POST https://wagate.opendrap.website/v1/devices/$DEV_ID/send-group -H "Authorization: Bearer $WA_API_TOKEN" \
-d '{"group_jid":"120363000000@g.us","message":"Hello team"}'Bulk send (mirrors bulkHandler — one result per contact, never aborts early).
curl -X POST https://wagate.opendrap.website/v1/devices/$DEV_ID/bulk -H "Authorization: Bearer $WA_API_TOKEN" \
-d '{"contacts":[{"name":"Ravi","number":"919876543210"}],"message":"Hi!"}'Status moves through queued, sent, delivered, read, or ends in failed with an error.
{ "id": "msg_1042", "status": "delivered", "to": "919876543210", "created_at": "..." }Save a message with {{variables}}. Also: GET /v1/templates, GET|PUT|DELETE /v1/templates/{id}, and POST /v1/templates/{id}/preview to render without sending. A missing value returns missing_variable: name.
{ "name": "appointment_reminder",
"body": "Hi {{name}}, your appointment is at {{place}} on {{date}}. Reply STOP to opt out." }Is this number on WhatsApp? (mirrors resolveNumberToJID / IsOnWhatsApp).
{ "number": "919876543210", "on_whatsapp": true }Who is this device logged in as? (mirrors userHandler).
{ "jid": "919876500001@s.whatsapp.net", "name": "Acme Store", "phone": "919876500001" }Address book (mirrors contactsHandler).
{ "contacts": [ { "jid": "919876543210@s.whatsapp.net", "name": "Ravi" } ] }Joined groups (mirrors groupsHandler).
{ "groups": [ { "jid": "120363000000@g.us", "name": "Team" } ] }All sessions on this org — device ID == session ID (mirrors sessionsHandler, no packing cap in mock).
{ "sessions": [ { "session": "dev_xxxx", "connected": true, "loggedIn": true } ], "count": 3 }Close / resume the socket without unpairing (mirrors disconnectHandler / reconnectHandler — no QR needed). Full logout stays DELETE /v1/devices/{id}. Test an inbound message: POST /v1/devices/{id}/incoming {"from":"...","message":"..."} fires message.received to your single webhook.
Set the single URL that receives events for ALL devices. Use device_name / device_id in the payload to route per client. Must be a public http(s) address.
curl -X PUT https://wagate.opendrap.website/v1/webhook -H "Authorization: Bearer $WA_API_TOKEN" -d '{"url":"https://your-app.example/hooks/wagate"}'device.connected, device.disconnected, message.sent, message.delivered, message.read, message.failed, message.received.
POST https://your-app.example/hooks/wagate
Content-Type: application/json
X-Event: message.delivered
X-Signature: 9f2c...e1
{ "event": "message.delivered", "device_id": "dev_xxxx", "device_name": "Your Client", "message_id": "msg_1042",
"status": "delivered", "timestamp": "2026-10-14T09:32:10Z" }200 OK (any 2xx within 5 seconds) Anything else is retried up to 3 times, then logged as failed in your dashboard.
Compute an HMAC-SHA256 of the raw request body with your signing secret and compare it to X-Signature using a constant-time comparison.
import hmac, hashlib
def valid(raw_body: bytes, header: str, secret: str) -> bool:
mac = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, header)const crypto = require("crypto");
const valid = (raw, header, secret) => {
const mac = crypto.createHmac("sha256", secret).update(raw).digest("hex");
return mac.length === header.length && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(header)); };Use it to test your key and show usage in your own dashboard.
{ "organization": "Acme Software", "plan": "Pro",
"devices": { "total": 3, "connected": 2, "limit": 10 },
"messages": { "sent_this_month": 1240, "delivered": 1180, "failed": 22, "cap": 20000 } }| Status | Error | What to do |
|---|---|---|
| 400 | invalid_number | Use digits with country code, 10 to 15 long. |
| 400 | missing_variable: x | Send a value for every template variable. |
| 401 | invalid_token | Check WA_API_TOKEN. |
| 402 | device_limit_reached / trial_expired | Upgrade your plan or ask for an extension. |
| 403 | org_suspended | Contact support. |
| 409 | device_offline | Reconnect the device with a new QR code. |
| 429 | monthly_cap_reached | Wait for next month or upgrade. |
Sends a real request to this server. Get your token from the dashboard: API keys page (/app/keys).