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.

Bearer / X-API-Key JSON, 1 MB max POST leads · PUT tickets · callback · unhold

Auth

Lead write endpoints require WS_SECRET. Send it as any one of:

HeaderValue
AuthorizationBearer YOUR_WS_SECRET
X-API-KeyYOUR_WS_SECRET
X-Access-KeyYOUR_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

MethodPathAuthWhat it does
POST/api/leadsYesUpsert one lead or a batch by oli_id
POST/api/masterYesSame handler as /api/leads
PUT/api/escalated-ticketsYesClose (dispose) or schedule a callback for priority_id 8
POST/api/send-callback-requestYesSet callback time on an existing master oliId
POST/api/unholdDialerDataYesMove a disposed master oli_id back to Callback
GET/api/pause-reasonsNoAgent pause-reason catalog for the picker and reports
GET/healthzNoAMI + DB liveness. No internals
GET/healthYesDetailed AMI / metrics health
GET/api/healthYesSame 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.

ColumnPush insert / updateCloseCallback Schedule
data_idNew UUIDNew UUIDNew UUID
dial_statusPending, or Callback when a callback time is sent on a callback priorityDisposedCallback
disposeRemarkNULLCRM Close (escalated ticket)NULL
last_dial_resultNULLNULLNULL
hangup_byNULLNULLNULL
hangup_causeNULLNULLNULL
last_attempt_atNULLNULLNULL
first_attempt_atNULLNULLNULL
next_retry_atNULL inside working hours; otherwise next work_start. Callbacks stay NULL and wait on callback_scheduled_atNULLNULL
callback_scheduled_atNew time, or NULL if omittedNULLFrom scheduledAt
call_count000
isReserved000
isRnrSent000

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

FieldRequiredRules
oli_idYesString, 1–100 chars. Unique. Same id upserts.
data_idGeneratedUUID minted by the dialer on insert/update. Not accepted on the body (extra field → 400).
phone_numberYes10–15 digits after stripping non-digits
client_nameYesString, 1–150 chars
form_typeYesMust match an agent skill to be dialed. Claim is skill-only.
priority_idYesActive row in dialer_priority.id
callback_scheduled_atNoOnly on callback priorities (dialer_priority.is_callback = 1, seeded as ids 1–3). Datetime with time; past values are allowed (due immediately).
isProspectRequiredNoJSON 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.

FieldRequiredRules
oliIdYesExisting master oli_id, 1–100 chars. Camel case (not oli_id).
statusYesClose or Callback Schedule (spacing/case-insensitive). Not written to the database.
scheduledAtFor Callback ScheduleSame 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

StatusWhen
400Invalid 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
401Missing or wrong key
404Unknown /api/... path, or unknown oliId / oli_id on escalated tickets / send-callback-request / unholdDialerData
409Escalated ticket, send-callback-request, or unholdDialerData while the lead is Calling and reserved
413Body larger than 1 MB
429More than 120 writes from this IP in 60 seconds on escalated tickets, send-callback-request, or unholdDialerData (lead push is not rate-limited)
500Unexpected 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