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, POST /api/send-callback-request, and POST /api/unholdDialerData. /healthz and GET /api/pause-reasons are 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 |
| POST | /api/unholdDialerData | Yes | Move a disposed master oli_id back to Callback |
| GET | /api/pause-reasons | No | Agent pause-reason catalog for the picker and reports |
| 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. Disposed rows come back with POST /api/unholdDialerData. The dialer still claims and settles live rows itself. Floor-wide new calls stop when dialer_config.isMaintainance = 1 (maintenance banner); live talks are not hung.
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), data_id, and the raw callback_scheduled_at that was sent (null if omitted). The dialer always mints data_id (UUID). Sending it on the body is a 400. Insert and update get a new id; skipped_in_flight returns the existing one.
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", "data_id": "11111111-1111-4111-8111-111111111111", "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, send-callback-request, or unhold writes these master columns back to a fresh cycle. A live Calling + reserved row is never touched.
| Column | Push insert / update | Close | Callback Schedule |
|---|---|---|---|
data_id | New UUID | New UUID | New UUID |
dial_status | Pending, or Callback when a callback time is sent on a callback priority | Disposed | Callback |
disposeRemark | NULL | CRM Close (escalated ticket) | NULL |
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 |
Unhold (POST /api/unholdDialerData) uses the same reset as Callback Schedule, but always sets priority_id = 3, dial_status = Callback, and callback_scheduled_at to now + 1 hour when that time is still inside work_start–work_end; otherwise the next work_start. next_retry_at stays NULL. Auto-dial max-tries settle (not this table) sets disposeRemark to Dialer system: max tries reached, sending hold request. Socket redial and inbound claim also set disposeRemark to NULL when they leave Disposed.
Fields
| Field | Required | Rules |
|---|---|---|
oli_id | Yes | String, 1–100 chars. Unique. Same id upserts. |
data_id | Generated | UUID minted by the dialer on insert/update. Not accepted on the body (extra field → 400). |
phone_number | Yes | 10–15 digits after stripping non-digits |
client_name | Yes | String, 1–150 chars |
form_type | Yes | Must match an agent skill to be dialed. Claim is skill-only. |
priority_id | Yes | Active row in dialer_priority.id |
callback_scheduled_at | No | Only on callback priorities (dialer_priority.is_callback = 1, seeded as ids 1–3). Datetime with time; past values are allowed (due immediately). |
isProspectRequired | No | JSON boolean only (true or false). Omitted stores false. Strings and numbers are rejected. |
priority_id must exist and be active or the API returns 400 unknown priority_id. 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 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 sets dial_status = Disposed, disposeRemark = CRM Close (escalated ticket), and moves priority_id to Final Disposal. Callback Schedule sets dial_status = Callback and callback_scheduled_at from scheduledAt, and leaves priority_id at 8 so that agent can be claimed when the time is due. Both apply the dial-state reset.
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", "data_id": "11111111-1111-4111-8111-111111111111", "dial_status": "Disposed", "disposeRemark": "CRM Close (escalated ticket)" } }
{ "ok": true, "lead": {
"oli_id": "OLI123",
"data_id": "11111111-1111-4111-8111-111111111111",
"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 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",
"data_id": "11111111-1111-4111-8111-111111111111",
"dial_status": "Callback",
"callback_scheduled_at": "2026-08-21 16:30:00"
} }
Unhold dialer data
Same WS_SECRET. Body is only oli_id (alias oliId). Extra keys are rejected. The row must already exist or the API returns 404. Use this after CRM takes a Final Disposal / hold-work row back into calling: the dialer moves it off the disposed priority onto hardcoded callback priority_id 3, sets dial_status = Callback, and applies the same dial-state reset as a new callback (including call_count = 0 so max tries start over). callback_scheduled_at is now + 1 hour when that time is still inside work_start–work_end on a work day; otherwise it is the next work_start. next_retry_at stays null (the engine waits on the callback time). A live Calling + reserved row returns 409. A row that is not disposed returns 400 unless it is already Callback on priority 3 (then the call is a no-op 200).
curl -sS -X POST https://YOUR_HOST/api/unholdDialerData \
-H "Authorization: Bearer YOUR_WS_SECRET" \
-H "Content-Type: application/json" \
-d '{
"oli_id": "OLI123"
}'
200 response
{ "ok": true, "lead": {
"oli_id": "OLI123",
"data_id": "11111111-1111-4111-8111-111111111111",
"dial_status": "Callback",
"priority_id": 3,
"callback_scheduled_at": "2026-08-26 12:00: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": 3,
"callback_scheduled_at": "2026-08-20 15:30:00"
}'
On POST /api/leads, the engine waits on this time for callback priorities (dialer_priority.is_callback = 1, seeded as ids 1, 2, or 3). A future callback_scheduled_at on any row (including priority 8 after Callback Schedule) is skipped until due. When due, any idle agent whose skills include form_type and whose priority_ids allow the lead’s priority_id (empty = all) may claim it. Full calling behaviour is in the Callback Request guide.
RNR mail
These are outbound from the dialer, not inbound HTTP routes. They are fire-and-forget and do not block settle.
The first Not Connected while isRnrSent is still 0 POSTs to RNR_MAIL_URL (default /employee/CronController/sendRnrmail) with oli_id and agent_id "0". On HTTP success the row is set isRnrSent = 1. A later push or escalated-ticket update sets that flag back to 0. Socket redial Not Connected does not send RNR mail.
When that Not Connected is also scheduled for retry (next_retry_at set, not yet Disposed), a silent POST goes to NC_RETRY_API_URL (default CRM /employee/CronController/dialerNotConnectCallbackSchedule) with Bearer / x-api-token from NC_RETRY_API_TOKEN (falls back to WS_SECRET). Body is oli_id, agent_id "0", and schedule_date as the next retry wall clock in DB_TIMEZONE. Failures are logged and never block settle.
When Not Connected hits max_tries and the lead is Disposed, a silent POST goes to HOLD_WORK_STATUS_URL (default CRM /employee/CronController/genarateHoldWorkStatus) with Bearer / x-api-token from HOLD_WORK_STATUS_TOKEN (falls back to WS_SECRET). Body is oli_id and remark Dialer system: max tries reached, sending hold request (<hmac>), where the hmac is SHA-256 of hold-work-status:<oli_id> with that secret. Master disposeRemark stores the same text without the hmac. This does not run for escalated-ticket Close. CRM later uses POST /api/unholdDialerData to put that row back on Callback priority 3.
While agents are live, the dialer polls PENDING_REMARKS_URL (default /employee/CronController/getAllRemarksNotProvided) about every 30 seconds per employee id. Body is { "agent_id": … }. remarks: true (also 1 / "true") blocks that agent’s auto-dial only; inbound and redial stay open. remarks: false (also 0) clears the block. HTTP errors keep the last flag. Dashboard Dialer column shows Remarks. Agent pause is refused until remarks are clear.
When the lead has priority_id = 8 (Escalated Tickets), each auto-dial Not Connected also POSTs to ESCALATION_CUSTODY_URL (default /escalation/ApiController/updateCustodyFromDialer) with Bearer / x-api-token from ESCALATION_CUSTODY_TOKEN (falls back to WS_SECRET). Body is only { "oli_id": "…" }. Socket redial Not Connected skips this call.
Pause reasons
Public. No WS_SECRET. CRM and agent.html fetch this once on connect to populate the pause picker. Codes are stored in dialer_agent_pauses.reason (VARCHAR(32)). Agent pause on the socket must use a picker code; system codes are never in this list. If the request fails, clients fall back to the same 15 codes. other requires a short note (max 12 words / 80 characters) stored in reason_note.
curl -sS https://YOUR_HOST/api/pause-reasons
{
"ok": true,
"note_max_words": 12,
"note_max_chars": 80,
"reasons": [
{ "code": "bio", "label": "Bio break" },
{ "code": "ondemand", "label": "On-demand call" },
{ "code": "other", "label": "Other", "note": true }
],
"system": [
{ "code": "left", "label": "Left / disconnect" },
{ "code": "toggle", "label": "Unspecified" },
{ "code": "day_reset", "label": "Day reset" }
]
}
Picker codes: bio, tea, lunch, meeting, training, backoffice, acw, escalation, process_query, system, logout, personal, poa, ondemand, other. Socket pause with a missing or system-only code emits server_error invalid_pause_reason. Wire details are in the Socket 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, escalated ticket when priority_id is not 8, or unhold when the lead is not disposed |
401 | Missing or wrong key |
404 | Unknown /api/... path, or unknown oliId / oli_id on escalated tickets / send-callback-request / unholdDialerData |
409 | Escalated ticket, send-callback-request, or unholdDialerData 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, send-callback-request, or unholdDialerData (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