API Reference
Devices
List channels and quota, add new Device (QR) numbers, pair via QR or pairing code, reconnect, delete, and onboard Cloud API / Coexistence numbers through hosted sessions.
POST /devices → QR or pairing code → poll status). Cloud / Coexistence uses a Washeej-hosted Embedded Signup page (Facebook login + OTP + PIN) because Meta does not allow partners to embed that flow with Washeej's app credentials.
Prerequisites
- Active plan with the right channel flags:
device_channels_availablefor QR,cloud_api_availablefor Cloud / Coexistence. - Free shared
account_limitslot (seequotaonGET /devices;-1= unlimited). - Scope
devices:readto list/status,qr:generatefor QR,devices:managefor create / pairing / reconnect / delete / default / hosted onboarding. - For Cloud hosted onboarding: register your callback host as a redirect URI under Dashboard → Developer Apps (required even with API-key auth).
/devices
devices:read
https://app.washeej.com/v1/devices
List all WhatsApp channels (Cloud + Device/QR) with the account quota.
curl -H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET" \
https://app.washeej.com/v1/devices
{
"ok": true,
"data": {
"devices": [
{
"id": 47,
"business_name": "My Business",
"phone_number": "966920000000",
"channel_type": "cloud",
"connection_status": "connected",
"is_default": true,
"origin": "cloud",
"created_at": "2026-07-01T10:00:00+03:00"
}
],
"quota": {
"account_limit_remaining": 3,
"channels_in_use": 2,
"cloud_api_available": true,
"device_channels_available": true
}
},
"meta": {
"request_id": "uuid"
}
}
- quota.account_limit_remaining = -1 means unlimited.
- origin is "cloud" for Cloud API / Coexistence numbers and "device" for QR device channels.
- effective_channel_type resolves legacy rows where channel_type and origin disagree — prefer it over channel_type when routing logic.
- Legacy keys (id, business_name, phone_number, channel_type, connection_status, last_seen_at, is_default) are stable; new fields are additive.
/devices/{id}/status
devices:read
https://app.washeej.com/v1/devices/{id}/status
Connection status for a channel (live-syncs Device/QR from GOWA).
| Parameter | Type | Required | Description |
|---|---|---|---|
id
path
|
integer | required | WhatsApp account / device ID. |
curl -H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET" \
https://app.washeej.com/v1/devices/YOUR_ACCOUNT_ID/status
{
"ok": true,
"data": {
"id": 52,
"connection_status": "connected",
"last_seen_at": "2026-07-18T14:12:41+03:00",
"last_heartbeat_at": "2026-07-18T14:12:41+03:00",
"last_error": null,
"phone_number": "966500000000",
"gowa_raw_status": "CONNECTED",
"awaiting_link": false
},
"meta": {
"request_id": "uuid"
}
}
- For Device/QR channels this endpoint refreshes status from the WhatsApp device service on every call — poll it after scanning a QR or entering a pairing code until connection_status is connected.
- While the link is still in progress, connection_status stays "pending" and awaiting_link is true — even if the raw device service reports DISCONNECTED (gowa_raw_status). Do not treat pending as a failure; wait for connected.
- A channel only becomes "disconnected" after it was connected and then lost the link.
- Cloud / Coexistence channels return the stored status (no GOWA sync; gowa_raw_status is null).
- Response also includes phone_number when known.
- Tip: instead of polling you can subscribe to the device.connected / device.status_changed webhooks — they also fire when a transition is discovered by this endpoint.
/devices
devices:manage
https://app.washeej.com/v1/devices
Create a new Device (QR) channel within the plan quota.
| Parameter | Type | Required | Description |
|---|---|---|---|
name
|
string | required | Channel display name (max 50). |
type
|
string | optional | Only "device" is accepted (default). Cloud numbers use hosted onboarding sessions. |
phone_number
|
string | optional | Digits only, with country code. If the number already has a pending device channel, that channel is reused instead of creating a new one. |
curl -X POST https://app.washeej.com/v1/devices \
-H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"type": "device",
"name": "Store Front",
"phone_number": "966500000000"
}'
{
"ok": true,
"data": {
"device": {
"id": 52,
"business_name": "Store Front",
"phone_number": "966500000000",
"channel_type": "device",
"connection_status": "pending",
"is_default": false,
"origin": "device"
},
"reused": false,
"message": "Device channel created. Generate a QR or pairing code to connect."
},
"meta": {
"request_id": "uuid"
}
}
- Requires device_channels_available on your plan and a free account_limit slot.
- After creation, call POST /devices/{id}/qr (or /pairing-code) and poll GET /devices/{id}/status until connected.
- A connected duplicate phone_number is rejected with duplicate_device (409).
Common errors:
feature_disabled, limit_reached, duplicate_device, unsupported_type, gowa_error, validation_error
/devices/{id}/qr
qr:generate
https://app.washeej.com/v1/devices/{id}/qr
Generate or refresh a QR code for Device/QR pairing.
| Parameter | Type | Required | Description |
|---|---|---|---|
id
path
|
integer | required | Device channel ID (channel_type=device). |
curl -X POST https://app.washeej.com/v1/devices/YOUR_DEVICE_ID/qr \
-H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET"
{
"ok": true,
"data": {
"device_id": 52,
"qr": "data:image/png;base64,iVBOR\u2026",
"status": "PENDING",
"connected": false,
"qr_duration": 30
},
"meta": {
"request_id": "uuid"
}
}
- Only works for Device/QR channels. Cloud accounts return invalid_channel_type.
- qr is a data:image/png;base64 URI. Scan it with WhatsApp on the phone, then poll GET /devices/{id}/status until connected.
- If the device is already connected, qr is null and connected is true.
- The QR expires after qr_duration seconds — call this endpoint again to refresh it.
Common errors:
invalid_channel_type, qr_failed, gowa_error, not_found
/devices/{id}/pairing-code
devices:manage
https://app.washeej.com/v1/devices/{id}/pairing-code
Generate a text pairing code (alternative to scanning a QR).
| Parameter | Type | Required | Description |
|---|---|---|---|
id
path
|
integer | required | Device channel ID. |
phone
|
string | required | Phone number with country code (digits). Alias: phone_number. |
curl -X POST https://app.washeej.com/v1/devices/YOUR_DEVICE_ID/pairing-code \
-H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{"phone": "966500000000"}'
{
"ok": true,
"data": {
"device_id": 52,
"pairing_code": "ABCD-1234",
"phone_number": "966500000000",
"message": "Pairing code generated. Enter it in WhatsApp on the phone."
},
"meta": {
"request_id": "uuid"
}
}
- The user enters the code in WhatsApp → Linked Devices → Link with phone number instead.
- Poll GET /devices/{id}/status until connected (status live-syncs from GOWA).
- If pairing fails, fall back to POST /devices/{id}/qr.
- Accepts either phone or phone_number in the JSON body.
Common errors:
invalid_channel_type, pairing_failed, gowa_error, validation_error, not_found
/devices/{id}/reconnect
devices:manage
https://app.washeej.com/v1/devices/{id}/reconnect
Reset a disconnected/orphaned device so it can be re-paired.
| Parameter | Type | Required | Description |
|---|---|---|---|
id
path
|
integer | required | Device channel ID. |
curl -X POST https://app.washeej.com/v1/devices/YOUR_DEVICE_ID/reconnect \
-H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET"
- After reconnect, generate a fresh QR or pairing code to complete the connection.
Common errors:
invalid_channel_type, not_found
/devices/{id}/default
devices:manage
https://app.washeej.com/v1/devices/{id}/default
Set a channel (any type) as the account default for sending.
| Parameter | Type | Required | Description |
|---|---|---|---|
id
path
|
integer | required | Channel ID. |
curl -X POST https://app.washeej.com/v1/devices/YOUR_DEVICE_ID/default \
-H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET"
Common errors:
not_found
/devices/{id}
devices:manage
https://app.washeej.com/v1/devices/{id}
Delete any channel (Device, Cloud, Coexistence); frees one quota slot.
| Parameter | Type | Required | Description |
|---|---|---|---|
id
path
|
integer | required | Channel ID (any type). |
curl -X DELETE https://app.washeej.com/v1/devices/YOUR_DEVICE_ID \
-H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET"
{
"ok": true,
"data": {
"deleted": true,
"channel_type": "cloud",
"quota_restored": true,
"device_id": 52
},
"meta": {
"request_id": "uuid"
}
}
- DESTRUCTIVE AND IRREVERSIBLE: deleting a channel also deletes all conversations, messages, templates, and campaigns linked to it — the same cascade as deleting the number from the dashboard.
- Works for every channel type: Device (QR), Cloud, and Coexistence.
- Device channels are logged out from the WhatsApp device service first (best-effort).
- One account_limit slot is restored on success (unless the plan is unlimited) — quota_restored in the response tells you which.
- If the deleted channel was the default, the first remaining channel becomes the new default.
Common errors:
not_found, server_error
/devices/cloud/onboarding-sessions
devices:manage
https://app.washeej.com/v1/devices/cloud/onboarding-sessions
Create a hosted onboarding session to add a Cloud API (or Coexistence) number.
| Parameter | Type | Required | Description |
|---|---|---|---|
return_url
|
url | required | HTTPS URL on your platform to receive the user back. Its host must be registered as a redirect URI on your developer app. |
state
|
string | optional | Opaque CSRF token (max 128 chars) echoed back on the redirect. |
mode
|
string | optional | "cloud" (default) or "coexistence" (keeps the number active in the WhatsApp Business App; requires the feature to be enabled). |
developer_app_id
|
integer | optional | When using API keys (no OAuth app context), pin the return_url allowlist to one Developer App instead of the union of all apps on the account. Ignored for OAuth Bearer tokens (bound automatically). |
curl -X POST https://app.washeej.com/v1/devices/cloud/onboarding-sessions \
-H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"return_url": "https://partner.example.com/washeej/callback",
"state": "random-csrf-token"
}'
{
"ok": true,
"data": {
"session_id": "9c2f1a4e-1234-4bcd-9e0f-abcdef012345",
"mode": "cloud",
"hosted_url": "https://app.washeej.com/user/whatsapp-onboarding/9c2f1a4e-\u2026",
"expires_at": "2026-07-18T14:30:00+03:00",
"status": "pending"
},
"meta": {
"request_id": "uuid"
}
}
- Prerequisites: (1) Register your callback host as a redirect URI on a Developer App in the Washeej Developer Portal — required even when authenticating with API keys. (2) Grant scope devices:manage. (3) Plan must have cloud_api_available and a free account_limit slot.
- Redirect your user to hosted_url in a full browser window (not an iframe — Meta Embedded Signup blocks framing).
- The end user must sign in to the same Washeej account that owns the API credentials. A different account receives HTTP 403.
- Washeej hosts Meta Embedded Signup (Facebook login, portfolio + number selection, OTP, PIN). Meta app secrets and access tokens never reach your platform.
- When onboarding ends, the user is redirected to return_url?session_id=…&status=completed|failed|expired&state=…&device_id=… — always confirm with GET /devices/cloud/onboarding-sessions/{id} server-side; never trust the redirect alone.
- Sessions expire after 30 minutes. If the number finishes connecting just after expiry, Washeej still finalizes the session so you receive device_id.
- return_url must be HTTPS (HTTP allowed only for localhost / 127.0.0.1). Only the host is allowlisted — path may differ from the registered redirect URI.
- Coexistence mode (mode=coexistence): number stays active in the WhatsApp Business App (no PIN). Contact & chat history sync must complete within Meta's 24-hour window; sending via API may be limited until sync finishes.
Common errors:
feature_disabled, limit_reached, return_url_not_allowed, validation_error
/devices/cloud/onboarding-sessions/{id}
devices:manage
https://app.washeej.com/v1/devices/cloud/onboarding-sessions/{id}
Check a hosted onboarding session status; returns the device on completion.
| Parameter | Type | Required | Description |
|---|---|---|---|
id
path
|
uuid | required | Session ID from the create call. |
curl -H "client-id: YOUR_CLIENT_ID" \
-H "client-secret: YOUR_CLIENT_SECRET" \
https://app.washeej.com/v1/devices/cloud/onboarding-sessions/SESSION_ID
{
"ok": true,
"data": {
"session_id": "9c2f1a4e-1234-4bcd-9e0f-abcdef012345",
"mode": "cloud",
"status": "completed",
"expires_at": "2026-07-18T14:30:00+03:00",
"completed_at": "2026-07-18T14:12:41+03:00",
"device": {
"id": 61,
"business_name": "My Business",
"phone_number": "966920000000",
"channel_type": "cloud",
"connection_status": "connected",
"is_default": false,
"origin": "cloud"
}
},
"meta": {
"request_id": "uuid"
}
}
Common errors:
not_found
QR pairing flow (Device channels)
POST /devices with type: device — requires scope devices:manage and a free quota slot. Optional phone_number reuses a pending record for the same digits.POST /devices/{id}/qr (scope qr:generate) or POST /devices/{id}/pairing-code with {"phone":"9665…"}.GET /devices/{id}/status every 2–3s until connection_status is connected. While the link is in progress the status stays pending with awaiting_link: true — it never flips to disconnected before the first successful link. Or subscribe to the device.connected webhook instead of polling.Cloud / Coexistence flow (hosted onboarding)
https://partner.example.com/washeej/callback (host is what matters).POST /devices/cloud/onboarding-sessions with return_url, optional state, optional mode (cloud | coexistence).hosted_url in a top-level window. User signs into Washeej (same account as the API), then Meta Embedded Signup.return_url?session_id&status&state&device_id. Confirm with GET /devices/cloud/onboarding-sessions/{id} — never trust the query string alone.Common scenarios
| Scenario | Behaviour |
|---|---|
Create with a phone already connected |
409 duplicate_device — use reconnect or a different number. |
| Create with a phone that has a pending/disconnected channel | 200 with reused: true — no extra quota consumed; generate QR/pairing again. |
POST /devices with type: cloud |
422 unsupported_type — use hosted onboarding sessions. |
| QR/pairing/reconnect on a Cloud ID | 422 invalid_channel_type. |
| Polling status while QR not yet scanned | connection_status: pending, awaiting_link: true (raw GOWA state in gowa_raw_status) — not a failure. |
| QR on an already connected device | qr: null, connected: true. |
| Plan without Device Channels | 403 feature_disabled on create. |
Quota exhausted (account_limit = 0) |
403 limit_reached. |
return_url host not registered |
422 return_url_not_allowed with allowed_hosts. |
API key without devices:manage in explicit scopes |
403 insufficient_scope on manage endpoints. Keys with scopes = null keep all scopes (including manage). |
| Delete any channel (Device, Cloud, Coexistence) | Irreversible cascade: conversations, messages, templates, and campaigns tied to the channel are deleted (GOWA logout first for Device). One account_limit slot restored — see quota_restored. |
Compatibility notes
GET /devices,GET /devices/{id}/status, andPOST /devices/{id}/qrkeep their legacy response keys. New fields (quota,origin,effective_channel_type,connected,qr_duration,phone_number/gowa_raw_status/awaiting_linkon status) are additive.- Legacy
/device-api/v1and/external-apiare unchanged. - Webhooks:
device.connected/device.disconnectedfire for Device channels (unchanged), and the newdevice.status_changedfires on every transition for all channel types withfrom_status/to_status. Transitions discovered by the status poll also fire these events — see Webhook payloads. DELETE /devices/{id}now accepts every channel type (previously Device only). Existing Device-only integrations are unaffected.