# Washeej API v1 — Developer Documentation

English developer reference. HTML UI: https://app.washeej.com/api-v1-documentation/getting-started

- **Base URL:** `https://app.washeej.com/v1`
- **Auth:** `client-id` + `client-secret` headers, or OAuth Bearer
- **Rate limit:** 120 req/min/account
- **Machine index:** https://app.washeej.com/llms.txt
- **OpenAPI (templates):** https://app.washeej.com/api-v1-documentation/openapi/templates.yaml

## Feature availability — WhatsApp Templates API

- Code deployed; public documentation available.
- Global lifecycle activation remains **disabled** unless a canary / feature assignment is granted.
- Internal Meta canary qualification pending (do not claim Meta E2E passed or globally enabled).
- Legacy send compatibility preserved (`POST /v1/templates/send`, `POST /external-api/templates/send` with durable Idempotency-Key; legacy `POST /external-api/inbox/send-template-message` does not require the key).
- Accounts pagination: optional `page` / `per_page` on WhatsApp accounts list (BC default without pagination params).
- HTML: https://app.washeej.com/api-v1-documentation/reference/templates/lifecycle
- Send: https://app.washeej.com/api-v1-documentation/reference/templates/lifecycle/send
- Idempotency: https://app.washeej.com/api-v1-documentation/reference/templates/lifecycle/idempotency
- Changelog: https://app.washeej.com/api-v1-documentation/changelog

## Table of contents

- [Getting Started](https://app.washeej.com/api-v1-documentation/getting-started)
- [Authentication](https://app.washeej.com/api-v1-documentation/authentication)
- [Response Format](https://app.washeej.com/api-v1-documentation/response-format)
- [Errors](https://app.washeej.com/api-v1-documentation/errors)
- [Account](https://app.washeej.com/api-v1-documentation/reference/account)
- [Contacts](https://app.washeej.com/api-v1-documentation/reference/contacts)
- [Conversations](https://app.washeej.com/api-v1-documentation/reference/conversations)
- [Messages](https://app.washeej.com/api-v1-documentation/reference/messages)
- [Templates (send)](https://app.washeej.com/api-v1-documentation/reference/templates)
- [WhatsApp Templates API](https://app.washeej.com/api-v1-documentation/reference/templates/lifecycle)
- [Templates lifecycle send](https://app.washeej.com/api-v1-documentation/reference/templates/lifecycle/send)
- [Templates lifecycle idempotency](https://app.washeej.com/api-v1-documentation/reference/templates/lifecycle/idempotency)
- [CRM Scenario Routing](https://app.washeej.com/api-v1-documentation/guides/crm-scenario-routing)
- [Devices](https://app.washeej.com/api-v1-documentation/reference/devices)
- [Webhooks Overview](https://app.washeej.com/api-v1-documentation/reference/webhooks)
- [Webhooks Manage](https://app.washeej.com/api-v1-documentation/reference/webhooks/manage)
- [Webhook Payloads](https://app.washeej.com/api-v1-documentation/reference/webhooks/payloads)
- [OAuth](https://app.washeej.com/api-v1-documentation/oauth)
- [Scopes](https://app.washeej.com/api-v1-documentation/scopes)
- [Integration Playbook](https://app.washeej.com/api-v1-documentation/guides/integration-playbook)
- [Channel Selection](https://app.washeej.com/api-v1-documentation/guides/channel-selection)
- [Session Window vs Templates](https://app.washeej.com/api-v1-documentation/guides/session-window)
- [Session Media](https://app.washeej.com/api-v1-documentation/guides/session-media)
- [Interactive Messages](https://app.washeej.com/api-v1-documentation/guides/interactive-messages)
- [Template Media Headers](https://app.washeej.com/api-v1-documentation/guides/template-media-headers)
- [Dynamic Invoice Link](https://app.washeej.com/api-v1-documentation/guides/dynamic-invoice-link)
- [Authentication OTP](https://app.washeej.com/api-v1-documentation/guides/authentication-otp)
- [Changelog](https://app.washeej.com/api-v1-documentation/changelog)

## Errors

| Code | HTTP | Description |
| --- | --- | --- |
| `unauthorized` | 401 | Missing or invalid credentials. |
| `invalid_credentials` | 401 | client-id / client-secret do not match. |
| `invalid_token` | 401 | OAuth bearer token expired or revoked. |
| `ip_not_whitelisted` | 401 | Caller IP is not on the account whitelist (API key auth). |
| `insufficient_scope` | 403 | Token or key lacks the required scope. |
| `subscription_required` | 403 | Active plan with API access is required. |
| `billing_blocked` | 403 | WhatsApp Cloud billing is not eligible for this number. |
| `rate_limit_exceeded` | 429 | More than 120 requests per minute for this account. |
| `not_found` | 404 | Resource not found or not owned by the account. |
| `validation_error` | 422 | Request parameters failed validation. |
| `duplicate_contact` | 409 | Contact with this phone already exists. |
| `marketing_suppressed` | 422 | Contact opted out of marketing messages. |
| `capability_error` | 422 | Operation not supported on this channel type. |
| `feature_disabled` | 403 | The channel type (Cloud / Device QR / Coexistence) is not available in the account plan or platform config. |
| `limit_reached` | 403 | The plan's WhatsApp account quota (account_limit) is exhausted. |
| `duplicate_device` | 409 | This phone number is already connected as a device channel. |
| `invalid_channel_type` | 422 | Operation only applies to Device (QR) channels. |
| `unsupported_type` | 422 | POST /devices only creates type=device. Cloud numbers use hosted onboarding sessions. |
| `return_url_not_allowed` | 422 | return_url host is not among the redirect URIs registered on your developer app. |
| `gowa_error` | 502 | The WhatsApp device service failed to process the request. Retry later. |
| `qr_failed` | 422 | QR code generation failed for this device. |
| `pairing_failed` | 422 | Pairing code generation failed — try the QR code instead. |
| `invalid_phone` | 422 | Phone number could not be normalized or is invalid. |
| `user_error` | 401 | Credentials did not resolve to an API user. |
| `send_failed` | 422 | Upstream WhatsApp send failed; inspect message details / Meta error. |
| `exception` | 500 | Unexpected server exception while processing the request. |

## Scopes

- `messages:send` — Send WhatsApp messages
- `messages:read` — Read messages and delivery status
- `messages:webhook` — Receive message events via webhooks
- `contacts:read` — Read contacts
- `contacts:write` — Create and update contacts
- `devices:read` — View connected devices and channels
- `devices:manage` — Add, connect, reconnect, and delete WhatsApp channels (Device QR + Cloud hosted onboarding)
- `qr:generate` — Generate QR codes for device pairing
- `conversations:read` — Read conversations and inbox
- `templates:read` — Read message templates
- `templates:write` — Create, edit, and delete Meta templates (explicit grant required)
- `templates:sync` — Sync Meta templates for a WhatsApp Cloud account
- `templates:send` — Send approved Meta templates (alias of messages:send when granted)
- `whatsapp_accounts:read` — List WhatsApp accounts and template capabilities
- `webhooks:manage` — Register and manage developer webhook endpoints
- `account:read` — Read account profile and usage

### Reserved / notes

- `templates:write` — Create/edit/delete Meta templates via Public API (canary + explicit scope).
- `templates:sync` — Sync Meta templates for a Cloud WhatsApp account (canary + explicit scope).

## Endpoints

### GET `/ping`

- **Key:** `ping`
- **URL:** `https://app.washeej.com/v1/ping`
- **Auth:** public / none
- **Summary:** Health check (public, no authentication).

```bash
curl https://app.washeej.com/v1/ping
```

```json
{
    "ok": true,
    "data": {
        "status": "ok",
        "version": "v1"
    },
    "meta": {
        "request_id": "uuid"
    }
}
```

### GET `/account`

- **Key:** `account.show`
- **URL:** `https://app.washeej.com/v1/account`
- **Scope:** `account:read`
- **Summary:** Return account profile, plan, and usage summary.

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/account
```

```json
{
    "ok": true,
    "data": {
        "id": 16,
        "email": "you@example.com",
        "api_available": true
    },
    "meta": {
        "request_id": "uuid"
    }
}
```

### GET `/contacts`

- **Key:** `contacts.index`
- **URL:** `https://app.washeej.com/v1/contacts`
- **Scope:** `contacts:read`
- **Summary:** List contacts with optional search and pagination.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `search` | string | no | Filter by first name, last name, or mobile. |
| `per_page` | integer | no | Page size (default 25, max 100). |
| `page` | integer | no | Page number. |

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     "https://app.washeej.com/v1/contacts?search=Ahmed&per_page=25"
```

### POST `/contacts`

- **Key:** `contacts.store`
- **URL:** `https://app.washeej.com/v1/contacts`
- **Scope:** `contacts:write`
- **Summary:** Create a contact.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `firstname` | string | yes | First name (max 100). |
| `lastname` | string | no | Last name (max 100). |
| `mobile_code` | string | yes | Country dial code without + (e.g. 966). |
| `mobile` | string | yes | National number digits only. |

```bash
curl -X POST https://app.washeej.com/v1/contacts \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "firstname": "Ahmed",
    "lastname": "Ali",
    "mobile_code": "966",
    "mobile": "500000000"
  }'
```

Common errors: `invalid_phone`, `duplicate_contact`, `validation_error`

### PATCH `/contacts/{id}`

- **Key:** `contacts.update`
- **URL:** `https://app.washeej.com/v1/contacts/{id}`
- **Scope:** `contacts:write`
- **Summary:** Update contact name fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Contact ID. |
| `firstname` | string | no | First name. |
| `lastname` | string | no | Last name. |

```bash
curl -X PATCH https://app.washeej.com/v1/contacts/4821 \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"firstname": "Ahmed","lastname": "Hassan"}'
```

### DELETE `/contacts/{id}`

- **Key:** `contacts.destroy`
- **URL:** `https://app.washeej.com/v1/contacts/{id}`
- **Scope:** `contacts:write`
- **Summary:** Delete a contact.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Contact ID. |

```bash
curl -X DELETE https://app.washeej.com/v1/contacts/4821 \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET"
```

### GET `/conversations`

- **Key:** `conversations.index`
- **URL:** `https://app.washeej.com/v1/conversations`
- **Scope:** `conversations:read`
- **Summary:** List inbox conversations.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `whatsapp_account_id` | integer | no | Filter by channel ID. |
| `per_page` | integer | no | Page size (default 25, max 100). |

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     "https://app.washeej.com/v1/conversations?whatsapp_account_id=YOUR_ACCOUNT_ID"
```

### GET `/conversations/{id}/messages`

- **Key:** `conversations.messages`
- **URL:** `https://app.washeej.com/v1/conversations/{id}/messages`
- **Scope:** `messages:read`
- **Summary:** List messages in a conversation (newest first, paginated).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Conversation ID. |
| `per_page` | integer | no | Page size. |

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/conversations/20717/messages
```

### POST `/messages`

- **Key:** `messages.send`
- **URL:** `https://app.washeej.com/v1/messages`
- **Scope:** `messages:send`
- **Summary:** Send a free-form session message (text, media, location, or Cloud interactive/commerce) on Cloud or Device channels.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `mobile_code` | string | yes | Country dial code without +. |
| `mobile` | string | yes | National number digits only. |
| `whatsapp_account_id` | integer | no | Preferred channel ID from GET /devices. Use this in multi-number accounts. |
| `from_number` | string | no | Alternative channel selector — digits only, no + or spaces. |
| `message` | string | no | Text body or media caption. Required unless sending media/interactive/location/product. |
| `image` | file | no | Image upload (multipart). |
| `document` | file | no | Document upload (multipart). |
| `video` | file | no | Video upload (multipart). |
| `audio` | file | no | Audio upload (multipart). |
| `image_url` | url | no | Public HTTPS image URL (fetched server-side). |
| `document_url` | url | no | Public HTTPS document URL (fetched server-side). |
| `video_url` | url | no | Public HTTPS video URL. |
| `audio_url` | url | no | Public HTTPS audio URL. |
| `filename` | string | no | Display filename when using document_url. |
| `latitude` | number | no | Location latitude (send with longitude). |
| `longitude` | number | no | Location longitude (send with latitude). |
| `name` | string | no | Optional location title. |
| `address` | string | no | Optional location address line. |
| `cta_url_id` | integer | no | Dashboard CTA URL asset ID (Cloud only). |
| `interactive_list_id` | integer | no | Dashboard interactive list ID (Cloud only). |
| `product` | json | no | Commerce product payload (Cloud). |
| `created_order_data` | json | no | Commerce order payload (Cloud). |

Notes:

- Inside the Cloud 24-hour customer care window you can send free-form messages. Outside that window use templates — see Guides → Session Window.
- Prefer whatsapp_account_id on multi-number accounts — see Guides → Channel Selection.
- Session media via multipart or *_url fields — see Guides → Session Media.
- CTA / lists / product require Cloud API — see Guides → Interactive Messages.
- Cloud numbers may return billing_blocked when Meta payment eligibility fails.

```bash
curl -X POST https://app.washeej.com/v1/messages \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "mobile_code": "966",
    "mobile": "500000000",
    "whatsapp_account_id": YOUR_ACCOUNT_ID,
    "message": "Hello from Washeej API"
  }'
```

```json
{
    "ok": true,
    "data": {
        "message_id": 300541,
        "conversation_id": 20717,
        "whatsapp_message_id": "wamid.XXX"
    },
    "meta": {
        "request_id": "uuid"
    }
}
```

Common errors: `billing_blocked`, `validation_error`, `capability_error`, `not_found`, `invalid_phone`

### GET `/messages/{id}`

- **Key:** `messages.show`
- **URL:** `https://app.washeej.com/v1/messages/{id}`
- **Scope:** `messages:read`
- **Summary:** Get a single message by Washeej message ID.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Internal message ID. |

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/messages/300541
```

### GET `/templates`

- **Key:** `templates.index`
- **URL:** `https://app.washeej.com/v1/templates`
- **Scope:** `templates:read`
- **Summary:** List approved Cloud templates for your account.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `whatsapp_account_id` | integer | no | Filter by Cloud account ID. |
| `per_page` | integer | no | Page size (default 25, max 100). |

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     "https://app.washeej.com/v1/templates?whatsapp_account_id=YOUR_ACCOUNT_ID"
```

### POST `/templates/send`

- **Key:** `templates.send`
- **URL:** `https://app.washeej.com/v1/templates/send`
- **Scope:** `messages:send`
- **Summary:** Send an approved Cloud template message (text variables and/or DOCUMENT/IMAGE/VIDEO header media).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `template_id` | string | yes | Washeej template ID or Meta whatsapp_template_id. |
| `mobile_code` | string | yes | Country dial code without +. |
| `mobile` | string | yes | National number digits only. |
| `whatsapp_account_id` | integer | no | Cloud account to send from. |
| `from_number` | string | no | Alternative channel selector (digits only). |
| `body_variables` | string[] | no | Ordered values for {{1}}, {{2}}, … in the body. Omit when the body has no placeholders. |
| `header_variables` | string[] | no | Values for TEXT header variables only. |
| `header_media_url` | url | no | Public HTTPS URL Meta will fetch when header_format is DOCUMENT, IMAGE, or VIDEO. Delivers a native media bubble (not a text link). |
| `header_media_file` | file | no | Multipart file upload alternative to header_media_url (also accepts media_file). |

Notes:

- Templates require a Cloud API channel (not Device/QR).
- Lifecycle send paths require Idempotency-Key (see WhatsApp Templates API → Send / Idempotency). Missing key → 422 idempotency_key_required.
- Programmatic create/submit is available via WhatsApp Templates API when the lifecycle feature flag / canary assignment allows; otherwise create text templates in the dashboard. API send still lists and sends approved templates.
- DOCUMENT/IMAGE/VIDEO: use header_media_url or header_media_file — see Guides → Template Media Headers.
- Text URL inside the body: use body_variables — see Guides → Dynamic Invoice Link.
- AUTHENTICATION OTP: exactly one 4–8 digit code in body_variables — see Guides → Authentication OTP.
- Meta requires body/header text variables to not sit at the start or end of the copy.
- If media fields are omitted on a media-header template, the template sample media may be reused.
- MARKETING templates may return marketing_suppressed for opted-out contacts.

```bash
curl -X POST https://app.washeej.com/v1/templates/send \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Idempotency-Key: send-YOUR-UNIQUE-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "YOUR_TEMPLATE_ID",
    "mobile_code": "966",
    "mobile": "500000000",
    "whatsapp_account_id": YOUR_CLOUD_ACCOUNT_ID,
    "header_media_url": "https://files.example.com/docs/INV-1001.pdf",
    "body_variables": ["Ahmed", "INV-1001"]
  }'
```

Common errors: `billing_blocked`, `marketing_suppressed`, `capability_error`, `validation_error`, `not_found`

### GET `/devices`

- **Key:** `devices.index`
- **URL:** `https://app.washeej.com/v1/devices`
- **Scope:** `devices:read`
- **Summary:** List all WhatsApp channels (Cloud + Device/QR) with the account quota.

Notes:

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

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/devices
```

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

### POST `/devices`

- **Key:** `devices.store`
- **URL:** `https://app.washeej.com/v1/devices`
- **Scope:** `devices:manage`
- **Summary:** Create a new Device (QR) channel within the plan quota.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Channel display name (max 50). |
| `type` | string | no | Only "device" is accepted (default). Cloud numbers use hosted onboarding sessions. |
| `phone_number` | string | no | Digits only, with country code. If the number already has a pending device channel, that channel is reused instead of creating a new one. |

Notes:

- 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).

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

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

Common errors: `feature_disabled`, `limit_reached`, `duplicate_device`, `unsupported_type`, `gowa_error`, `validation_error`

### GET `/devices/{id}/status`

- **Key:** `devices.status`
- **URL:** `https://app.washeej.com/v1/devices/{id}/status`
- **Scope:** `devices:read`
- **Summary:** Connection status for a channel (live-syncs Device/QR from GOWA).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | WhatsApp account / device ID. |

Notes:

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

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/devices/YOUR_ACCOUNT_ID/status
```

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

### POST `/devices/{id}/qr`

- **Key:** `devices.qr`
- **URL:** `https://app.washeej.com/v1/devices/{id}/qr`
- **Scope:** `qr:generate`
- **Summary:** Generate or refresh a QR code for Device/QR pairing.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Device channel ID (channel_type=device). |

Notes:

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

```bash
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"
```

```json
{
    "ok": true,
    "data": {
        "device_id": 52,
        "qr": "data:image/png;base64,iVBOR\u2026",
        "status": "PENDING",
        "connected": false,
        "qr_duration": 30
    },
    "meta": {
        "request_id": "uuid"
    }
}
```

Common errors: `invalid_channel_type`, `qr_failed`, `gowa_error`, `not_found`

### POST `/devices/{id}/pairing-code`

- **Key:** `devices.pairing_code`
- **URL:** `https://app.washeej.com/v1/devices/{id}/pairing-code`
- **Scope:** `devices:manage`
- **Summary:** Generate a text pairing code (alternative to scanning a QR).

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Device channel ID. |
| `phone` | string | yes | Phone number with country code (digits). Alias: phone_number. |

Notes:

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

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

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

Common errors: `invalid_channel_type`, `pairing_failed`, `gowa_error`, `validation_error`, `not_found`

### POST `/devices/{id}/reconnect`

- **Key:** `devices.reconnect`
- **URL:** `https://app.washeej.com/v1/devices/{id}/reconnect`
- **Scope:** `devices:manage`
- **Summary:** Reset a disconnected/orphaned device so it can be re-paired.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Device channel ID. |

Notes:

- After reconnect, generate a fresh QR or pairing code to complete the connection.

```bash
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"
```

Common errors: `invalid_channel_type`, `not_found`

### POST `/devices/{id}/default`

- **Key:** `devices.default`
- **URL:** `https://app.washeej.com/v1/devices/{id}/default`
- **Scope:** `devices:manage`
- **Summary:** Set a channel (any type) as the account default for sending.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Channel ID. |

```bash
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`

### DELETE `/devices/{id}`

- **Key:** `devices.destroy`
- **URL:** `https://app.washeej.com/v1/devices/{id}`
- **Scope:** `devices:manage`
- **Summary:** Delete any channel (Device, Cloud, Coexistence); frees one quota slot.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Channel ID (any type). |

Notes:

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

```bash
curl -X DELETE https://app.washeej.com/v1/devices/YOUR_DEVICE_ID \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET"
```

```json
{
    "ok": true,
    "data": {
        "deleted": true,
        "channel_type": "cloud",
        "quota_restored": true,
        "device_id": 52
    },
    "meta": {
        "request_id": "uuid"
    }
}
```

Common errors: `not_found`, `server_error`

### POST `/devices/cloud/onboarding-sessions`

- **Key:** `devices.onboarding.store`
- **URL:** `https://app.washeej.com/v1/devices/cloud/onboarding-sessions`
- **Scope:** `devices:manage`
- **Summary:** Create a hosted onboarding session to add a Cloud API (or Coexistence) number.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `return_url` | url | yes | 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 | no | Opaque CSRF token (max 128 chars) echoed back on the redirect. |
| `mode` | string | no | "cloud" (default) or "coexistence" (keeps the number active in the WhatsApp Business App; requires the feature to be enabled). |
| `developer_app_id` | integer | no | 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). |

Notes:

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

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

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

Common errors: `feature_disabled`, `limit_reached`, `return_url_not_allowed`, `validation_error`

### GET `/devices/cloud/onboarding-sessions/{id}`

- **Key:** `devices.onboarding.show`
- **URL:** `https://app.washeej.com/v1/devices/cloud/onboarding-sessions/{id}`
- **Scope:** `devices:manage`
- **Summary:** Check a hosted onboarding session status; returns the device on completion.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Session ID from the create call. |

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/devices/cloud/onboarding-sessions/SESSION_ID
```

```json
{
    "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`

### GET `/webhooks`

- **Key:** `webhooks.index`
- **URL:** `https://app.washeej.com/v1/webhooks`
- **Scope:** `messages:webhook`
- **Summary:** List registered webhook endpoints.

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/webhooks
```

### POST `/webhooks`

- **Key:** `webhooks.store`
- **URL:** `https://app.washeej.com/v1/webhooks`
- **Scope:** `messages:webhook`
- **Summary:** Register a webhook endpoint. Secret is returned once.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | yes | Human-readable label (max 100). |
| `url` | url | yes | HTTPS URL that accepts POST JSON. |
| `events` | string[] | no | Event names to subscribe. Empty/null = all events. |

```bash
curl -X POST https://app.washeej.com/v1/webhooks \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Production",
    "url": "https://your-server.com/webhooks/washeej",
    "events": ["message.received", "message.delivered", "device.connected"]
  }'
```

### PATCH `/webhooks/{id}`

- **Key:** `webhooks.update`
- **URL:** `https://app.washeej.com/v1/webhooks/{id}`
- **Scope:** `messages:webhook`
- **Summary:** Update label, URL, or subscribed events.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Webhook ID. |
| `label` | string | no | New label. |
| `url` | url | no | New callback URL. |
| `events` | string[] | no | Replace subscribed events. |

```bash
curl -X PATCH https://app.washeej.com/v1/webhooks/12 \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"events": ["message.received", "message.failed"]}'
```

### DELETE `/webhooks/{id}`

- **Key:** `webhooks.destroy`
- **URL:** `https://app.washeej.com/v1/webhooks/{id}`
- **Scope:** `messages:webhook`
- **Summary:** Delete a webhook endpoint.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Webhook ID. |

```bash
curl -X DELETE https://app.washeej.com/v1/webhooks/12 \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET"
```

### POST `/webhooks/{id}/test`

- **Key:** `webhooks.test`
- **URL:** `https://app.washeej.com/v1/webhooks/{id}/test`
- **Scope:** `messages:webhook`
- **Summary:** Enqueue a webhook.test delivery to your endpoint.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Webhook ID. |

```bash
curl -X POST https://app.washeej.com/v1/webhooks/12/test \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET"
```

### GET `/webhooks/{id}/deliveries`

- **Key:** `webhooks.deliveries`
- **URL:** `https://app.washeej.com/v1/webhooks/{id}/deliveries`
- **Scope:** `messages:webhook`
- **Summary:** List delivery logs for a webhook.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | yes | Webhook ID. |

```bash
curl -H "client-id: YOUR_CLIENT_ID" \
     -H "client-secret: YOUR_CLIENT_SECRET" \
     https://app.washeej.com/v1/webhooks/12/deliveries
```

### POST `/webhooks/deliveries/{deliveryId}/resend`

- **Key:** `webhooks.resend`
- **URL:** `https://app.washeej.com/v1/webhooks/deliveries/{deliveryId}/resend`
- **Scope:** `messages:webhook`
- **Summary:** Retry a failed delivery.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `deliveryId` | integer | yes | Delivery log ID. |

```bash
curl -X POST https://app.washeej.com/v1/webhooks/deliveries/901/resend \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET"
```

## Guides

### Integration Playbook

- **Key:** `integration-playbook`
- **Summary:** End-to-end SaaS path: auth, connect a channel, pick the right send API, register webhooks, production checklist.

#### Recommended order

- Authenticate with client-id/secret (+ IP whitelist) or OAuth Connect.
- Connect Cloud (hosted onboarding) and/or Device/QR via /devices.
- Always pass whatsapp_account_id in multi-number accounts.
- Use POST /messages inside the Cloud 24h window; POST /templates/send outside it.
- Register webhooks before go-live and verify HMAC signatures.

### Channel Selection

- **Key:** `channel-selection`
- **Summary:** Select the sending WhatsApp number with whatsapp_account_id (preferred) or from_number.

#### Priority

- whatsapp_account_id → exact channel from GET /devices.
- from_number → digits-only match against linked phone numbers.
- Otherwise a default/first available channel is used (Cloud preferred for templates).
- Templates and interactive Cloud features require a Cloud/Coexistence channel.

### Session Window vs Templates

- **Key:** `session-window`
- **Summary:** Cloud free-form messaging is limited to ~24h after the customer’s last inbound message; otherwise use approved templates.

#### Rules

- Open session (Cloud) → POST /messages (text/media/interactive/location).
- Closed session (Cloud) → POST /templates/send.
- Device/QR → session messages without Meta template requirement.
- Track sessions per contact AND WhatsApp number using message.received webhooks.

### Session Media

- **Key:** `session-media`
- **Summary:** Send session image/document/video/audio via multipart fields or public *_url fields.

#### Fields

- Upload: image, document, video, audio.
- URL: image_url, document_url, video_url, audio_url (+ optional filename for documents).
- URLs must be public HTTPS; Washeej downloads them server-side.
- Outside Cloud 24h use Template Media Headers instead.

### Interactive Messages & Location

- **Key:** `interactive-messages`
- **Summary:** Cloud-only CTA URL, interactive lists, location pins, and commerce product/order cards.

#### API fields

- cta_url_id and interactive_list_id reference dashboard-created assets.
- latitude + longitude (+ optional name/address) send a location pin.
- product / created_order_data JSON build commerce CTA cards.
- Requires open Cloud session; Device channels return capability_error for Cloud-only types.

### Template Media Headers (DOCUMENT / IMAGE / VIDEO)

- **Key:** `template-media-headers`
- **Summary:** Send native WhatsApp file/image/video bubbles from approved Cloud templates using header_media_url or multipart upload. Do not put the file URL in body_variables if you want a media bubble.

#### Choose the right pattern

- Native PDF/document bubble → template header DOCUMENT + header_media_url or header_media_file.
- Native image/video → header IMAGE or VIDEO + same media fields.
- Clickable URL in chat text → no media header; put the URL in body_variables (see Dynamic Invoice Link).
- Same sample every time → omit media fields and rely on the template sample uploaded at creation.

#### How DOCUMENT delivery works

- Approve a Cloud template with header_format DOCUMENT (sample PDF for Meta review).
- At send time pass a public HTTPS URL (header_media_url) or upload bytes (header_media_file / media_file).
- Meta fetches the URL server-to-server and delivers a WhatsApp document. End users see a file, not the raw URL.

#### Media URL requirements

- HTTPS, publicly reachable on GET, HTTP 200 with binary body (query-string download endpoints are fine).
- Preferred Content-Type application/pdf for documents; matching image/video MIME otherwise.
- Size limits: document 100MB, image 5MB, video 16MB.
- Prefer .pdf in the path or Content-Disposition filename. Signed URLs must still be valid at send time.
- Auth walls / HTML login or error pages cause Meta media download failures.

#### Scenario A — Dynamic PDF via public URL (JSON)

```bash
curl -X POST https://app.washeej.com/v1/templates/send \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "YOUR_TEMPLATE_ID",
    "mobile_code": "966",
    "mobile": "500000000",
    "whatsapp_account_id": YOUR_CLOUD_ACCOUNT_ID,
    "header_media_url": "https://files.example.com/docs/INV-1001.pdf",
    "body_variables": ["Ahmed", "INV-1001"]
  }'
```

#### Scenario B — Upload PDF at send time (multipart)

```bash
curl -X POST https://app.washeej.com/v1/templates/send \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -F "template_id=YOUR_TEMPLATE_ID" \
  -F "mobile_code=966" \
  -F "mobile=500000000" \
  -F "whatsapp_account_id=YOUR_CLOUD_ACCOUNT_ID" \
  -F "header_media_file=@/path/to/invoice.pdf;type=application/pdf" \
  -F "body_variables[]=Ahmed" \
  -F "body_variables[]=INV-1001"
```

#### Scenario C — DOCUMENT + dynamic body text

- Use DOCUMENT header plus body placeholders such as: Hello {{1}}, invoice {{2}} is attached.
- Send both header_media_url (or file) and ordered body_variables.
- Meta forbids variables as the first or last characters of the copy.

#### Scenario D — IMAGE or VIDEO headers

- Same API fields; only approved header_format changes.
- IMAGE max 5MB; VIDEO (MP4) max 16MB; DOCUMENT max 100MB.

#### Scenario E — URL in message text

- Do not use a DOCUMENT header. Pass the URL in body_variables on a text template.
- See guide: Dynamic Invoice Link.

#### Scenario F — Free-form document inside 24h window

- Inside the customer service window you may use POST /messages with a document.
- Outside that window WhatsApp requires an approved template — use DOCUMENT header + header_media_url.

#### Legacy Cloud API

- Same fields on POST /external-api/inbox/send-template-message.
- Prefer POST /v1/templates/send for new integrations.

#### Troubleshooting

- URL visible in text → you used body_variables on a text template; switch to DOCUMENT + header_media_url.
- Meta media errors → URL not publicly downloadable, HTML error page, wrong MIME, or oversized file.
- Same sample every time → media fields omitted at send time.
- capability_error → sending from Device/QR; use a Cloud whatsapp_account_id.
- template not_found → wrong id, not approved, or wrong Cloud account.

### Dynamic Invoice Link (URL in body text)

- **Key:** `dynamic-invoice-link`
- **Summary:** Put a per-customer HTTPS URL inside approved UTILITY template body text. Customer sees a clickable link, not a native file bubble.

#### When to use

- ERP hosts a download page / attachment API and you want the customer to open that URL in-chat.
- Template has no DOCUMENT/IMAGE/VIDEO header (or media is intentionally static).
- For a native PDF bubble use Template Media Headers instead.

#### Template body example

```text
Dear Customer, Your invoice: {{1}} Thank you.
```

#### Send via API

```bash
curl -X POST https://app.washeej.com/v1/templates/send \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "YOUR_TEMPLATE_ID",
    "mobile_code": "966",
    "mobile": "500000000",
    "whatsapp_account_id": YOUR_ACCOUNT_ID,
    "body_variables": [
      "https://files.example.com/invoices/attachment?id=UUID"
    ]
  }'
```

#### URL button alternative

- Keep body static and put the dynamic segment in a URL button path/query (domain itself cannot be variable).

### Authentication OTP Templates

- **Key:** `authentication-otp`
- **Summary:** Send AUTHENTICATION templates with exactly one 4–8 digit OTP in body_variables.

#### Rules

- Template category must be AUTHENTICATION on a Cloud channel.
- body_variables must contain exactly one numeric code (4–8 digits).
- Washeej attaches the OTP button parameter when the template requires copy-code / one-tap.
- Invalid payloads return validation_error.

#### Example

```bash
curl -X POST https://app.washeej.com/v1/templates/send \
  -H "client-id: YOUR_CLIENT_ID" \
  -H "client-secret: YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "YOUR_AUTH_TEMPLATE_ID",
    "mobile_code": "966",
    "mobile": "500000000",
    "whatsapp_account_id": YOUR_CLOUD_ACCOUNT_ID,
    "body_variables": ["482913"]
  }'
```

## Webhook events

### `message.received`

A new inbound message was received (Cloud API or Device/QR channel).

### `message.sent`

An outbound message was accepted and sent from your account.

### `message.delivered`

An outbound message was delivered to the recipient device.

### `message.read`

An outbound message was read by the recipient.

### `message.failed`

An outbound message failed to send or deliver.

### `device.connected`

A Device/QR channel connected successfully (WhatsApp linked).

### `device.disconnected`

A Device/QR channel lost its WhatsApp connection.

### `device.status_changed`

Any channel connection-status transition (payload includes from_status and to_status). Fired in addition to device.connected/device.disconnected.


---
Generated from `config/api_v1_documentation.php`. Prefer this file over scraping the HTML UI.
