WaGate API

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.

Authentication

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

POST/v1/clients

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.

GET/v1/clients

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", ... } ] }

GET/v1/devices

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", ... } ] }

POST/v1/clients/{id}/devices

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" }

GET/v1/devices/{id}/qr

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 }

POST/v1/devices/{id}/pair

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" }

GET/v1/devices/{id}/status

{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" }

DELETE/v1/devices/{id}

Log out the WhatsApp link and remove the device. Accepts ID or name.

{ "deleted": true }

POST/v1/devices/{id}/messages

Send a message. {id} accepts device ID or name. Dynamic body — any of these shapes work (mirrors sendHandler / sendTemplateHandler in your ref):

FieldTypeNotes
to / numberstringRequired for DM. Number with country code (or full JID).
text / message / bodystringFree text.
group_jidstringSend to a group instead (...@g.us).
templatestringTemplate name or ID.
variablesobjectValues for the template's variables.
header_text, footer, buttonsstring,objectHydrated 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" }

POST/v1/devices/{id}/send-group

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"}'

POST/v1/devices/{id}/bulk

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!"}'

GET/v1/messages/{id}

Status moves through queued, sent, delivered, read, or ends in failed with an error.

{ "id": "msg_1042", "status": "delivered", "to": "919876543210", "created_at": "..." }

POST/v1/templates

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." }

GET/v1/devices/{id}/check?number=...

Is this number on WhatsApp? (mirrors resolveNumberToJID / IsOnWhatsApp).

{ "number": "919876543210", "on_whatsapp": true }

GET/v1/devices/{id}/user

Who is this device logged in as? (mirrors userHandler).

{ "jid": "919876500001@s.whatsapp.net", "name": "Acme Store", "phone": "919876500001" }

GET/v1/devices/{id}/contacts

Address book (mirrors contactsHandler).

{ "contacts": [ { "jid": "919876543210@s.whatsapp.net", "name": "Ravi" } ] }

GET/v1/devices/{id}/groups

Joined groups (mirrors groupsHandler).

{ "groups": [ { "jid": "120363000000@g.us", "name": "Team" } ] }

GET/v1/sessions

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 }

POST/v1/devices/{id}/disconnect · /reconnect

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.

PUT/v1/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"}'

Event types

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.

Verify the signature

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)); };

GET/v1/stats

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 } }

Errors

StatusErrorWhat to do
400invalid_numberUse digits with country code, 10 to 15 long.
400missing_variable: xSend a value for every template variable.
401invalid_tokenCheck WA_API_TOKEN.
402device_limit_reached / trial_expiredUpgrade your plan or ask for an extension.
403org_suspendedContact support.
409device_offlineReconnect the device with a new QR code.
429monthly_cap_reachedWait for next month or upgrade.

Try it

Sends a real request to this server. Get your token from the dashboard: API keys page (/app/keys).

Response appears here.