API Docs
Integrate external apps with CEOX
GuestU

Integrate external apps with the CEOX WhatsApp API. Authenticate every request with an API key (create one under API Keys).

Authentication

Send your key in the Authorization header on every request:

Authorization: ceox_YOUR_KEY

No Bearer prefix needed for API keys. Keep keys secret — anyone with a key has full workspace access. Revoke a leaked key from the API Keys page.

Access model. API keys have full workspace access. The user-management, roles and inbox endpoints below are also reachable with a logged-in user's JWT (Authorization: Bearer <jwt>), but only if that user's role holds the required capability (noted per endpoint). Inbox membership scopes what data a user sees: members of an inbox see only its devices and conversations, unless their role has inbox.view_all.

Base URL

https://api-wa.fantecno.net/api/v1

Example

curl -H "Authorization: ceox_YOUR_KEY" \
  https://api-wa.fantecno.net/api/v1/conversations

Endpoints

GET/conversations
List inbox conversations (one row per contact PER DEVICE — chats from the same person to different numbers are separate threads). Each row includes device_id, device_name, device_phone of the WhatsApp number it belongs to, plus last message, unread, status.
GET/conversations/{id}/messages
Full message thread for a single conversation (this device only).
POST/conversations/{id}/messages
Send a message. Body fields below.
{
  "Body": "Hello!",            // text (optional if media)
  "MediaBase64": "",           // base64 file (optional)
  "MediaMime": "image/jpeg",   // required with media
  "MediaFilename": "pic.jpg",  // optional
  "ReplyTo": ""                // wa_message_id to quote (optional)
}
POST/conversations/{id}/read
Mark a conversation as read (clears unread).
POST/conversations/{id}/status
Close (solved) or reopen (open) a conversation. Requires chats.send. Solved conversations are hidden from the default inbox list but remain under the Solved filter and can be reopened.
{
  "status": "solved"   // or "open" to reopen
}
POST/messages/send
Send a TEXT message to a phone number (creates contact/conversation if needed). To control WHICH of your WhatsApp numbers it is sent FROM, pass device_id (from GET /devices) or from (a device wa_jid / phone). Omit both to use any connected device. A requested device that is not connected returns 502 (the message is never sent from the wrong number).
{
  "to": "+6281234567890",   // E.164 phone (required)
  "body": "Hello!",         // message text (required)
  "device_id": 2,            // optional: send FROM this device (see GET /devices)
  "from": "",                // optional alt: device wa_jid or phone to send from
  "name": "Budi"             // optional: contact name if the number is NEW (else the number is used)
}
POST/messages/send
Send an IMAGE. Provide the image as base64 with an image/* mime; "body" becomes the caption. device_id/from select the sending number (same as text).
{
  "to": "+6281234567890",
  "media_base64": "<base64 of the image>",
  "media_mime": "image/jpeg",   // image/png, image/webp, ...
  "media_filename": "photo.jpg",
  "body": "optional caption",
  "device_id": 2               // optional: send FROM this device
}
POST/messages/send
Send a DOCUMENT (PDF, etc). Use the file mime; media_filename is shown to the recipient. device_id/from select the sending number (same as text).
{
  "to": "+6281234567890",
  "media_base64": "<base64 of the file>",
  "media_mime": "application/pdf",
  "media_filename": "invoice.pdf",
  "body": "optional caption",
  "device_id": 2               // optional: send FROM this device
}
GET/webhooks
List webhook endpoints.
POST/webhooks
Register a webhook. Body: {"url","secret"}. CEOX POSTs each inbound message to url; if secret is set, the body is signed (HMAC-SHA256) in header X-CEOX-Signature: sha256=<hex>.
// webhook delivery payload (POSTed to your url):
{
  "event": "message.received",
  "conversation_id": 12,
  "from": "+6281234567890",
  "name": "Budi",
  "wa_message_id": "3EB0...",
  "body": "hi",
  "media_type": "image",      // "" if text
  "media_url": "/media/ab12.jpg",
  "is_group": false,
  "timestamp": "2026-06-21T08:15:00Z"
}
DELETE/webhooks/{id}
Delete a webhook.
GET/contacts
List contacts.
POST/contacts
Create a contact. Body: {"name","phone_e164","company","segment"}.
GET/devices
List paired WhatsApp devices and their status (includes inbox_id).
GET/wa/status
WhatsApp transport + pairing + device status.
GET/users
List users (id, name, email, status, role, role_id, inbox_ids[]).
POST/users
Create a user. Requires team.manage.
{
  "name": "Budi",
  "email": "budi@acme.co",
  "password": "minimal8",   // >= 8 chars
  "role_id": 3,              // see GET /roles
  "inbox_ids": [1, 2]        // inbox memberships (access)
}
PATCH/users/{id}
Update a user (any subset). Requires team.manage. Guarded so the last team.manage user is never stripped.
{
  "name": "Budi S.",
  "email": "budi@acme.co",
  "role_id": 2,
  "status": "online",       // online | idle | offline
  "inbox_ids": [1]           // replaces memberships
}
DELETE/users/{id}
Delete a user. Requires team.manage. Cannot delete yourself or the last team.manage user.
POST/users/{id}/password
Admin reset a user password. Requires team.manage.
{
  "password": "minimal8"
}
POST/me/password
Change your OWN password (any logged-in user). Verifies current password.
{
  "current_password": "old",
  "new_password": "minimal8"
}
GET/roles
List roles (id, name, is_system, capabilities[]). Capabilities: chats.send, broadcasts.manage, contacts.write, devices.manage, analytics.view, team.manage, inbox.manage, inbox.view_all.
POST/roles
Create a custom role. Requires team.manage.
{
  "name": "Supervisor"
}
PATCH/roles/{id}
Rename a role and/or replace its capability set. System roles cannot be renamed (capabilities still editable). Requires team.manage.
{
  "name": "Supervisor",          // optional (non-system only)
  "capabilities": ["chats.send", "analytics.view"]  // replaces the set
}
DELETE/roles/{id}
Delete a custom role (409 if assigned to users; system roles cannot be deleted). Requires team.manage.
GET/inboxes
List inboxes (id, name, description, color, member_count, device_count). Requires inbox.manage.
POST/inboxes
Create an inbox. Requires inbox.manage.
{
  "name": "Sales",
  "description": "Sales team",  // optional
  "color": "#16a34a"             // optional UI accent
}
PATCH/inboxes/{id}
Update an inbox (any subset of name/description/color). Requires inbox.manage.
{
  "name": "Sales APAC"
}
DELETE/inboxes/{id}
Delete an inbox (its devices become unassigned). Requires inbox.manage.
GET/inboxes/{id}/members
List member users of an inbox. Requires inbox.manage.
POST/inboxes/{id}/members
Add a user as a member (grants access to this inbox's devices/chats). Requires inbox.manage.
{
  "user_id": 5
}
DELETE/inboxes/{id}/members/{userId}
Remove a member from an inbox. Requires inbox.manage.
POST/inboxes/{id}/devices
Assign a device to this inbox (a device belongs to one inbox). Requires inbox.manage.
{
  "device_id": 2
}