API reference
For developers integrating directly with CallFlowHQ, not just through the dashboard.
Base URL
https://api.callflowhq.com/v1
Every path below is relative to this -- for example, POST /phone-numbers means POST https://api.callflowhq.com/v1/phone-numbers, as shown in each section's own curl example.
Authentication
Every request needs an API key, created from your account's Developers page (Owner or Admin only). Send it as a bearer token:
Authorization: Bearer cfhq_live_...
A revoked or invalid key returns 401. A key is tied to one account -- there's no per-key role today, so a valid key can read and write anything this reference describes for that account (see Write access & audit log below for exactly which accounts that includes).
Rate limits
30 requests/minute per key across the API generally. Endpoints that carry real carrier cost -- currently POST /v1/phone-numbers -- have an additional, tighter limit of 5 requests/minute per key. A request over either limit returns 429 with X-RateLimit-Limit/-Remaining/-Reset headers.
Account scoping & pagination
Every GET list endpoint below is scoped to your API key's account. If that account is a top-level (parent) account, results automatically roll up to include its sub-accounts too -- the same rule the dashboard's own account switcher applies. Pass ?accountId=<uuid> to narrow to one specific account within that rollup; requesting an account outside it returns 403.
List endpoints that can return a lot of rows (calls, leads, compliance-flags, appointments, dnc-entries) support ?limit= (default 50, max 200) and ?offset= (default 0), and are sorted newest first.
Write access & audit log
An API key can write to its own account, plus -- if that account is a top-level (parent) account -- its direct sub-accounts. The same rollup described above for reads is also the write boundary: passing an accountId outside it returns 403, and it never applies to a sub-account's own sub-accounts (accounts are capped at two levels).
Every write -- creating a sub-account or branch, editing/reassigning/releasing a phone number, or creating/updating/deleting an agent -- is recorded in CallFlowHQ's internal audit log, the same trail operator actions and dashboard actions land in, attributed to the specific API key that made the call. Read-only endpoints (calls, leads, compliance flags, appointments, DNC entries) are not logged.
List Sub-accounts
GET /v1/sub-accounts
Lists the direct sub-accounts of your API key's own account. Empty array if your key's account is itself a sub-account -- accounts are capped at two levels, so it can't have any of its own.
curl https://api.callflowhq.com/v1/sub-accounts \ -H "Authorization: Bearer cfhq_live_..."
Response
{
"subAccounts": [
{
"id": "<uuid>",
"name": "Downtown branch office",
"account_number": 1002,
"timezone": "America/New_York",
"require_mfa": false,
"plan": "active",
"status": "active",
"created_at": "2026-09-17T18:04:00Z"
}
]
}Create Sub-account
POST /v1/sub-accounts
Creates a sub-account directly under your API key's own account. Only works if your key's account is itself top-level -- sub-accounts can't be nested.
curl -X POST https://api.callflowhq.com/v1/sub-accounts \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "Downtown branch office"}'Request body
- name (string, required)
Response
201 with the new account. A new sub-account starts with the same default Mon-Fri 9am-5pm appointment availability a self-serve signup gets.
{ "account": { "id": "<uuid>", "name": "Downtown branch office" } }403 if your key's own account is already a sub-account.
Update Sub-account
PATCH /v1/sub-accounts/:id
Renames a direct sub-account of your API key's own account. Your own top-level account (the one your key belongs to) isn't renameable through this endpoint -- only its children.
curl -X PATCH https://api.callflowhq.com/v1/sub-accounts/<uuid> \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "Downtown branch office (renamed)"}'Request body
- name (string, required)
Response
{ "account": { "id": "<uuid>", "name": "Downtown branch office (renamed)", ... } }404 if :id isn't a direct sub-account of your key's own account.
Delete Sub-account
DELETE /v1/sub-accounts/:id
Permanently deletes a direct sub-account of your API key's own account. This only succeeds if the sub-account has no other data attached -- phone numbers, users, agents, calls, tools, branches, webhooks, API keys, DNC entries, usage history, and so on all block it. There's no way to force it through the API; remove or move whatever's listed in the error first (from the dashboard, or by reassigning phone numbers to another account).
curl -X DELETE https://api.callflowhq.com/v1/sub-accounts/<uuid> \ -H "Authorization: Bearer cfhq_live_..."
Response
{ "deleted": true }404 if :id isn't a direct sub-account of your key's own account. 409 if it still has other data attached.
List Branches
GET /v1/branches
Lists branches across your API key's account rollup (self + direct sub-accounts by default -- see Account scoping above). Narrow with ?accountId=.
curl https://api.callflowhq.com/v1/branches \ -H "Authorization: Bearer cfhq_live_..."
Response
{
"branches": [
{
"id": "<uuid>",
"account_id": "<uuid>",
"name": "Main St office",
"address_line1": null,
"city": null,
"state": null,
"zip": null,
"created_at": "2026-09-17T18:04:00Z"
}
]
}Create Branch
POST /v1/branches
Creates a branch (Account -> Branch -> Phone numbers) under your key's account or one of its direct sub-accounts.
curl -X POST https://api.callflowhq.com/v1/branches \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "Main St office", "accountId": "<uuid>"}'Request body
- name (string, required)
- accountId (string, optional) -- defaults to your key's own account
Response
{ "branch": { "id": "<uuid>", "account_id": "<uuid>", "name": "Main St office" } }Update Branch
PATCH /v1/branches/:id
Renames a branch on your account or one of its direct sub-accounts.
curl -X PATCH https://api.callflowhq.com/v1/branches/<uuid> \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "Main St office (renamed)"}'Request body
- name (string, required)
Response
{ "branch": { "id": "<uuid>", "account_id": "<uuid>", "name": "Main St office (renamed)" } }Provision Number
POST /v1/phone-numbers
Provisions a new phone number for your account from CallFlowHQ's carrier inventory (toll-free today). No agent is assigned yet -- assign one with PATCH .../phone-numbers/:id below, or from the dashboard, before it can answer calls.
curl -X POST https://api.callflowhq.com/v1/phone-numbers \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"e164Number": "+18335550100", "friendlyName": "Main line", "callbackUrl": "https://example.com/cfhq-callback"}'Request body
- e164Number (string, required) -- the number to provision, in E.164 format (e.g. +18335550100)
- friendlyName (string, optional) -- a label shown on the Phone Numbers dashboard page
- callbackUrl (string, optional) -- an https:// URL to receive a single, signed POST the moment this number resolves to active or failed. One-off and specific to this request -- see "Getting notified" below for the other two options.
Response
201 on success, with the created phone number:
{
"phoneNumber": {
"id": "<uuid>",
"e164_number": "+18335550100",
"status": "provisioning",
"created_at": "2026-09-17T12:00:00.000Z"
}
}A new number starts in provisioning status and typically takes a few minutes to resolve to active (or, rarely, failed) once the carrier confirms it -- never assume it's immediately callable.
A 4xx response is a JSON body with an error message string -- for example, a trial account already at its 1-number limit (403), a missing e164Number (400), or a carrier provisioning failure (502).
Getting notified once it's ready
Three options, not mutually exclusive:
- Poll GET /phone-numbers and watch this number's status. Always works, no setup required.
- callbackUrl on this request (above) -- a single signed POST for just this number, no webhook registration needed.
- Subscribe to phone_number.active/phone_number.failed on a registered webhook (see Webhooks below) if you want every number on the account pushed automatically, not just ones provisioned with a callbackUrl.
callbackUrl is a best-effort, single-attempt delivery -- no retry if your endpoint is briefly unreachable at that moment. A registered webhook subscription retries up to 5 times (see Webhooks below). Either way, treat polling as the source of truth and the push options as a convenience on top of it.
List Phone Numbers
GET /v1/phone-numbers
Lists phone numbers provisioned for your account (see Account scoping & pagination above).
curl https://api.callflowhq.com/v1/phone-numbers \ -H "Authorization: Bearer cfhq_live_..."
{
"phoneNumbers": [
{
"id": "<uuid>",
"account_id": "<uuid>",
"e164_number": "+18335550100",
"friendly_name": "Main line",
"status": "active",
"agent_config_id": "<uuid>",
"created_at": "2026-09-17T12:00:00.000Z",
"released_at": null,
"purged_at": null
}
]
}Update Phone Number
PATCH /v1/phone-numbers/:id
Edits a phone number already in your account -- its label, the agent it's assigned to, its branch, and its escalation (transfer) number. Send only the fields you want to change.
curl -X PATCH https://api.callflowhq.com/v1/phone-numbers/<id> \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"friendlyName": "Sales line", "escalationPhoneNumber": "+18005550199", "agentConfigId": "<uuid>"}'Request body (all optional, at least one required)
- friendlyName (string, max 40 chars)
- escalationPhoneNumber (string, E.164 format) -- where a call transfers to. Required before an agent can be assigned.
- agentConfigId (string | null) -- must belong to the same account as this number
- branchId (string | null) -- must belong to the same account as this number
Response
{
"phoneNumber": {
"id": "<uuid>", "account_id": "<uuid>", "agent_config_id": "<uuid> | null",
"branch_id": "<uuid> | null", "escalation_phone_number": "+18005550199", "friendly_name": "Sales line"
}
}400 if agentConfigId/branchId don't belong to this number's account, or if you try to assign an agent with no escalation number set (on this call or already saved).
Reassign Number
POST /v1/phone-numbers/:id/reassign
Moves a phone number to a different account within your key's write scope (your own account, or one of its direct sub-accounts). Clears the number's assigned agent and branch on the move -- both belong to the old account, so you'll need to assign a new one with PATCH .../phone-numbers/:id afterward.
curl -X POST https://api.callflowhq.com/v1/phone-numbers/<id>/reassign \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"toAccountId": "<uuid>"}'Response
{ "phoneNumber": { "id": "<uuid>", "account_id": "<uuid>" } }Release Number
POST /v1/phone-numbers/:id/release
Releases (soft-deletes) an active phone number -- it stops answering calls immediately and can be restored from the dashboard within 7 days, after which it's permanently released back to the carrier.
curl -X POST https://api.callflowhq.com/v1/phone-numbers/<id>/release \ -H "Authorization: Bearer cfhq_live_..."
Response
{ "ok": true }403 on a trial account (upgrade first), 400 if the number isn't currently active.
List Agents
GET /v1/agent-configs
Lists your account's agents, oldest first.
{
"agentConfigs": [
{
"id": "<uuid>", "account_id": "<uuid>", "name": "Main agent",
"greeting_script": "Thanks for calling...", "voice_id": "...", "realtime_model": "default",
"qualification_fields": ["loan_purpose", "credit_band"],
"custom_qualification_fields": [{ "key": "referral_source", "label": "Referral source" }],
"created_at": "2026-09-17T12:00:00.000Z", "updated_at": "2026-09-17T12:00:00.000Z"
}
]
}Create Agent
POST /v1/agent-configs
Creates a new agent (and the live assistant behind it) on your account or one of its direct sub-accounts. A phone number doesn't use this agent until you assign it with PATCH .../phone-numbers/:id.
curl -X POST https://api.callflowhq.com/v1/agent-configs \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Purchase agent",
"identity": "You are a mortgage intake assistant for Sunshine Mortgage...",
"style": "Warm, concise, never pushy...",
"taskFlow": "1. Greet and disclose you are an AI. 2. Ask loan purpose...",
"qualificationFields": ["loan_purpose", "credit_band", "timeline"]
}'Request body
- identity, style, taskFlow (strings, required, non-empty) -- the agent's core prompt sections
- name, firstMessage, opening, toolsGuidance, complianceGuardrails, specialInstructions, errorHandling (strings, all optional)
- voiceId (string, optional) -- must be a supported voice
- qualificationFields (string[], optional) -- can't include SSN or date-of-birth
- customFieldLabels (string[], optional) -- free-text labels for account-defined fields, e.g. ["Referral source"]
- toolIds (string[], optional) -- ids of tools from the Tools page to attach
- accountId (string, optional) -- defaults to your key's own account
Response
201 with the created agent, or a 400/502 if the underlying assistant couldn't be created (nothing is left half-saved either way).
Update Agent
PATCH /v1/agent-configs/:id
Partial update -- send only the fields you want to change, using the same field names as POST /agent-configs above. The live assistant is re-synced automatically whenever a prompt section, voice, tools, or qualification fields change.
curl -X PATCH https://api.callflowhq.com/v1/agent-configs/<id> \
-H "Authorization: Bearer cfhq_live_..." \
-H "Content-Type: application/json" \
-d '{"style": "Warmer, and never mention rates or fees directly."}'Response
{ "ok": true }502 if the DB save succeeded but the live assistant sync failed -- safe to retry the same PATCH.
Delete Agent
DELETE /v1/agent-configs/:id
Soft-deletes an agent. Any phone number still assigned to it stops using it -- reassign those numbers first if you don't want them to go unanswered by an agent.
curl -X DELETE https://api.callflowhq.com/v1/agent-configs/<id> \ -H "Authorization: Bearer cfhq_live_..."
Response
{ "ok": true }List Calls
GET /v1/calls
Lists calls, newest first. Filter with ?phoneNumber= (matches the CallFlowHQ number that was called) and/or ?startDate=&endDate= (YYYY-MM-DD, inclusive, UTC).
curl "https://api.callflowhq.com/v1/calls?startDate=2026-09-01&endDate=2026-09-30&phoneNumber=%2B18335550100" \ -H "Authorization: Bearer cfhq_live_..."
{
"calls": [
{
"id": "<uuid>",
"account_id": "<uuid>",
"direction": "inbound",
"from_number": "+18005550100",
"to_number": "+18335550100",
"status": "completed",
"disposition": "answered",
"started_at": "2026-09-17T12:00:00.000Z",
"ended_at": "2026-09-17T12:04:12.000Z",
"duration_seconds": 231,
"cost_cents": 58,
"end_reason_label": "Transferred to a team member",
"created_at": "2026-09-17T12:00:00.000Z"
}
],
"limit": 50,
"offset": 0
}Get Call
GET /v1/calls/:id
Fetches one call with its transcript, transfer attempts, compliance flags, and a recording URL (a signed link, valid for 1 hour, or null if there's no recording).
{
"call": {
"id": "<uuid>", "account_id": "<uuid>", "direction": "inbound",
"from_number": "+18005550100", "to_number": "+18335550100",
"status": "completed", "disposition": "answered",
"started_at": "2026-09-17T12:00:00.000Z", "ended_at": "2026-09-17T12:04:12.000Z",
"duration_seconds": 231, "cost_cents": 58, "end_reason_label": "Transferred to a team member",
"created_at": "2026-09-17T12:00:00.000Z",
"recording_url": "https://.../recordings/....wav?token=..."
},
"transcript": { "full_text": "...", "segments": [ { "speaker": "agent", "start_ms": 0, "end_ms": 1200, "text": "...", "confidence": 0.98 } ] },
"transfers": [
{ "id": "<uuid>", "attempt_number": 1, "to_user_id": null, "to_user_name": null, "to_phone_number": "+18005550199",
"fallback_action": null, "status": "connected",
"initiated_at": "...", "connected_at": "...", "ended_at": "..." }
],
"compliance_flags": [
{ "id": "<uuid>", "rule_code": "RATE_QUOTE_ATTEMPT", "severity": "blocking", "status": "resolved",
"raised_at": "...", "resolution_notes": "..." }
]
}A call outside your account's rollup, or a nonexistent id, returns 404.
List Leads
GET /v1/leads
Lists leads, newest first -- including leads with no call_id (source: "api" or "webhook", captured before any call happened).
{
"leads": [
{
"id": "<uuid>", "account_id": "<uuid>", "call_id": "<uuid> | null",
"first_name": "Jamie", "last_name": "Rivera", "phone": "+18005550100", "email": "jamie@example.com",
"loan_purpose": "purchase", "credit_band": "680-719", "income_band": "$75k-$100k", "timeline": "30-60 days",
"property_details": { "address": "..." },
"source": "call", "status": "qualified",
"custom_field_answers": { "referral_source": "Google search" },
"created_at": "2026-09-17T12:00:00.000Z"
}
],
"limit": 50,
"offset": 0
}Get Lead
GET /v1/leads/:id
Fetches one lead with the same fields. A lead outside your account's rollup, or a nonexistent id, returns 404.
List Compliance Flags
GET /v1/compliance-flags
Lists compliance flags raised on your calls, newest first. Filter with ?status=open|reviewed|resolved and/or ?severity=info|warning|blocking.
{
"complianceFlags": [
{
"id": "<uuid>", "account_id": "<uuid>", "call_id": "<uuid>",
"rule_code": "CONSENT_NOT_VERIFIED", "severity": "blocking", "raised_by": "system",
"status": "open", "assigned_reviewer_id": null, "resolution_notes": null,
"raised_at": "2026-09-17T12:00:00.000Z", "reviewed_at": null, "resolved_at": null
}
],
"limit": 50,
"offset": 0
}List DNC Entries
GET /v1/dnc-entries
Lists do-not-call flags -- a caller who asked to stop being contacted -- newest first. Raised automatically mid-call, so this is read-only: there's no create/delete endpoint.
{
"dncEntries": [
{
"id": "<uuid>", "account_id": "<uuid>", "call_id": "<uuid> | null",
"phone": "+18005550100", "reference_number": "MTG-48213", "caller_name": "Jamie Rivera",
"created_at": "2026-09-17T12:00:00.000Z"
}
],
"limit": 50,
"offset": 0
}List Appointments
GET /v1/appointments
Lists booked appointments, newest first. Filter with ?status=booked|canceled|completed.
{
"appointments": [
{
"id": "<uuid>", "account_id": "<uuid>", "scheduled_at": "2026-09-20T15:00:00.000Z",
"duration_minutes": 30, "caller_name": "Jamie Rivera", "caller_phone": "+18005550100",
"email": "jamie@example.com", "status": "booked"
}
],
"limit": 50,
"offset": 0
}Webhooks
Register an endpoint from the Developers page to get a signed POST whenever one of these happens on your account:
- call.completed -- a call finished
- lead.created -- a new lead was captured
- compliance_flag.raised -- a compliance flag was raised on a call
- transfer.missed -- a warm transfer attempt to a loan officer wasn't answered
- transfer.recording_ready -- a transferred call's recording and transcript for the transferred segment are both available (fetch them via GET /api/v1/calls/:callId; typically a couple of minutes after the call ends)
- appointment.booked -- a caller booked an appointment
- dnc.flagged -- a caller asked to stop being contacted
- phone_number.active -- a provisioned number finished carrier setup and can take calls
- phone_number.failed -- a provisioned number failed carrier setup
Every delivery is a POST with this envelope:
{
"id": "<delivery id>",
"event_type": "call.completed",
"version": "v1",
"created_at": "2026-09-17T12:00:00.000Z",
"data": { ... event-specific fields ... }
}Each request carries an X-CallFlowHQ-Signature header: t=<unix timestamp>,v1=<hex hmac-sha256>. Verify it by computing an HMAC-SHA256 of ${timestamp}.${raw request body} using your webhook's signing secret (shown once, when you create the webhook), and comparing it to the v1= value. Reject anything where your computed timestamp is more than a few minutes old, to protect against a replayed payload.
A non-2xx response is retried up to 5 times; after 5 consecutive failures the webhook is automatically disabled (re-enable it from the Developers page once your endpoint is fixed).