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 · callback · pending GET missed activities

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, POST /api/send-callback-request, POST /api/pending-callbacks, and POST /api/pending-dispositions. GET /api/missed-activities uses it too. /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
POST/api/send-callback-requestYesSet callback time on an existing master oliId
POST/api/pending-callbacksYesCRM after-call callback. Held until the live call settles, then applied
POST/api/pending-dispositionsYesCRM final disposition. Held until settle, then sets Disposed
GET/api/missed-activitiesYesOne agent's open missed activities and outbound-blocked flag
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 with POST /api/leads (or /api/master) using one of the three priorities: 1 Callback Schedule, 2 Fresh Assign (3 Final Disposal is set by the dialer and rejected on push). The dialer still claims and settles live rows itself. Floor-wide new calls stop when sales_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": 2
  }'

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": 2
      },
      {
        "oli_id": "OLI124",
        "phone_number": "9876543211",
        "client_name": "B",
        "form_type": "GST",
        "priority_id": 1,
        "callback_scheduled_at": "2026-08-20 15:30:00"
      }
    ]
  }'

Dial-state reset

Every successful insert, update, or send-callback-request writes these master columns back to a fresh cycle. A live Calling + reserved row is never touched.

ColumnPush insert / updateSend callback request
data_idNew UUIDNew UUID
dial_statusPending, or Callback when a callback time is sent on Callback ScheduleCallback
disposeRemarkNULLNULL
last_dial_resultNULLNULL
hangup_byNULLNULL
hangup_causeNULLNULL
last_attempt_atNULLNULL
first_attempt_atNULLNULL
next_retry_atNULL inside working hours; otherwise next work_start. Callbacks stay NULL and wait on callback_scheduled_atNULL
callback_scheduled_atNew time, or NULL if omittedFrom scheduledAt
call_count00
isReserved00

Auto-dial max-tries settle (not this table) sets disposeRemark to Dialer system: max tries reached. 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_idYes1 Callback Schedule or 2 Fresh Assign. 3 Final Disposal is rejected (the dialer sets it at max tries).
callback_scheduled_atNoOnly on Callback Schedule (priority_id 1, the only sales_dialer_priority.is_callback = 1 row). Datetime with time; past values are allowed (due immediately).

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 sales_dialer_call_attempts at settle, not on master.

Send callback request

Same WS_SECRET. Body is only oliId and scheduledAt (alias ScheduledAt). Extra keys are rejected. The oliId must already exist in sales_dialer_master_data or the API returns 404. 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 dial-state reset.

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"
} }

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": 1,
    "callback_scheduled_at": "2026-08-20 15:30:00"
  }'

On POST /api/leads, the engine waits on this time for Callback Schedule (priority_id 1). A future callback_scheduled_at on any row (including one set by send-callback-request) 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.

Pending callbacks

For callbacks scheduled from the CRM page during or right after a call. send-callback-request would return 409 while the lead is live, and the hangup settle would overwrite it anyway. This endpoint accepts at any time: it stores a pending row in sales_dialer_pending_callbacks (older pending rows for the same OLI become superseded, latest wins) and applies it as soon as the lead is not on a live call.

Body is camelCase. Extra keys are rejected. oliId must exist in master (404 unknown oliId). scheduledAt is a wall clock in DB_TIMEZONE (2026-10-01 15:00[:00]) or ISO-8601 with offset, and must not be more than 5 minutes in the past. Optional audit fields: agentId, extension, callbackType (today / future_day), workStatus, subDisposition, remarks (max 500).

curl -sS -X POST https://YOUR_HOST/api/pending-callbacks \
  -H "Authorization: Bearer YOUR_WS_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "oliId": "OLI124",
    "scheduledAt": "2026-10-01T15:00:00+05:30",
    "agentId": 42,
    "extension": "1030",
    "callbackType": "future_day"
  }'

200 when applied now, 202 when held until the live call settles:

{ "ok": true, "pending": {
  "id": 17,
  "oli_id": "OLI124",
  "scheduled_at": "2026-10-01 15:00:00",
  "status": "applied"
} }

Applying sets dial_status = Callback, callback_scheduled_at, priority_id = 1 (Callback Schedule), clears next_retry_at, call_count, attempt timestamps, last_dial_result, hangup_by, hangup_cause, disposeRemark and the reserve, and mints a new data_id. Storing the row also clears any missed activity for that OLI.

Pending dispositions

Final disposition from the CRM page (Not Intrested, Invalid Lead, Language Barrier, DND). Same table (action = dispose) and the same latest-wins / apply-after-settle flow, so a callback followed by a disposition (or the reverse) ends with whichever was submitted last.

Body: oliId (required, existing), reason (required, 1–200 chars). Optional audit fields: agentId, extension, workStatus, subDisposition, remarks.

curl -sS -X POST https://YOUR_HOST/api/pending-dispositions \
  -H "Authorization: Bearer YOUR_WS_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "oliId": "OLI124", "reason": "Not Intrested - Price too high", "agentId": 42, "workStatus": 4 }'
{ "ok": true, "pending": {
  "id": 18,
  "oli_id": "OLI124",
  "action": "dispose",
  "reason": "Not Intrested - Price too high",
  "status": "pending"
} }

Applying sets dial_status = Disposed, disposeRemark = CRM: <reason>, priority_id = disposed_priority_id (Final Disposal), clears callback_scheduled_at, next_retry_at and the reserve, and mints a new data_id. The engines stop claiming the lead. Storing the row also clears any missed activity for that OLI.

Missed activities

Every bridged outgoing or redial call opens a missed activity for that agent and OLI. If no pending callback or disposition is stored within MISSED_ACTIVITY_GRACE_MINUTES (default 10) after hangup, the agent's outbound is paused (inbound still rings). While paused, they can still redial their own missed OLIs. The CRM sales-dialer page uses this endpoint for its Missed activities panel.

Query one of agentId (sales_dialer_employee.id) or extension (3–16 digits). An extension not in the roster returns 404 unknown_extension. Items are every uncleared row (max 100, oldest first): overdue ones, ones still inside the grace window (overdue: false), and a live call (ended_at and due_at are null).

curl -sS -H "Authorization: Bearer YOUR_WS_SECRET" \
  "https://YOUR_HOST/api/missed-activities?extension=1030"
{
  "ok": true,
  "agent_id": 42,
  "blocked": true,
  "grace_minutes": 10,
  "items": [
    {
      "id": 5,
      "oli_id": "OLI124",
      "lead_id": 991,
      "call_type": "outgoing",
      "extension": "1030",
      "connected_at": "2026-09-28T05:10:00.000Z",
      "ended_at": "2026-09-28T05:14:00.000Z",
      "due_at": "2026-09-28T05:24:00.000Z",
      "overdue": true
    }
  ]
}

Live changes come over the socket as dialer.outbound_blocked / dialer.missed_activities (see the Socket guide). Rules and UI are in Settle, retry & more.

Pause reasons

Public. No WS_SECRET. CRM and agent.html fetch this once on connect to populate the pause picker. Codes are stored in sales_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, or priority_id 3 (Final Disposal)
401Missing or wrong key
404Unknown /api/... path, unknown oliId on send-callback-request / pending-callbacks / pending-dispositions, or unknown_extension on missed-activities
409send-callback-request while the lead is Calling and reserved (pending-callbacks accepts it with 202 instead)
413Body larger than 1 MB
429rate_limited: more than 120 requests per IP in 60 seconds on send-callback-request or pending-callbacks / pending-dispositions (shared), or 240 on missed-activities. 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