REST API for integrating external systems, websites, and automation tools.
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer YOUR_PUBLIC_API_TOKEN
Log into see and pre-fill your workspace's token. You can also find it in Settings → API Settings.
Configure allowed origins in Settings → API Settings to restrict API access to specific domains. Requests from non-whitelisted origins will be rejected with 403.
Leaving the whitelist empty allows any origin. Once you add even one domain, requests without an Origin header — which is what curl sends by default — are also rejected. Add the header when testing from the terminal:
-H 'Origin: https://yourdomain.com'
Configure webhook URLs in Settings to receive real-time event notifications. Payloads are sent as JSON POST requests.
/api/client/contactsPublic API TokenCreate a new contact or update an existing one by phone number (upsert).
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Contact full name. |
| phone | string | required | Phone number (digits). Used as the upsert/dedupe key. |
| string | optional | Contact email address. | |
| ic_number | string | optional | National ID / IC number. |
| status | enum | optional | dormant | very_cold | cold | warm | hot | customer (default: cold). |
| branch_slug | string | optional | Slug of the branch to route the contact to. |
| source | enum | optional | manual | api | n8n | form (default: api). |
Example Request
curl -X POST 'https://your-crm-domain.com/api/client/contacts' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"name":"John Doe","phone":"60123456789","email":"[email protected]","ic_number":"901231-14-5555","status":"cold","branch_slug":"kl-branch","source":"api"}'Request Body
{
"name": "John Doe",
"phone": "60123456789",
"email": "[email protected]",
"ic_number": "901231-14-5555",
"status": "cold",
"branch_slug": "kl-branch",
"source": "api"
}Response
{
"success": true,
"contact_id": "uuid",
"status": "cold"
}Error Responses
/api/client/appointmentsPublic API TokenCreate a new appointment/booking. Validates blocked dates and operational hours.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Phone used to find or create the contact. |
| name | string | optional | Contact name — used to create a contact when phone is not found. |
| string | optional | Contact email address. | |
| appointment_date | string | required | YYYY-MM-DD. |
| appointment_time | string | required | HH:MM (24-hour). |
| duration_minutes | number | optional | Booking length in minutes (min 15, default 30). |
| notes | string | optional | Internal note for the booking. |
| branch_slug | string | optional | Slug of the branch to route the booking to. |
| source | enum | optional | manual | api | n8n | form. |
Example Request
curl -X POST 'https://your-crm-domain.com/api/client/appointments' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"phone":"60123456789","name":"John Doe","email":"[email protected]","appointment_date":"2026-08-15","appointment_time":"10:00","duration_minutes":60,"notes":"Service request","branch_slug":"kl-branch","source":"api"}'Request Body
{
"phone": "60123456789",
"name": "John Doe",
"email": "[email protected]",
"appointment_date": "2026-08-15",
"appointment_time": "10:00",
"duration_minutes": 60,
"notes": "Service request",
"branch_slug": "kl-branch",
"source": "api"
}Response
{
"success": true,
"appointment_id": "uuid",
"appointment_code": "APT-2026-0001"
}Error Responses
/api/client/appointments/availabilityPublic API TokenReturns available time slots for a given date. Checks blocked days, public holidays, operational hours, and existing bookings.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| date | query | required | Date to check (YYYY-MM-DD). |
| branch_slug | query | optional | Filter by branch slug. Omit to check HQ-level availability. |
| slot_minutes | query | optional | Slot duration in minutes (15–120, default 30). |
Example Request
curl -X GET 'https://your-crm-domain.com/api/client/appointments/availability?date=2026-08-21' \ -H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN'
Response
{
"date": "2026-08-21",
"available": true,
"operational_hours": {
"start": "09:00",
"end": "18:00"
},
"slot_minutes": 30,
"slots": [
{ "time": "09:00", "available": true },
{ "time": "09:30", "available": true },
{ "time": "10:00", "available": false },
{ "time": "10:30", "available": true },
{ "time": "11:00", "available": true }
]
}Error Responses
/api/client/inquiriesPublic API TokenCreate a new inquiry or lead linked to a contact.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Phone used to find or create the contact. |
| name | string | optional | Contact name — used to create a contact when phone is not found. |
| string | optional | Contact email address. | |
| subject | string | optional | Inquiry subject. |
| notes | string | optional | Inquiry notes / details. |
| branch_slug | string | optional | Slug of the branch to route the inquiry to. |
| source | enum | optional | manual | api | n8n | form. |
Example Request
curl -X POST 'https://your-crm-domain.com/api/client/inquiries' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"phone":"60123456789","name":"John Doe","email":"[email protected]","subject":"Interested in Jet Ski rental","notes":"Looking for weekend packages","branch_slug":"kl-branch","source":"api"}'Request Body
{
"phone": "60123456789",
"name": "John Doe",
"email": "[email protected]",
"subject": "Interested in Jet Ski rental",
"notes": "Looking for weekend packages",
"branch_slug": "kl-branch",
"source": "api"
}Response
{
"success": true,
"inquiry_id": "uuid",
"inquiry_code": "INQ-2026-0001"
}Error Responses
/api/client/quotationsPublic API TokenCreate a new quotation with line items. Contact is auto-created or matched by billing phone.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | optional | Shown on quotation detail, print, and CSV export. |
| billing.name | string | required | Billing contact name. |
| billing.phone | string | required | Billing phone — used to match or create the contact. |
| billing.email | string | optional | Billing email address. |
| billing.company_name | string | optional | Billing company name. |
| billing.address_line_1 | string | optional | Billing address line 1. |
| billing.address_line_2 | string | optional | Billing address line 2. |
| billing.city | string | optional | Billing city. |
| billing.state | string | optional | Billing state. |
| billing.postcode | string | optional | Billing postcode. |
| billing.country | string | optional | Billing country. |
| shipping.name | string | required | Shipping contact name. |
| shipping.phone | string | required | Shipping phone. |
| shipping.address | string | optional | Shipping address (single line). |
| shipping.address_line_1 | string | optional | Shipping address line 1. |
| shipping.address_line_2 | string | optional | Shipping address line 2. |
| shipping.city | string | optional | Shipping city. |
| shipping.state | string | optional | Shipping state. |
| shipping.postcode | string | optional | Shipping postcode. |
| shipping.country | string | optional | Shipping country. |
| shipping.date_time | string | optional | Preferred shipping/delivery date & time. |
| line_items[].item_type | enum | optional | product | service | package | custom. |
| line_items[].item_name | string | required | Line item name. |
| line_items[].description | string | optional | Line item description. |
| line_items[].quantity | number | required | Quantity (>= 0). |
| line_items[].unit | string | optional | Unit of measure (e.g. hour, unit). |
| line_items[].unit_price | number | required | Unit price (>= 0). |
| line_items[].line_total | number | required | Total for the line (>= 0). |
| line_items[].metadata | object | optional | Free-form key/value metadata. |
| subtotal | number | required | Sum of line totals (>= 0). |
| other_charges[].charge_type | string | optional | Charge type/label. |
| other_charges[].description | string | optional | Charge description. |
| other_charges[].amount | number | optional | Charge amount (>= 0). |
| total_amount | number | required | Grand total including charges (>= 0). |
| notes | string | optional | Quotation notes. |
| terms | string | optional | Quotation terms & conditions. |
| valid_until | string | optional | Expiry date (YYYY-MM-DD). |
| discount | number | optional | Total discount amount (>= 0, default 0). |
| tax | number | optional | Total tax amount (>= 0, default 0). |
| branch_slug | string | optional | Slug of the branch to route the quotation to. |
| custom_fields | object | optional | Free-form custom field values. |
Example Request
curl -X POST 'https://your-crm-domain.com/api/client/quotations' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"title":"Jet Ski Rental Package","billing":{"name":"John Doe","phone":"60123456789","email":"[email protected]","company_name":"Acme Sdn Bhd","address_line_1":"123 Main St","address_line_2":"Unit 2","city":"Kuala Lumpur","state":"WP","postcode":"50000","country":"Malaysia"},"shipping":{"name":"John Doe","phone":"60123456789","address":"123 Main St","address_line_1":"123 Main St","address_line_2":"Unit 2","city":"Kuala Lumpur","state":"WP","postcode":"50000","country":"Malaysia","date_time":"2026-08-15 10:00"},"line_items":[{"item_type":"service","item_name":"Jet Ski Rental – 1 Hour","description":"Includes life vest and briefing","quantity":2,"unit":"hour","unit_price":150,"line_total":300,"metadata":{}}],"subtotal":300,"other_charges":[{"charge_type":"Delivery","description":"On-site delivery","amount":18}],"total_amount":318,"discount":0,"tax":18,"notes":"Please arrive 15 minutes early.","terms":"Full payment due before delivery.","valid_until":"2026-09-01","branch_slug":"kl-branch","custom_fields":{}}'Request Body
{
"title": "Jet Ski Rental Package",
"billing": {
"name": "John Doe",
"phone": "60123456789",
"email": "[email protected]",
"company_name": "Acme Sdn Bhd",
"address_line_1": "123 Main St",
"address_line_2": "Unit 2",
"city": "Kuala Lumpur",
"state": "WP",
"postcode": "50000",
"country": "Malaysia"
},
"shipping": {
"name": "John Doe",
"phone": "60123456789",
"address": "123 Main St",
"address_line_1": "123 Main St",
"address_line_2": "Unit 2",
"city": "Kuala Lumpur",
"state": "WP",
"postcode": "50000",
"country": "Malaysia",
"date_time": "2026-08-15 10:00"
},
"line_items": [
{
"item_type": "service",
"item_name": "Jet Ski Rental – 1 Hour",
"description": "Includes life vest and briefing",
"quantity": 2,
"unit": "hour",
"unit_price": 150.00,
"line_total": 300.00,
"metadata": {}
}
],
"subtotal": 300.00,
"other_charges": [
{
"charge_type": "Delivery",
"description": "On-site delivery",
"amount": 18.00
}
],
"total_amount": 318.00,
"discount": 0,
"tax": 18.00,
"notes": "Please arrive 15 minutes early.",
"terms": "Full payment due before delivery.",
"valid_until": "2026-09-01",
"branch_slug": "kl-branch",
"custom_fields": {}
}Response
{
"success": true,
"quotation_id": "uuid",
"quotation_number": "QT-2026-0001",
"status": "pending_review",
"saved_to": "branch",
"branch_slug": "kl-branch",
"message": "Quotation created and is ready for admin review."
}Error Responses
/api/client/warranty/lookupPublic API TokenLook up warranties by serial number or by the registered email address.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| type | enum | required | serial_number | email. |
| value | string | required | Serial number or email address to search for. |
Example Request
curl -X POST 'https://your-crm-domain.com/api/client/warranty/lookup' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"type":"serial_number","value":"SKI-2025-0001"}'Request Body
{
"type": "serial_number",
"value": "SKI-2025-0001"
}Response
{
"warranties": [
{
"warranty_code": "WRN-2025-0001",
"product_name": "Jet Ski Ultra 310",
"serial_number": "SKI-2025-0001",
"sku_number": "SKU-310-BLU",
"purchase_date": "2025-06-15",
"warranty_start": "2025-06-15",
"warranty_end": "2027-06-15",
"status": "active",
"contact_name": "John Doe",
"contact_email": "[email protected]"
}
]
}Error Responses
/api/client/warranty/activatePublic API TokenRegister a new warranty for a product by serial number.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Customer full name. |
| phone | string | required | Customer phone — used to match or create the contact. |
| string | optional | Customer email address. | |
| ic_number | string | optional | Customer IC number. |
| product_name | string | optional | Product name (defaults to "Self-registered product"). |
| product_id | string | optional | Product identifier / SKU model code. |
| serial_number | string | required | Unique product serial number. |
| sku_number | string | optional | Product SKU number. |
| purchase_date | string | required | Purchase date (YYYY-MM-DD). |
| notes | string | optional | Warranty notes. |
| branch_slug | string | optional | Slug of the branch to route the warranty to. |
Example Request
curl -X POST 'https://your-crm-domain.com/api/client/warranty/activate' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"name":"John Doe","phone":"60123456789","email":"[email protected]","ic_number":"901231-14-5555","product_name":"Jet Ski Ultra 310","product_id":"JS-ULTRA-310","serial_number":"SKI-2025-0001","sku_number":"SKU-310-BLU","purchase_date":"2025-06-15","notes":"Purchased at KL showroom.","branch_slug":"kl-branch"}'Request Body
{
"name": "John Doe",
"phone": "60123456789",
"email": "[email protected]",
"ic_number": "901231-14-5555",
"product_name": "Jet Ski Ultra 310",
"product_id": "JS-ULTRA-310",
"serial_number": "SKI-2025-0001",
"sku_number": "SKU-310-BLU",
"purchase_date": "2025-06-15",
"notes": "Purchased at KL showroom.",
"branch_slug": "kl-branch"
}Response
{
"success": true,
"warranty_code": "WRN-2026-0001"
}Error Responses
/api/client/whatsapp/messagesPublic API TokenReturns a chat's messages newest-first, with voice notes resolved to text. Address the chat by sender — the same identifiers the inbound webhook gives you. Use this to give an AI agent the conversation history before it replies.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| from_user_id | query | optional | Preferred. The sender's BSUID, exactly as it appears on the webhook at messages[].from_user_id (e.g. MY.1513614423794111). |
| phone | query | optional | Phone number in any format. Matched on the last 9 digits. |
| username | query | optional | Fallback. The sender's WhatsApp username, from messages[].username. |
| conversation_id | query | optional | Deprecated. A cnv_… id or the CRM thread uuid. Still accepted, but see the notes. |
| limit | query | optional | Messages per page, 1–100. Defaults to 10. |
| before | query | optional | Cursor from a previous response's next_cursor, to page further back in history. |
Example Request
curl -X GET 'https://your-crm-domain.com/api/client/whatsapp/messages?from_user_id=MY.1513614423794111&limit=10' \ -H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN'
Response
{
"matched_by": "from_user_id",
"data": [
{
"id": "msg_abc123",
"conversation_id": "cnv_abc123",
"direction": "inbound",
"type": "audio",
"content": "Hi, is the jet ski available this Saturday?",
"media_url": "https://…/voice.ogg",
"status": "read",
"sent_by": null,
"created_at": "2026-08-21T09:12:04Z",
"transcribed": true
}
],
"has_more": false,
"next_cursor": null
}Error Responses
/api/client/whatsapp/{phoneNumberId}/modePublic API TokenReturns whether a WhatsApp contact is handled by the AI agent or by a human in the CRM inbox. Call this before generating a reply.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| phoneNumberId | path | required | The WhatsApp Business phone number ID (kirim_phone_number_id). |
| phone | query | optional | Phone number in any format. Matched on the last 9 digits. |
| recipient_id | query | optional | Business-Scoped User ID (BSUID). Exact match. |
Example Request
curl -X GET 'https://your-crm-domain.com/api/client/whatsapp/{phoneNumberId}/mode?recipient_id=US.13491208655302741918' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN'Response
{
"mode": "ai",
"exists": true,
"contact_id": "uuid",
"name": "John Doe",
"phone": "60123456789",
"recipient_id": "US.13491208655302741918"
}Error Responses
/api/client/whatsapp/{phoneNumberId}/modePublic API TokenSwitches a contact between AI and human handling — use it to hand a conversation to staff when the agent cannot answer.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
| phoneNumberId | path | required | The WhatsApp Business phone number ID (kirim_phone_number_id). |
| phone | string | optional | Phone number in any format. Matched on the last 9 digits. |
| recipient_id | string | optional | Business-Scoped User ID (BSUID). Exact match. |
| mode | enum | required | ai | human. |
Example Request
curl -X PATCH 'https://your-crm-domain.com/api/client/whatsapp/{phoneNumberId}/mode' \
-H 'Authorization: Bearer YOUR_PUBLIC_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"phone":"60123456789","mode":"human"}'Request Body
{
"phone": "60123456789",
"mode": "human"
}Response
{
"mode": "human",
"contact_id": "uuid",
"name": "John Doe",
"phone": "60123456789",
"recipient_id": "US.13491208655302741918"
}Error Responses