Payer calls
“What did the payer say, and what do we do next?”
The Payer calls workflow phones payers for you. Daybook's voice agent works through the phone tree, waits on hold, and when a rep picks up it either asks your questions itself or brings your biller onto the line. Every call ends in a structured answer with the payer's reference number, written to the claim and sent to your system, not a note someone has to type up. This page is the integration guide: how a work queue, a CRM or an internal dashboard hands Daybook a claim to call about, follows the call, receives the answer and closes the loop.
At a glance
- Workflow
- Payer calls
- Stage
- Any time EDI or the portal can't answer
- Primary users
- AR follow-up, denial and eligibility teams, and the engineers wiring their queues to Daybook
- Core question
- What did the payer say, and what do we do next?
- Call types
- Claim status · Eligibility · Prior auth · Appeal status · Payment trace
- Lifecycle
- queued · dialing · navigating · on_hold · with_rep · completed, failed or canceled
- Comes back as
- Status · Reference number · Rep name · Next step · Call-type details, with your metadata echoed
- API
- POST /v1/calls · GET /v1/calls · GET /v1/calls/{id} · cancel · instruct · transcript · call.* webhooks · MCP start_call
- In a sandbox
- Real calls through the same voice agent, behind the same gates. Events carry mode test
What this workflow enables
- Put a payer call on a claim from your own queue, with your ids on it, and get the answer back under the same ids.
- Map a payer's phone tree once and save it, so later calls go straight to the right option.
- Hand a live rep to a biller with the questions ready, or let Daybook handle the call.
- Follow the call as it moves: queued, dialing, phone tree, hold, rep, done.
- Repeat a call on a schedule, such as every 5 days until a pending claim moves, and cancel a call your queue no longer needs.
- Tell the agent something while it is on the line, and keep the transcript beside the answer.
- Write status, reference number, rep name and next step straight into your ticket, claim or CRM record.
Typical flow
- Pick the work
Your queue decides what needs a call: a claim silent for 30 days after the 277 said nothing useful, a denial your rules say to challenge by phone, a payment to trace. Pull the claim, patient and provider from your record, and keep your own ids ready: the account, the ticket, the work item. They travel with the call as
metadataand come back on every event about it. - Start the call
POST /v1/callswith the payer, the call type, the claim, patient and provider, andmetadataholding your ids. Send anIdempotency-Key, such as the ticket id and attempt number, so a retry after a timeout never dials twice. The response is the call in statusqueuedwith an id prefixedcall_; store it on your ticket. - Daybook works the phone tree
It presses the keys and enters the ids the menu asks for, following the payer's saved phone tree where there is one and learning it the first time. The call moves through
dialingtonavigating, and you receivecall.started. - Hold
Daybook waits.
call.on_holdcarries the hold time so far, andGET /v1/calls/{id}showson_holdwithhold_seconds. In the Daybook app the call sits under Switchboard → Live calls with its transcript as it happens. - A rep picks up
The call is
with_rep. Inautonomousmode Daybook asks the call script's questions and reads the answers back. Inhandoffmode it rings the biller named inhandoff.phoneand sendscall.handoff_readywith the questions to ask. From Live calls, anyone allowed to control the call can tell the agent something, take the call over or hang up. - Structured answer and write-back
call.completedcarries the status, reference number, rep name, next step and the call type's details, with yourmetadataechoed. Verify the signature, record the event id, update your ticket and return2xx. A call that could not finish sendscall.failedinstead. A completed call with a reference number also moves the claim in Daybook to what the payer said: paid, denied, pended or in process.
Step 3 of 6: Daybook works the phone tree, sends call.started
{
"id": "evt_01JBXM2A4C6E8G0J2M4P6R8T12",
"type": "call.started",
"created_at": "2026-10-29T14:31:40Z",
"mode": "live",
"data": {
"id": "call_01JBX8M4Q2T7",
"payer": "bayline",
"call_type": "claim_status",
"metadata": {
"account": "A-10442",
"ticket": "TCK-55821"
}
}
}# 1. A queue item that needs a call becomes one request. Your ids ride along as metadata,
# In your code make the Idempotency-Key the ticket and attempt (the Node tab does); here a fresh UUID.
curl https://api.daybookhealthcare.com/v1/calls \
-H "Authorization: Bearer $DAYBOOK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"payer": "bayline",
"call_type": "claim_status",
"claim": { "patient_control_number": "A-10442-1", "date_of_service": "2026-09-14" },
"patient": { "first_name": "Jordan", "last_name": "Sample",
"date_of_birth": "1980-01-01", "member_id": "W000000001" },
"provider": { "npi": "1999999984" },
"metadata": { "account": "A-10442", "ticket": "TCK-55821" }
}'
# -> { "id": "call_01JBX8M4Q2T7", "status": "queued", ... } store the id on the ticket
# 2. Where does it stand? Poll if you must; the answer itself arrives as call.completed.
curl https://api.daybookhealthcare.com/v1/calls/call_01JBX8M4Q2T7 \
-H "Authorization: Bearer $DAYBOOK_API_KEY"The control surface
Everything an integration can do with a payer call, and what it is called. The last two rows are firm set-up and operator controls, done in the Daybook app.
| Operation | Shape | Notes |
|---|---|---|
| Start a call | POST /v1/calls | Gated the same way everywhere: the firm's owner has confirmed Daybook as billing agent, a call script fits the claim's step, the payer's line is linked. Send an Idempotency-Key. |
| List and filter calls | GET /v1/calls | By claim, status, payer, call type, time window or metadata[key]. Pages of up to 100. |
| Retrieve a call | GET /v1/calls/{id} | Prefer call.completed over polling; it carries the same answer and your metadata. |
| Cancel a call | POST /v1/calls/{id}/cancel | Queued: never dials. Live: the agent says goodbye. Repeating: the series stops. Sends call.canceled. |
| Tell the agent something | POST /v1/calls/{id}/instruct | Up to 280 characters, never spoken as written; it shows in the transcript as an operator note. |
| Read the transcript | GET /v1/calls/{id}/transcript | Turn by turn, with the phone tree, the hold and your notes. |
| Hand off to a biller | mode: handoff and handoff.phone on start; call.handoff_ready | The agent gets through the tree, holds, and rings your biller when a rep answers. Nobody picks up: the call ends as failed with the reason. |
| Repeat until the status moves | schedule on start; cancel stops the series | Each run is a new call with repeat_of the first one and the same metadata. Each run is a real call, in a sandbox too. |
| Hear about it | call.started, call.on_hold, call.handoff_ready, call.completed, call.failed, call.canceled | The same id on every retry; your metadata echoed on each. |
| Let an agent do it | MCP list_calls, get_call; start_call | Same permissions and audit trail as the API. See Payer calls from Claude. |
| Take over the line, press keys, listen in | Daybook app, Live calls | A takeover needs a person's softphone in the browser; keys and listening are operator controls, not an integration surface. |
| Payer lines, phone trees, voice agents, call scripts | Daybook app, Switchboard area | Firm set-up a lead does once. Not part of the firm API contract; the voice agent's own control plane is behind a credential Daybook holds. |
Auth, isolation and errors
Every call route takes your firm's key as a bearer token (Authentication). A key opens one workspace in one environment: sk_test_ keys see only their sandbox's calls and sk_live_ keys only their production account's, and a call id from another account answers 404 not_found, never a hint that it exists. A missing or wrong key is 401 authentication_failed; a route a key may not use is 403. Every POST takes an Idempotency-Key: the same key within 24 hours returns the first answer, the same key with a different body is 409 idempotency_conflict. Cancel or instruct on a call that has ended is 409 conflict; a missing field is 400 invalid_request with field naming it. Shapes on Idempotency and errors.
metadata is for your ids: account, ticket, queue, work item. Never put a patient's name, date of birth or member id in it; those go in patient and claim, where Daybook handles them as PHI. Metadata is echoed on every event and will end up in your logs.
A denial queue that calls the payer and writes the answer back
The most common hookup. Your work queue, whether it is a denial worklist, an aged AR report or a task list in your PM system, decides which claims need a payer on the phone. Daybook makes the calls, and the verified answer lands back on the same ticket with the payer's reference number before a biller looks at it. The queue a person works shrinks to the calls that needed a person.
- Pick the rule in your queue. Denials with CARC CO-197 or CO-16 more than five days old and no appeal filed; claims silent 30 days after submission whose 277 said nothing useful. Each is a ticket with the claim, patient and provider at hand.
- Start one call per ticket with
POST /v1/calls:call_typeclaim_status, your extraquestions, andmetadatawith the ticket, account and queue. TheIdempotency-Keyis the ticket id and the attempt number, so a crashed sweep never dials twice. Storecall.idon the ticket and mark it "calling payer". - Register a webhook endpoint for
call.completed,call.failedandcall.canceled(the Quickstart shows how). Verify the signature, return2xx, and drop anidyou have seen. - On
call.completed, writeanswer.status,reference_number,rep_name,next_stepand the details onto the ticket, then move it:paidcloses it with the check number for posting to match;deniedopens the appeal step with the denial reason;pendingorin_processsets a follow-up date;not_on_fileroutes it to resubmission. - On
call.failedorcall.canceled, take the exception path. Count the attempt: under three, let the rule start a new call tomorrow with a newIdempotency-Key; at three, assign the ticket to a person withended_reasonand the transcript link. - Reconcile nightly with
GET /v1/calls?metadata[queue]=denials&status=queued,dialing,navigating,on_hold,with_rep: anything running longer than your ceiling gets a cancel and a person.
import express from "express";
import { verifyDaybookWebhook } from "./verify.js";
import { crm } from "./crm.js"; // your denial queue, CRM or PM system
const API = "https://api.daybookhealthcare.com/v1";
const auth = { Authorization: "Bearer " + process.env.DAYBOOK_API_KEY, "Content-Type": "application/json" };
const MAX_ATTEMPTS = 3;
// 1. The queue rule: denials for a missing authorization, five days old, no appeal filed, no call yet.
export async function sweepDenialQueue() {
const tickets = await crm.tickets.find({
queue: "denials", carc: ["CO-197", "CO-16"], olderThanDays: 5, appealFiled: false, daybookCallId: null,
});
for (const t of tickets) await startCall(t);
}
// 2. One ticket, one call. The ticket and attempt are the idempotency key, so a crashed sweep never dials twice.
async function startCall(t) {
const res = await fetch(API + "/calls", {
method: "POST",
headers: { ...auth, "Idempotency-Key": "ticket-" + t.id + "-call-" + (t.callAttempts + 1) },
body: JSON.stringify({
payer: t.payerId,
call_type: "claim_status",
claim: { patient_control_number: t.claimId, payer_claim_number: t.icn, date_of_service: t.dateOfService },
patient: t.patient, // first_name, last_name, date_of_birth, member_id: PHI goes here, never in metadata
provider: { npi: t.renderingNpi },
questions: ["Was the retro authorization request received?"],
metadata: { ticket: t.id, account: t.accountId, queue: "denials" },
}),
});
if (!res.ok) {
await crm.tickets.note(t.id, "Daybook refused the call: " + (await res.json()).error.message);
return;
}
const call = await res.json(); // status "queued"
await crm.tickets.update(t.id, { daybookCallId: call.id, callAttempts: t.callAttempts + 1, status: "calling payer" });
}
// 3. The answer comes back on the same ticket, signed. Verify, acknowledge, then write.
const app = express();
app.post("/daybook/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
const event = verifyDaybookWebhook(req.body, req.headers, process.env.DAYBOOK_WEBHOOK_SECRET);
res.sendStatus(200);
if (await crm.events.seen(event.id)) return; // a retried delivery carries the same id
const call = event.data;
const ticketId = call.metadata?.ticket;
if (!ticketId) return;
if (event.type === "call.completed") {
const a = call.answer;
await crm.tickets.update(ticketId, {
payerSaid: a.status, reference: a.reference_number, rep: a.rep_name, nextStep: a.next_step,
payerClaimNumber: call.claim?.payer_claim_number, transcriptUrl: call.transcript_url,
});
// 4. Move the ticket by what the payer said.
if (a.status === "paid") {
await crm.tickets.close(ticketId, { matchDeposit: a.details?.check_number, paidCents: a.details?.paid_amount_cents });
} else if (a.status === "denied") {
await crm.tickets.openStep(ticketId, "file_appeal", { reason: a.details?.denial_reason });
} else if (a.status === "not_on_file") {
await crm.tickets.openStep(ticketId, "resubmit");
} else {
await crm.tickets.followUp(ticketId, { inDays: 7 }); // pending or in_process
}
} else if (event.type === "call.failed" || event.type === "call.canceled") {
// 5. The exception path: try again tomorrow, then hand the ticket to a person with the reason.
const t = await crm.tickets.get(ticketId);
if (event.type === "call.failed" && t.callAttempts < MAX_ATTEMPTS) {
await crm.tickets.update(ticketId, { status: "retry call", retryAfter: tomorrow() });
} else {
await crm.tickets.assign(ticketId, {
to: "ar-follow-up",
note: "Daybook could not reach " + t.payerName + " after " + t.callAttempts + " calls: " + call.ended_reason,
});
}
}
await crm.events.mark(event.id);
});
const tomorrow = () => new Date(Date.now() + 86_400_000).toISOString();What the firm gets: every denial in the rule is worked the day it qualifies, with the payer's own words and a reference number on the ticket, and billers spend their time on the appeals and the exceptions rather than on hold.
Hold the line for a biller and hand the rep over
Some calls need a person: an appeal argument, a contract dispute, a payer that will only speak to the office. Daybook still does the part nobody wants, the phone tree and the hold. When a rep answers, Daybook rings your biller, and your tool pops the ticket with the questions as the call connects.
- Start the call with
mode: handoffandhandoff.phone, the biller's number in E.164, plusnamefor your logs. Add thequestionsthe biller should cover beyond the call type's standard ones; they ride with the call and come back oncall.handoff_ready. - On
call.startedandcall.on_hold, your dashboard shows "Daybook is holding with Meridian for TCK-61044" withhold_seconds, so the biller keeps working. - On
call.handoff_ready, a rep is on the line and Daybook is ringing the biller. Pop the ticket on the biller's screen withhandoff.questionsandhandoff.rep_name; the biller picks up and is talking to a rep who already knows which claim this is. - Nobody picks up within the ring: the agent says goodbye and the call ends as
failed,ended_reasonsaying so. Route the ticket back to the queue. - On
call.completed,answercarries what the agent captured before the handoff, such as the reference number and the rep's name. What the biller learned on the line, they record on the ticket in your tool, or in Daybook's app as a call logged by hand. - The browser alternative: Take over in Live calls opens a softphone for the person watching the call. By API, the handoff is the ring.
import os, requests
from flask import Flask, request
from verify import verify_daybook_webhook
from desk import desk # your dashboard: tickets, billers, screen pops
API = "https://api.daybookhealthcare.com/v1"
AUTH = {"Authorization": "Bearer " + os.environ["DAYBOOK_API_KEY"]}
# 1. Hold the line for a biller: the agent does the phone tree and the hold, then rings them.
def hold_the_line_for(ticket, biller):
res = requests.post(
API + "/calls",
headers={**AUTH, "Idempotency-Key": f"ticket-{ticket.id}-handoff-{ticket.call_attempts + 1}"},
json={
"payer": ticket.payer_id, # "meridian"
"call_type": "appeal_status",
"mode": "handoff",
"handoff": {"phone": biller.phone_e164, "name": biller.name},
"questions": [
"Was the appeal received, and which reviewer has it?",
"What is the decision deadline?",
],
"claim": {"patient_control_number": ticket.claim_id, "payer_claim_number": ticket.icn,
"date_of_service": ticket.date_of_service},
"patient": ticket.patient, # PHI goes here, never in metadata
"provider": {"npi": ticket.npi},
"metadata": {"ticket": ticket.id, "biller": biller.id},
},
)
call = res.json()
desk.tickets.update(ticket.id, daybook_call_id=call["id"], status="Daybook is calling the payer")
app = Flask(__name__)
@app.post("/daybook/webhooks")
def daybook_webhook():
event = verify_daybook_webhook(request.get_data(), request.headers, os.environ["DAYBOOK_WEBHOOK_SECRET"])
if desk.events.seen(event["id"]):
return "", 200
call = event["data"]
ticket_id = call["metadata"]["ticket"]
biller_id = call["metadata"]["biller"]
if event["type"] == "call.on_hold":
# 2. The biller keeps working; the ticket shows the hold ticking.
desk.tickets.banner(ticket_id, f"Daybook is on hold with the payer, {call['hold_seconds'] // 60} min so far")
elif event["type"] == "call.handoff_ready":
# 3. A rep is on the line and the biller's phone is ringing: pop the ticket with the questions.
desk.screen_pop(biller_id, ticket_id, questions=call["handoff"]["questions"], rep=call["handoff"].get("rep_name"))
elif event["type"] == "call.completed":
# 4. What the agent captured before the handoff; the biller records the rest on the ticket.
a = call["answer"] or {}
desk.tickets.update(ticket_id, status="worked by biller", reference=a.get("reference_number"),
rep=a.get("rep_name"), transcript_url=call["transcript_url"])
desk.tickets.ask(biller_id, ticket_id, "Record what the payer said on the call")
elif event["type"] in ("call.failed", "call.canceled"):
# 5. Nobody picked up the ring, or the payer hung up: back to the queue with the reason.
desk.tickets.update(ticket_id, status="needs a call", note=f"Handoff did not complete: {call.get('ended_reason')}")
desk.events.mark(event["id"])
return "", 200What the firm gets: billers take the calls that need their judgment without the twenty minutes of hold before each, and every handoff is on the record with a transcript up to the moment the rep answered.
Recheck a pending claim every five days until it moves
A claim the payer says is in process will move eventually, and someone has to keep asking. A schedule on the first call makes Daybook ask every few days and stop on its own when the answer changes; your 835 or 277 can stop it sooner.
- Start the first call with
schedule.repeat_every_daysanduntilstatus_changes(or a date). The response carriesnext_run_at. - Each later run is its own call with
repeat_ofthe first call's id and the samemetadata, so the handler from the first walkthrough needs no special case. The series stops itself whenanswer.statuschanges. - Stop it early with
POST /v1/calls/{id}/cancelon any call in the series when your remit or claim status check says the claim moved.
# Start a claim status call that repeats every 5 days until the status changes
curl https://api.daybookhealthcare.com/v1/calls \
-H "Authorization: Bearer $DAYBOOK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"payer": "keystone",
"call_type": "claim_status",
"claim": { "patient_control_number": "A-20817-1", "date_of_service": "2026-09-30" },
"patient": { "first_name": "Jordan", "last_name": "Sample",
"date_of_birth": "1980-01-01", "member_id": "W000000001" },
"provider": { "npi": "1999999984" },
"schedule": { "repeat_every_days": 5, "until": "status_changes" },
"metadata": { "ticket": "TCK-61007", "queue": "silent-claims" }
}'
# -> "next_run_at": "2026-11-14T14:02:11Z". Every later run is a new call with
# "repeat_of": "call_01JBX8M4Q2T7" and the same metadata, so your handler needs no special case.
# Your 835 says it paid: stop the series before the next run
curl -X POST https://api.daybookhealthcare.com/v1/calls/call_01JBX8M4Q2T7/cancel \
-H "Authorization: Bearer $DAYBOOK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "reason": "Paid on the 835 of 2026-11-12" }'Journeys in this workflow
Use cases
What teams build on this workflow, each with a prompt you can paste into Claude, ChatGPT or Cursor.
APIs powering this workflow
/v1/callsStart a callGET/v1/callsList callsGET/v1/calls/{id}Retrieve a callPOST/v1/calls/{id}/cancelCancel a callPOST/v1/calls/{id}/instructTell the agent somethingGET/v1/calls/{id}/transcriptRead the transcriptEvents you receive
Subscribe to these with a webhook endpoint. Every event is signed; see Verifying signatures.
| Event | Sent when |
|---|---|
call.started | Daybook dialed and is working the phone tree. The call is dialing, then navigating. |
call.on_hold | Daybook reached the hold queue. Carries hold_seconds so far. |
call.handoff_ready | A rep answered a handoff call. Carries the bridge details and the questions to ask. |
call.completed | The call ended with a structured answer, your metadata echoed. |
call.failed | The call could not be completed. ended_reason says why. |
call.canceled | The call was stopped by Cancel a call or by a person in the Daybook app, before it dialed or while it ran. ended_reason carries the reason given. |
{
"id": "evt_01JBXM2A4C6E8G0J2M4P6R8T0W",
"type": "call.completed",
"created_at": "2026-10-29T14:31:40Z",
"mode": "live",
"data": {
"id": "call_01JBX8M4Q2T7",
"payer": "bayline",
"call_type": "claim_status",
"mode": "autonomous",
"hold_seconds": 1412,
"answer": {
"status": "denied",
"reference_number": "BHP-4471-0092",
"rep_name": "Dana",
"next_step": "Submit a retro authorization, then request reprocessing.",
"details": {
"denial_reason": "CO-197 precertification absent"
}
},
"metadata": {
"account": "A-10442",
"ticket": "TCK-55821"
}
}
}
