Callback Request

One time. Any matching skill. Nobody dials it early.

A callback is a time-gated pool lead. CRM sends callback_scheduled_at. 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. Master has no agent_id pin. Field shapes are in the API reference.

priority_id 1 (Callback Schedule) callback_scheduled_at Skill pool when due retry_pattern after no-answer

End-to-end flow

The row sits as Callback until the time is due. Claim starts when any matching-skill agent is panel-active, ready, and AMI idle.

  1. POST callback_scheduled_at on Callback Schedule, or send-callback-request on an existing row
  2. Store Callback (skill pool, no employee pin)
  3. Wait until the time is due (and inside working hours when outbound_obey_work_hours = 1)
  4. Any idle agent with matching form_type claims it
  5. Progressive: AMI to that desk. Predictive: customer first, then skill bridge

Push

callback_scheduled_at is only for priority_id 1 (Callback Schedule) on POST. The other priorities are 2 Fresh Assign and 3 Final Disposal (set by the dialer, rejected on push). Extra keys such as agent_id are rejected. An existing row of any priority can get a time with POST /api/send-callback-request.

BodyResult
Time on priority 1 (Callback Schedule)Stored. dial_status = Callback.
Time on any other priority400 callback_scheduled_at is only allowed for callback priorities
agent_id present400 extra field rejected

Accepted times: 2026-08-20 15:30:00 or ISO-8601, including past datetimes (due immediately). Rejected: date-only. Other priority_id values cannot send a time on this POST.

curl -sS -X POST https://YOUR_HOST/api/leads \
  -H "Authorization: Bearer YOUR_WS_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "oli_id": "OLI124",
    "phone_number": "9876543211",
    "client_name": "B",
    "form_type": "GST",
    "priority_id": 3,
    "callback_scheduled_at": "2026-08-20 15:30:00"
  }'

Wait until due

Both engines skip a lead while callback_scheduled_at is in the future. Until then the row stays unreserved. When the time is null or <= NOW(), it can be claimed as soon as a matching agent is ready. Only when sales_dialer_config.outbound_obey_work_hours = 1 is it also held to work_start–work_end. After a later Not Connected, the pattern-bucket scheduler resumes.

Any matching skill

1
Skill + priority
Claim requires form_type in that agent’s skills and priority_id in their allowlist (empty = all).
2
No employee pin
Master has no agent_id. Any matching idle agent may take it.
3
Still eligible
Panel active, presence ready, AMI idle, wrap finished. If nobody matching is free, the lead waits.

Progressive vs Predictive

ProgressivePredictive
When claimedNext in that agent’s skill claimNextLead queueWhen any matching agent is idle, via claimNextForPool
Overdial poolSkill-matched like other leadsIn the pool when due. Bridged to a free skilled agent
OriginateAMI to the claiming extensionCustomer first, then Bridge to a skilled extension
If agent busy after answerN/A (agent-first)Hold until a skilled agent is free, or Abandoned if the customer hangs up

Hangup and retry

OutcomeStatusNext
Talked (ANSWER)ReceivedNever
No answer / busy / congestionNot Connectedretry_pattern + retry_seconds
call_count reaches max_triesDisposedFinal Disposal until redial, a new CRM push, or a CRM pending callback
Reject / customer cancelRejectedNever
Customer left while waitingAbandonedretry_seconds (or keep existing next_retry_at)

Failed originate restores the previous status and clears reserve without incrementing call_count or touching first_attempt_at / last_attempt_at (those are stamped only at a real dial), and retries after RELEASE_RETRY_MS (default 60s). Received / Rejected / Disposed are not claimed by the engines; socket redial can reopen them (call_type = redial). Not Connected uses sales_dialer_config.retry_pattern (currently [4,4,3,2,1,1,1,1,1,1,1]), same-day retry_seconds (3600), window work_start–work_end, until max_tries (20). Leftover in the current bucket carries to the next working day. Abandoned is not on that cycle — it keeps a future next_retry_at, otherwise waits retry_seconds. A due callback_scheduled_at waits for working hours only when outbound_obey_work_hours = 1. Redial NC does not bump call_count. Claim order: due Callback, then Pending, then Abandoned, then Not Connected.

Pending callbacks from CRM

An agent usually schedules the callback while still on the call. send-callback-request returns 409 lead_in_flight then, and the hangup settle would overwrite the status anyway. POST /api/pending-callbacks accepts it at any time and holds it until the call is settled.

  1. The CRM agent page saves the callback in the CRM (scheduleCallBack_new), then posts straight to the dialer (Bearer WS_SECRET).
  2. The dialer inserts a pending row in sales_dialer_pending_callbacks. Older pending rows for the same oli_id become superseded (latest wins).
  3. Lead not on a live call → applied now (200, status: applied). Otherwise 202, status: pending.
  4. settleLead (every engine and inbound) applies the pending row right after the hangup settle.
  5. The 60s stale sweep retries pending rows older than 30s whose lead is not active. Rows whose oli_id no longer exists become failed.

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 / reserve, and rotates data_id. It never touches a Calling + reserved row. From there the normal pool flow above applies.

Final disposition from CRM

POST /api/pending-dispositions uses the 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 the agent submitted last. The CRM page calls it after ChangeWorkStatus succeeds (Not Intrested, Invalid Lead, Language Barrier, DND). Applying sets dial_status = Disposed, disposeRemark = CRM: <reason>, priority_id = disposed_priority_id (Final Disposal), clears callback_scheduled_at / next_retry_at / reserve, and rotates data_id.

Clears missed activities

Storing either a pending callback or a pending disposition clears every open sales_dialer_missed_activities row for that OLI (any agent), with cleared_by = callback | disposition. A blocked agent gets outbound back straight away. See Missed activities.