Skip to content

Debts & leads

Generated from DialerDigital/core commit 6e9f026f0a72 — spec openapi/dialer-v1.yaml v0.36.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Bulk lead import, debt detail, per-debt CDR, defense packet.

Endpoints

MethodPathSummary
GET/v1/debtsAccount list + search (filters, keyset pagination)
POST/v1/debts/importBulk JSON lead import (idempotent)
GET/v1/debts/{id}Debt + contacts
GET/v1/debts/{id}/call_attemptsDurable per-debt CDR history
GET/v1/debts/{id}/defense-packetLitigation Defense Packet (sealed evidence bundle)

Account list + search (filters, keyset pagination)

http
GET /v1/debts

One page of the tenant's accounts, read from the wide debts_wide view. Tenant-scoped: a debt of another tenant is never returned by any filter or search term — an id, a consumer_ref or a phone that belongs elsewhere yields an EMPTY page, never a 403 (existence is not confirmed).

Phone numbers come back MASKED (primary_phone_masked, *** + last four). A list is a sweep surface; the full E.164 of ONE account stays on GET /v1/debts/{id}. account_number is not part of this projection at all.

phone searches by deterministic blind index and therefore requires a normalized E.164 (+13125550100); anything else is a 400, not an empty page. consumer_ref and external_ref match EXACTLY.

dialable_by selects the accounts dialable at that instant: a debt the disposition engine has not parked (next_dialable_at null) counts as dialable now and is included.

Parameters

NameInTypeRequiredDescription
campaign_idquerystring (uuid)no
statequeryopen · in_collection · promise_to_pay · paid · settled · disputed · closednoExact debt state. An unknown value is a 400, not an empty page.
min_amount_centsqueryintegernoLower bound (inclusive) on the balance, in cents.
max_amount_centsqueryintegernoUpper bound (inclusive) on the balance, in cents.
dialable_byquerystring (date-time)noRFC-3339 instant; returns accounts dialable at or before it.
phonequerystringnoNormalized E.164 of any contact of the debt. Percent-encode the leading + as %2B: it is a reserved query-string character that decodes to a SPACE, so an unescaped number arrives mangled and the request is refused with a 400 rather than silently searching for something else.
consumer_refquerystringnoExact CMS consumer id.
external_refquerystringnoExact CMS debt id.
limitqueryintegernoPage size (default 50, max 200).
cursorquerystringnoOpaque keyset cursor from the previous page's next_cursor.

Responses

200

One page of accounts.

FieldTypeRequiredDescription
debtsarray of DebtSummaryyes
debts[].idstring (uuid)no
debts[].tenant_idstring (uuid)no
debts[].campaign_idstring (uuid) | nullno
debts[].campaign_namestring | nullno
debts[].external_refstringno
debts[].consumer_refstringno
debts[].debt_typeother · student_loanno
debts[].amount_centsintegerno
debts[].currencystringno
debts[].stateopen · in_collection · promise_to_pay · paid · settled · disputed · closedno
debts[].last_conversation_atstring (date-time) | nullno
debts[].next_dialable_atstring (date-time) | nullnoNull means the debt is dialable now (not parked by the disposition engine).
debts[].primary_phone_maskedstring | nullno*** + last four of the primary contact; null when the debt has no contact.
debts[].primary_line_typemobile · landline · voip · unknown | nullno
debts[].primary_timezonestring | nullno
debts[].primary_us_statestring | nullno
debts[].primary_citystring | nullno
debts[].contact_countintegerno
debts[].created_atstring (date-time)no
debts[].updated_atstring (date-time)no
next_cursorstring | nullyesOpaque keyset cursor; null on the last page.

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Bulk JSON lead import (idempotent)

http
POST /v1/debts/import

Per-item fail-soft, max 10 000 items per request and max 100 contacts per debt. Debts upsert on external_ref (THE Reg F counter key), contacts on phone_e164. Replaying the same payload is safe.

An unrecognized field in an item — or in one of its contacts — fails THAT item and nothing else: it comes back in failed with the item's own index and a message naming the key, while every other item still imports. A bad contact is located by its position inside the item (contact 0: ...). Fail-soft is why this route answers per item instead of rejecting the whole body with a 400 the way the single-object routes do.

A contact sent with is_primary: true asserts which number is THE primary for that debt: the previous primary is demoted in the same transaction and kept (numbers are never deleted), and the new one is inserted or promoted. Omitting is_primary, or sending false, never changes the existing primary. An item carrying more than one is_primary: true fails THAT item and writes nothing — the correction is ambiguous and last-one-wins would lose it silently.

The import is ADDITIVE on a contact's data fields, and they are TRI-STATE. Omitting a field and sending it as "" (or as blanks) mean the same thing — "the feed did not say" — and the stored value is left exactly as it was; a re-import can therefore CORRECT a value but never CLEAR one. Sending the field as null is not a clear either: it FAILS that item, with a message naming the field, and nothing is written for it. There is no verb on this route that empties a stored field, and that is deliberate: "" is what an exporter puts in every blank cell of a bulk feed, so reading it as "erase" would let a routine export silently drop the state and ZIP that decide the NYC per-debt cap. line_type: "unknown" is NOT an empty value — it is a published member of the enum, so the feed IS reclassifying the number and it lands.

Request body (JSON, required)

FieldTypeRequiredDescription
debtsarray of DebtImportItemyes
debts[].external_refstringyesTHE Reg F counter key (upsert key).
debts[].consumer_refstringyes
debts[].debt_typeother · student_loanno
debts[].account_numberstringno
debts[].amount_centsintegerno
debts[].currencystringno
debts[].stateopen · in_collection · promise_to_pay · paid · settled · disputed · closedno
debts[].campaign_idstring (uuid)no
debts[].prior_attempts_7dintegernoHow many calls about THIS debt the client already placed from its previous system in the last 7 days. Without it the Reg F 7-in-7 counter starts BLIND on a migrated portfolio — it only ever saw the attempts this platform placed — and a book of business moved mid-cycle can burn the legal cap in its first week. Materialized as attempt rows the gate already reads, so the gate itself is unchanged. It is a TOTAL for the window, not a delta: re-sending the same payload adds nothing, and re-sending a SMALLER number never retracts history (the count only goes up, like last_conversation_at). Values above 7 are stored as 7 — 7 already saturates every frequency cap the engine has, so the verdict is identical and the cap only bounds the write. A value that is not an integer >= 0 fails THAT item, like any other bad field on this route; omitting it changes nothing.
debts[].contactsarray of objectno
debts[].contacts[].phone_e164stringyesE.164 (upsert key within the debt).
debts[].contacts[].line_typemobile · landline · voip · unknownno
debts[].contacts[].timezonestringnoIANA.
debts[].contacts[].us_statestringno
debts[].contacts[].citystringno
debts[].contacts[].postal_codestringnoZIP of the consumer. It is what resolves the NYC borough for the per-debt cap: without it a NY contact cannot be ruled OUT of the city and gets the strict cap. Refreshed on re-import only when the payload carries it — omitting it, or sending it as "", never clears a stored value, and sending it as null fails the item. Same rule as consumer_ref, timezone, us_state and city: the contract is one, even though this column is the only nullable one of the six.
debts[].contacts[].is_primarybooleannoAsserts THE primary contact of the debt. On import, true demotes the previous primary (kept, never deleted) and promotes this one; omitted or false leaves the existing primary untouched. More than one per item fails the item.
debts[].contacts[].consumer_refstringno
json
{
  "debts": [
    {
      "external_ref": "cms-debt-0001",
      "consumer_ref": "cms-consumer-9001",
      "debt_type": "other",
      "account_number": "ACC-1001",
      "amount_cents": 125000,
      "currency": "USD",
      "campaign_id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99",
      "contacts": [
        {
          "phone_e164": "+13125551001",
          "line_type": "mobile",
          "timezone": "America/Chicago",
          "us_state": "IL",
          "city": "Chicago",
          "is_primary": true
        }
      ]
    }
  ]
}

Responses

200

Import outcome (per-item fail-soft).

FieldTypeRequiredDescription
importedintegeryes
failedarray of objectyes
failed[].indexintegerno
failed[].errorstringno
json
{
  "imported": 1,
  "failed": []
}

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Debt + contacts

http
GET /v1/debts/{id}

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The debt with its contacts.

FieldTypeRequiredDescription
debtobject (Debt)yes
debt.idstring (uuid)no
debt.tenant_idstring (uuid)no
debt.campaign_idstring (uuid) | nullno
debt.external_refstringno
debt.consumer_refstringno
debt.debt_typeother · student_loanno
debt.account_numberstringno
debt.amount_centsintegerno
debt.currencystringno
debt.stateopen · in_collection · promise_to_pay · paid · settled · disputed · closedno
debt.last_conversation_atstring (date-time) | nullno
debt.contactsarray of ContactnoPresent on GET /v1/debts/{id} only.
debt.contacts[].idstring (uuid)no
debt.contacts[].debt_idstring (uuid)no
debt.contacts[].consumer_refstringno
debt.contacts[].phone_e164stringno
debt.contacts[].line_typemobile · landline · voip · unknownno
debt.contacts[].timezonestring | nullno
debt.contacts[].us_statestring | nullno
debt.contacts[].citystring | nullno
debt.contacts[].postal_codestring | nullno
debt.contacts[].is_primarybooleanno
debt.created_atstring (date-time)no
debt.updated_atstring (date-time)no

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Durable per-debt CDR history

http
GET /v1/debts/{id}/call_attempts

Full durable history of the debt (originals + compensating corrections), keyset-paginated. Accepts the same filters as GET /v1/call_attempts; the path debt_id always wins over any ?debt_id= in the query.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
campaign_idquerystring (uuid)no
agent_idquerystring (uuid)no
dispositionquerystringnoExact match against an OPEN vocabulary: call_attempts.disposition is written VERBATIM (20260725000003_create_disposition_engine.exs) and the seven engine values — answered_human, abandoned, busy, no_answer, canceled, rejected, failed — are a NAMED SUBSET of it, not its boundary: typed agent/AI outcomes (promise_to_pay, wrong_number) and platform markers (not_placed) are equally valid. An unknown code answers 200 with zero rows, never 400. What IS 400 is a malformed SHAPE: the empty string, and the parameter repeated or sent in array form.
fromquerystring (date-time)noRFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted).
toquerystring (date-time)noRFC 3339 upper bound.
limitqueryintegernoPage size (default 100, max 1000).
cursorquerystringnoOpaque keyset cursor from the previous page's next_cursor.

Responses

200

One page of CDR rows.

FieldTypeRequiredDescription
call_attemptsarray of CallAttemptyes
call_attempts[].idstring (uuid)no
call_attempts[].tenant_idstring (uuid)no
call_attempts[].debt_idstring (uuid)no
call_attempts[].campaign_idstring (uuid) | nullno
call_attempts[].agent_idstring (uuid) | nullno
call_attempts[].consumer_refstringno
call_attempts[].debt_keystringno
call_attempts[].call_uuidstring | nullno
call_attempts[].from_numberstring | nullno
call_attempts[].to_numberstringno
call_attempts[].started_atstring (date-time) | nullno
call_attempts[].answered_atstring (date-time) | nullno
call_attempts[].ended_atstring (date-time) | nullno
call_attempts[].dispositionstring | nullnoEngine dispositions: answered_human|abandoned|busy|no_answer|canceled|rejected|failed; typed agent/AI outcomes are free-form (e.g. promise_to_pay).
call_attempts[].hangup_causestring | nullno
call_attempts[].sip_response_codeinteger | nullno
call_attempts[].compliance_snapshotobject | nullnoFrozen decision evidence at originate time.
call_attempts[].amd_verdictstring | nullno
call_attempts[].recording_uristring | nullnoWhere the call recording landed (object path/URI); null when the call was not recorded.
call_attempts[].ai_usedbooleannoWhether the voice-AI bot ran on this call (the AI add-on meter key).
call_attempts[].notestring | nullnoThe agent's free-text note about the call, entered at disposition time on ANY disposition. It rides on the correction record that carries the disposition, so the ORIGINAL row is normally null. Encrypted at rest (per-tenant envelope) and never queried by content. null = no note, or a record written before the column existed.
call_attempts[].corrects_idstring (uuid) | nullno
call_attempts[].inserted_atstring (date-time)no
next_cursorstring | nullyesOpaque keyset cursor; null on the last page.

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Litigation Defense Packet (sealed evidence bundle)

http
GET /v1/debts/{id}/defense-packet

ONE sealed JSON evidence bundle per debt, assembled in a single tenant transaction: full immutable CDR history with frozen compliance snapshots, gate refusals, consent/DNC/C&D state, promises, callbacks, supervision audit and the policy versions in force. integrity.digest is a SHA-256 over the canonical JSON WITHOUT the integrity key (object keys sorted bytewise, no insignificant whitespace — any third party can re-verify; recipe in API.md). With ?format=zip (or Accept: application/zip) ships a ZIP of packet.json + summary.txt with Content-Disposition: attachment and X-Artifact-SHA256 over the ZIP bytes.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
formatqueryzipnozip for the downloadable artifact (equivalent: Accept: application/zip).

Responses

200

The sealed evidence bundle (JSON envelope or ZIP artifact).

FieldTypeRequiredDescription
packet"litigation_defense"no
packet_versionintegerno
tenant_idstring (uuid)no
packet_generated_atstring (date-time)no
debtobjectno
contactsarray of objectno
call_attemptsarray of objectnoFULL history, originals + corrections, each with the frozen compliance_snapshot.
conversationsarray of objectnoReg F G5 contact anchors.
gate_blocksarray of objectnoEvery durable refusal (frozen reason_detail).
consentsarray of objectno
dncobjectno
dnc.tenant_listingsarray of objectno
dnc.global_listingsarray of objectno
dnc.reassigned_numbersarray of objectno
cease_and_desistarray of objectnoConsumer-wide ("" scope) + debt-scoped.
promisesarray of objectno
callbacksarray of objectno
supervision_actionsarray of objectno
policy_versionsarray of stringno
integrityobjectno
integrity.algorithm"sha256"no
integrity.digeststringnoHex sha256 over the canonical envelope without integrity.
integrity.canonicalizationstringno
integrity.worm_attestationobject | nullno
json
{
  "packet": "litigation_defense",
  "packet_version": 1,
  "tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
  "packet_generated_at": "2026-07-05T10:00:00Z",
  "debt": {
    "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
    "external_ref": "cms-debt-0001"
  },
  "contacts": [],
  "call_attempts": [],
  "conversations": [],
  "gate_blocks": [],
  "consents": [],
  "dnc": {
    "tenant_listings": [],
    "global_listings": [],
    "reassigned_numbers": []
  },
  "cease_and_desist": [],
  "promises": [],
  "callbacks": [],
  "supervision_actions": [],
  "policy_versions": [
    "usa-cell.v1"
  ],
  "integrity": {
    "algorithm": "sha256",
    "digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "canonicalization": "sorted-keys utf-8, no insignificant whitespace",
    "worm_attestation": null
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.