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 request

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, and POST /api/send-callback-request. /healthz is 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
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. 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.

ColumnPush insert / updateCloseCallback Schedule
dial_statusPending, or Callback when a callback time is sent on a callback priorityDisposedCallback
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

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

FieldRequiredRules
oli_idYesString, 1–100 chars. Unique. Same id upserts.
phone_numberYes10–15 digits after stripping non-digits
client_nameYesString, 1–150 chars
form_typeYesStored for CRM/logging. Not used to pick an agent.
priority_idYesActive row in hold_dialer_priority.id. Seeded 1 Hold Lead, 2 Hold + Not Connected, 3 Final Disposal (not dialable).
callback_scheduled_atNoOnly 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.

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

StatusWhen
400Invalid 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
401Missing or wrong key
404Unknown /api/... path, or unknown oliId on escalated tickets / send-callback-request
409Escalated ticket or send-callback-request 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 or send-callback-request (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