HTTP API
Push leads into master. The engine does the rest.
CRM and other services use this API. It does not originate calls. Push upserts by unique oli_id. Progressive or Predictive then claims matching rows from the same master table.
Auth
Lead write endpoints require WS_SECRET. Send it as any one of:
| Header | Value |
|---|---|
Authorization | Bearer YOUR_WS_SECRET |
X-API-Key | YOUR_WS_SECRET |
X-Access-Key | YOUR_WS_SECRET |
Wrong or missing key returns 401 with {"ok":false,"error":"unauthorized"}. All write endpoints use this key: POST /api/leads, POST /api/master, PUT /api/escalated-tickets, and POST /api/send-callback-request. /healthz is public. Detailed /health uses the same key.
Endpoints
| Method | Path | Auth | What it does |
|---|---|---|---|
| POST | /api/leads | Yes | Upsert one lead or a batch by oli_id |
| POST | /api/master | Yes | Same handler as /api/leads |
| PUT | /api/escalated-tickets | Yes | Close (dispose) or schedule a callback for priority_id 8 |
| POST | /api/send-callback-request | Yes | Set callback time on an existing master oliId |
| GET | /healthz | No | AMI + DB liveness. No internals |
| GET | /health | Yes | Detailed AMI / metrics health |
| GET | /api/health | Yes | Same as /health |
There is no GET/PATCH/DELETE for leads. Push a ticket with POST /api/leads (or /api/master) and priority_id 8. After the call, CRM uses PUT /api/escalated-tickets — that path does not store the incoming status string. The dialer still claims and settles live rows itself.
Push leads
Body is JSON. One object upserts one row keyed by oli_id (unique). {"leads":[...]} upserts 1–500 rows in one request. Extra fields are rejected.
New oli_id inserts with dial-state defaults. An existing oli_id updates contact fields and resets dial state unless the row is currently Calling and reserved. In that case the API returns skipped_in_flight and leaves the live call alone. HTTP status is always 201 for a valid body. Each row in the response is oli_id, status (inserted, updated, or skipped_in_flight), and the raw callback_scheduled_at that was sent (null if omitted).
Single Pending lead
curl -sS -X POST https://YOUR_HOST/api/leads \
-H "Authorization: Bearer YOUR_WS_SECRET" \
-H "Content-Type: application/json" \
-d '{
"oli_id": "OLI123",
"phone_number": "9876543210",
"client_name": "Test Client",
"form_type": "GST",
"priority_id": 1
}'
201 response
{
"ok": true,
"leads": [
{ "oli_id": "OLI123", "status": "inserted", "callback_scheduled_at": null }
]
}
Batch
curl -sS -X POST https://YOUR_HOST/api/leads \
-H "Authorization: Bearer YOUR_WS_SECRET" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{
"oli_id": "OLI123",
"phone_number": "9876543210",
"client_name": "A",
"form_type": "GST",
"priority_id": 1
},
{
"oli_id": "OLI124",
"phone_number": "9876543211",
"client_name": "B",
"form_type": "GST",
"priority_id": 3,
"callback_scheduled_at": "2026-08-20 15:30:00"
}
]
}'
Dial-state reset
Every successful insert, update, Close, Callback Schedule, or send-callback-request writes these master columns back to a fresh cycle. A live Calling + reserved row is never touched.
| Column | Push insert / update | Close | Callback Schedule |
|---|---|---|---|
dial_status | Pending, or Callback when a callback time is sent on a callback priority | Disposed | Callback |
last_dial_result | NULL | NULL | NULL |
hangup_by | NULL | NULL | NULL |
hangup_cause | NULL | NULL | NULL |
last_attempt_at | NULL | NULL | NULL |
first_attempt_at | NULL | NULL | NULL |
next_retry_at | NULL inside working hours; otherwise next work_start. Callbacks stay NULL and wait on callback_scheduled_at | NULL | NULL |
callback_scheduled_at | New time, or NULL if omitted | NULL | From scheduledAt |
call_count | 0 | 0 | 0 |
isReserved | 0 | 0 | 0 |
isRnrSent | 0 | 0 | 0 |
Push also refreshes phone_number, client_name, form_type, priority_id, push_date, and status = 1. Extra keys (including agent_id) are rejected. Master does not store agent_id.
Fields
| Field | Required | Rules |
|---|---|---|
oli_id | Yes | String, 1–100 chars. Unique. Same id upserts. |
phone_number | Yes | 10–15 digits after stripping non-digits |
client_name | Yes | String, 1–150 chars |
form_type | Yes | Stored for CRM/logging. Not used to pick an agent. |
priority_id | Yes | Active row in hold_dialer_priority.id. Seeded 1 Hold Lead, 2 Hold + Not Connected, 3 Final Disposal (not dialable). |
callback_scheduled_at | No | Only on callback priorities (hold_dialer_priority.is_callback = 1, seeded as id 2). Datetime with time; past values are allowed (due immediately). |
priority_id must exist and be active or the API returns 400 unknown priority_id. Final Disposal is not dialable. Stored dial_status is Callback when the priority is a callback priority and a callback time is sent; otherwise Pending. Connected agent is recorded on hold_dialer_call_attempts at settle, not on master.
Escalated tickets
Separate from lead push. Same WS_SECRET. Extra JSON keys are rejected. The incoming status is not stored as a column. The master row must already have priority_id = 8 or the API returns 400. Unknown oliId returns 404. A live Calling + reserved row returns 409 and is not changed.
| Field | Required | Rules |
|---|---|---|
oliId | Yes | Existing master oli_id, 1–100 chars. Camel case (not oli_id). |
status | Yes | Close or Callback Schedule (spacing/case-insensitive). Not written to the database. |
scheduledAt | For Callback Schedule | Same datetime rules as callback_scheduled_at. Ignored on Close. |
Close copies the row to hold_dialer_disposed and deletes it from master. Callback Schedule sets dial_status = Callback and callback_scheduled_at from scheduledAt, and leaves priority_id at 8 so any idle agent can claim it when the time is due. Both apply the dial-state reset before Close archives.
curl -sS -X PUT https://YOUR_HOST/api/escalated-tickets \
-H "Authorization: Bearer YOUR_WS_SECRET" \
-H "Content-Type: application/json" \
-d '{
"oliId": "OLI123",
"status": "Close"
}'
curl -sS -X PUT https://YOUR_HOST/api/escalated-tickets \
-H "Authorization: Bearer YOUR_WS_SECRET" \
-H "Content-Type: application/json" \
-d '{
"oliId": "OLI123",
"status": "Callback Schedule",
"scheduledAt": "2026-08-21 16:30:00"
}'
200 responses
{ "ok": true, "lead": { "oli_id": "OLI123", "dial_status": "Disposed" } }
{ "ok": true, "lead": {
"oli_id": "OLI123",
"dial_status": "Callback",
"callback_scheduled_at": "2026-08-21 16:30:00"
} }
Send callback request
Same WS_SECRET. Body is only oliId and scheduledAt (alias ScheduledAt). Extra keys are rejected. The oliId must already exist in hold_dialer_master_data or the API returns 404. Unlike escalated tickets, any priority_id is allowed. A live Calling + reserved row returns 409.
Sets dial_status = Callback and callback_scheduled_at, keeps priority_id, and applies the same dial-state reset as Callback Schedule.
curl -sS -X POST https://YOUR_HOST/api/send-callback-request \
-H "Authorization: Bearer YOUR_WS_SECRET" \
-H "Content-Type: application/json" \
-d '{
"oliId": "OLI123",
"scheduledAt": "2026-08-21 16:30:00"
}'
200 response
{ "ok": true, "lead": {
"oli_id": "OLI123",
"dial_status": "Callback",
"callback_scheduled_at": "2026-08-21 16:30:00"
} }
Callback time
Accepted: 2026-08-20 15:30:00 or ISO-8601, including times already in the past (the lead is due immediately). Rejected: date-only (2026-08-20), empty invalid strings, and any time on a non-callback priority. Extra keys such as agent_id are rejected.
curl -sS -X POST https://YOUR_HOST/api/master \
-H "Authorization: Bearer YOUR_WS_SECRET" \
-H "Content-Type: application/json" \
-d '{
"oli_id": "OLI123",
"phone_number": "9876543210",
"client_name": "Test Client",
"form_type": "GST",
"priority_id": 2,
"callback_scheduled_at": "2026-08-20 15:30:00"
}'
On POST /api/leads, the engine waits on this time for callback priorities (hold_dialer_priority.is_callback = 1, seeded as id 2). A future callback_scheduled_at on any row (including priority 8 after Callback Schedule) is skipped until due. When due, any idle ready agent may claim it. Full calling behaviour is in the Callback Request guide.
Errors
| Status | When |
|---|---|
400 | Invalid JSON, failed field validation, unknown extra keys (including agent_id), unknown priority_id, callback time on a non-callback priority, or escalated ticket when priority_id is not 8 |
401 | Missing or wrong key |
404 | Unknown /api/... path, or unknown oliId on escalated tickets / send-callback-request |
409 | Escalated ticket or send-callback-request while the lead is Calling and reserved |
413 | Body larger than 1 MB |
429 | More than 120 writes from this IP in 60 seconds on escalated tickets or send-callback-request (lead push is not rate-limited) |
500 | Unexpected server error |
Validation shape:
{
"ok": false,
"error": "validation_failed",
"details": [
{ "field": "phone_number", "message": "phone_number must be 10-15 digits" }
]
}
Health
/healthz is public and returns {"ok":true,"ami":true,"db":true} (booleans only). Detailed /health and /api/health require the same key as lead writes and include AMI status, logs, DB host, and metrics. 200 when healthy, otherwise 503.
curl -sS https://YOUR_HOST/healthz curl -sS -H "Authorization: Bearer YOUR_WS_SECRET" https://YOUR_HOST/api/health