QE AI Gateway API
A simple REST API for your apps and AI agents to manage contacts, tags, custom fields and messages on QE AI.
Every endpoint lives under /v1. All requests and responses are JSON. Successful responses are wrapped as { "ok": true, "data": … }.
Authentication
Authenticate every request (except /v1/health) with a Bearer QE AI API key in the Authorization header. Keys look like qeai_sk_… and are created in the key portal.
Authorization: Bearer qeai_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Each key is scoped to one Business and carries a set of scopes — contacts, tags, messages. A call outside a key's scope returns QE_SCOPE_DENIED. Keep keys secret; anyone with a key can act as that business.
Quick start
Find a contact by phone:
# 1. Search for a contact curl https://gateway.qeofficial.com/v1/contacts/search?phone=60123456789 \ -H "Authorization: Bearer qeai_sk_YOUR_KEY" # 2. Tag them curl -X POST https://gateway.qeofficial.com/v1/contacts/ct_abc123/tags \ -H "Authorization: Bearer qeai_sk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"tags":["vip"]}'
Concepts & IDs
| Term | Meaning |
|---|---|
| Contact | A person. IDs are prefixed ct_. |
| Business | Your account/workspace. IDs are prefixed biz_. |
| QE AI API key | The qeai_sk_ token you send as a Bearer credential. |
| fields | Custom fields on a contact, returned as a flat { key: value } object. |
Contact object
{
"id": "ct_abc123",
"firstName": "Aisha",
"lastName": "Rahman",
"email": "aisha@example.com",
"phone": "+60123456789",
"tags": ["vip"],
"fields": { "company": "Acme" },
"businessId": "biz_xxx",
"createdAt": "2026-09-16T02:00:00.000Z",
"updatedAt": "2026-09-16T02:00:00.000Z"
}
Errors
Errors return the matching HTTP status and a stable machine code:
{ "error": { "code": "QE_VALIDATION", "message": "…", "status": 400 } }
| Code | Status | Meaning |
|---|---|---|
QE_UNAUTHORIZED | 401 | Missing or invalid API key. |
QE_SCOPE_DENIED | 403 | Key lacks the scope for this endpoint. |
QE_BUSINESS_MISMATCH | 403 | Resource belongs to another business. |
QE_NOT_FOUND | 404 | Resource or route not found. |
QE_VALIDATION | 400 | Request body/params were rejected. |
QE_RATE_LIMIT | 429 | Too many requests — retry later. |
QE_UPSTREAM_ERROR | 502 | Service temporarily unavailable. |
QE_INTERNAL | 500 | Unexpected server error. |
POST/v1/contacts
Create or update a contact (upsert by phone/email). scope: contacts
Body
| Field | Type | Notes |
|---|---|---|
phone | string | Required if no email. |
email | string | Required if no phone. |
firstName, lastName, name | string | Optional. |
tags | string[] | Optional. |
fields | object | Optional custom fields, { key: value }. |
curl -X POST …/v1/contacts -H "Authorization: Bearer qeai_sk_…" \ -H "Content-Type: application/json" \ -d '{"phone":"+60123456789","firstName":"Aisha","tags":["lead"]}' → 201 { "ok": true, "data": { "id": "ct_…", … } }
GET/v1/contacts/search
Search contacts. scope: contacts
| Query | Notes |
|---|---|
phone | Search by phone. |
email | Search by email. |
q | Free-text search. |
limit | Max results (default 20). |
curl "…/v1/contacts/search?phone=60123456789" -H "Authorization: Bearer qeai_sk_…" → 200 { "ok": true, "data": { "contacts": [ … ], "total": 1 } }
GET/v1/contacts/{id}
Fetch one contact by its ct_ id. scope: contacts
curl …/v1/contacts/ct_abc123 -H "Authorization: Bearer qeai_sk_…" → 200 { "ok": true, "data": { "id": "ct_abc123", … } }
PATCH/v1/contacts/{id}
Update fields on a contact. Send only what changes. scope: contacts
curl -X PATCH …/v1/contacts/ct_abc123 -H "Authorization: Bearer qeai_sk_…" \ -H "Content-Type: application/json" -d '{"email":"new@example.com"}'
POST DELETE/v1/contacts/{id}/tags
Add or remove tags. scope: tags
# add curl -X POST …/v1/contacts/ct_abc123/tags -H "Authorization: Bearer qeai_sk_…" \ -H "Content-Type: application/json" -d '{"tags":["vip","paid"]}' # remove curl -X DELETE …/v1/contacts/ct_abc123/tags -H "Authorization: Bearer qeai_sk_…" \ -H "Content-Type: application/json" -d '{"tags":["vip"]}'
GET/v1/businesses/{id}/fields
List the custom fields defined for a business. scope: contacts
curl …/v1/businesses/biz_xxx/fields -H "Authorization: Bearer qeai_sk_…" → 200 { "ok": true, "data": { "fields": [ { "key":"company","name":"Company","type":"TEXT" } ] } }
POST/v1/businesses/{id}/fields
Create a new custom field. scope: contacts
| Field | Type | Notes |
|---|---|---|
name | string | Required. |
type | string | Optional, defaults to TEXT. |
curl -X POST …/v1/businesses/biz_xxx/fields -H "Authorization: Bearer qeai_sk_…" \ -H "Content-Type: application/json" -d '{"name":"Company"}'
POST/v1/messages
Send a message to a contact. scope: messages
| Field | Type | Notes |
|---|---|---|
contactId | string | Required, ct_ id. |
type | string | Required — SMS, WhatsApp, or Email. |
message | string | Required. |
curl -X POST …/v1/messages -H "Authorization: Bearer qeai_sk_…" \ -H "Content-Type: application/json" \ -d '{"contactId":"ct_abc123","type":"WhatsApp","message":"Hi Aisha!"}' → 201 { "ok": true, "data": { "contactId":"ct_abc123","type":"WhatsApp","status":"sent" } }
GET/v1/health
Liveness check. No authentication required.
curl …/v1/health
→ 200 { "ok": true, "service": "QE AI Developer Gateway", "version": "5.0.0", "timestamp": "…" }
qeai_sk_ key, and these endpoints. It never needs to know anything about the platform underneath — QE AI is the whole surface.