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