Docs · REST API

API Reference

Debt Digest exposes two HTTPS JSON surfaces: the creditor portal uses /api/v1/, while the integration API uses /external/v1. Portal requests use short-lived bearer tokens; integrations use live per-creditor API keys.

Sample · place an account
POST /api/v1/creditor/place-account
Authorization: Bearer <token>
Content-Type: application/json

{
  "consumerEmail": "member@example.com",
  "originalCreditor": "Riverbend Federal Credit Union",
  "principalBalance": 5247.63
}

This placement endpoint requires the consumer profile to already exist. To establish a new or unclaimed consumer profile, use verified receivable activation below.

Overview

The creditor portal uses the versioned /api/v1/ alias. The separately mounted integration surface uses /external/v1 and is authenticated by per-creditor API keys, which are live and managed in portal Settings.

Request IDs support tracing and support investigations; they do not make a write safe to replay. Writes that implement replay protection require a stable Idempotency-Key, as stated on the individual endpoint.

Versioning. Use /api/v1/<path> for portal endpoints. The unversioned /api/<path> form still works as a transitional alias but returns RFC 8594 Deprecation + Sunset + Link headers. Sunset for unversioned: 2027-05-03 (365-day grace). The /external/v1 integration API, webhooks, healthcheck, and internal endpoints keep their fixed URLs. Full policy: docs/API_VERSIONING.md.

Authentication

For the creditor portal, log in at POST /api/auth/creditor-login, receive an access token, and send it as a bearer token on every request:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Access tokens expire after 15 minutes. A refresh token lasts 7 days; rotate it with POST /api/auth/refresh. If refresh-token storage is unavailable, login falls back to a legacy 4-hour access token without a refresh token.

For service-to-service integrations, per-creditor API keys are live on /external/v1. Mint, list, and revoke them from creditor portal Settings.

Errors

Standard shape:

Sample · error response
{
  "error": "Insufficient permissions",
  "code": "FORBIDDEN",
  "requestId": "01h8k2rjm7c3q4v5w6x7y8z9ab"
}
StatusCodeMeaning
400VALIDATION_ERRORRequest body or query failed validation. See details field.
401AUTH_ERRORMissing, invalid, or expired authentication.
403FORBIDDENAuthenticated but not permitted.
404NOT_FOUNDResource does not exist or you cannot see it.
409CONFLICTState conflict (e.g., settlement already accepted).
422VariesThe code identifies the business-rule or screening failure; inspect the response rather than assuming one standard code.
429RATE_LIMITEDToo many requests. Honor Retry-After header.
500INTERNAL_ERRORAn unexpected server error occurred. Share the response requestId with support.

Rate limits

  • 60 requests/min/IP on general API.
  • 5 requests/min/IP on /api/auth/* endpoints.
  • List-endpoint limit query parameter is clamped server-side to 100.

Performance targets

We track and publish response-time percentiles. The health commitments below are SLOs, not SLAs - we use them to decide what to fix first, and share the numbers on request during pilots.

MetricTargetWhat happens if we miss
p50 latency (median)< 150msLogged internally, reviewed weekly
p95 latency< 500msFlags degraded status on /status
p99 latency< 1500msTriggers slow-query investigation
5xx error rate< 0.5% of requestsEmail to pilot contact within 24h
Uptime99.9% monthlySee /status - incidents enumerated there

Queries running ≥1s are logged automatically with a truncated SQL preview so we can find N+1 offenders before they affect you.

Public REST API (v1)

The /external/v1 surface is the integration API for creditor systems that push accounts to Debt Digest and pull state back into a system of record. It is authenticated with an API key (not a portal session), tenant-scoped to the creditor that owns the key, and rate-limited per key.

Base URL: https://www.debt-digest.com/external/v1/

Mounted separately from the portal API. Portal clients use /api/v1/creditor/*, which the server maps to its internal /api/creditor/* handlers. Integrations use the distinct /external/v1 prefix and are not affected by that portal alias rewrite.

API keys

Mint, list, and revoke keys from the Settings tab of the creditor portal. Minting requires the admin role. Each raw key is shown exactly once: store it in your secret manager immediately; it is unrecoverable.

Authorization: Bearer 1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890

Rate limit: 100 requests per 15 minutes per API key. Exceeded requests return 429 with a Retry-After: 900 response header.

Tenant isolation: every query is scoped to the creditor the key resolves to. Account IDs that belong to another creditor read as 404, identical to a non-existent ID, so cross-tenant existence cannot be inferred.

Public API endpoints

Identity

GET/external/v1/me

Returns the creditor profile owning the key plus the key's own metadata (id, name, prefix, last-used, created-at). Confirms key ownership during setup.

Portfolio metrics

GET/external/v1/metrics

Aggregate counts, total balance, resolution count, resolution rate, and a status histogram across every account.

List accounts

GET/external/v1/accounts?page=1&limit=20&status=contacted

limit is clamped to 100. status is optional and filters to one of the public account statuses.

Get one account

GET/external/v1/accounts/:id

Returns the account row plus every note attached to it.

Activate a verified receivable

POST/external/v1/receivables/activate
POST/external/v1/actions/activate-receivable (Zapier)
Sample · request body
{
  "externalReceivableId": "RBFCU-AUTO-2026-00123",
  "consumerEmail": "member@example.com",
  "consumerFirstName": "Jordan",
  "consumerLastName": "Rivera",
  "consumerLast4Ssn": "4321",
  "consumerPhone": "+15125550199",
  "consumerDateOfBirth": "1984-09-17",
  "consumerState": "TX",
  "originalCreditor": "Riverbend Federal Credit Union",
  "originalBalance": 4820.55,
  "principalBalance": 4820.55,
  "pastDueAmount": 410.25,
  "daysPastDue": 78,
  "statementData": {
    "statementDate": "2026-05-31",
    "minimumPayment": 145
  },
  "chargeOffDate": "2026-05-15",
  "accountPhase": "phase1",
  "authority": {
    "type": "original_creditor",
    "evidenceType": "business_records",
    "evidenceReference": "member-loan-ledger-00123",
    "attested": true
  }
}

Send a stable externalReceivableId in the body and a stable Idempotency-Key header. The source ID is stored only as a tenant-bound digest and prevents a second key or batch from activating the same debt. A successful activation establishes or reuses the consumer profile, then creates the account, core participant graph, and first Receivable Passport event atomically after legal, subscription-capacity, identity, authority, and screening gates pass. For phase 1, it generates and queues validation information. Activation sends no outreach and records no consumer consent.

Legacy single-account import

POST/external/v1/accounts

This REST compatibility path requires consumerEmail and a positive principalBalance. It accepts originalBalance, firstName, lastName, last4ssn, originalCreditor, chargeOffDate, pastDueAmount, daysPastDue, dateOfBirth, and state (or addressState). The phase field defaults to phase1; a phase0 placement is subject to the state-licensing gate.

Sample · legacy REST request body
{
  "consumerEmail": "member@example.com",
  "firstName": "Jordan",
  "lastName": "Rivera",
  "last4ssn": "4321",
  "dateOfBirth": "1984-09-17",
  "state": "TX",
  "originalCreditor": "Riverbend Federal Credit Union",
  "originalBalance": 5247.63,
  "principalBalance": 5247.63,
  "pastDueAmount": 410.25,
  "daysPastDue": 78,
  "chargeOffDate": "2026-05-15",
  "phase": "phase1"
}

Legacy Zapier create-account action

POST/external/v1/actions/create-account (Zapier)

This Zapier action uses a different field contract. It requires email, firstName, lastName, and a positive balance (the balance may instead be supplied as originalBalance or principalBalance). It accepts phone, originalCreditor, last4ssn (or ssn4), dateOfBirth, state (or addressState), chargeOffDate, daysPastDue, pastDueAmount, and accountPhase. For a new consumer, identity screening needs the last four SSN digits. accountPhase defaults to phase0, which also needs a state for the licensing check; use phase1 when that workflow is intended.

Sample · Zapier create-account request body
{
  "email": "member@example.com",
  "firstName": "Jordan",
  "lastName": "Rivera",
  "phone": "+15125550199",
  "last4ssn": "4321",
  "dateOfBirth": "1984-09-17",
  "state": "TX",
  "originalCreditor": "Riverbend Federal Credit Union",
  "balance": 5247.63,
  "pastDueAmount": 410.25,
  "daysPastDue": 78,
  "chargeOffDate": "2026-05-15",
  "accountPhase": "phase0"
}

Compatibility paths for existing integrations. New integrations should use verified receivable activation above to establish the canonical authority, screening, participant, timeline, and idempotency record.

Requests that pass API-key authentication and the action rate-limit admission gate include an RFC 9745 Deprecation date plus Link relations to the canonical successor and migration guidance. Pre-handler 401 and 429 responses do not carry those headers. No Sunset is published: the paths remain operational until every active integration is migrated or explicitly retired and removal is approved.

Update an account

PATCH/external/v1/accounts/:id
{ "status": "contacted", "currentBalance": 4980.00 }

Partial update: at least one of status or currentBalance is required. Terminal statuses (settled, paid_in_full) and the consumer-only disputed status are workflow-gated and rejected with 400.

Set status only

POST/external/v1/accounts/:id/status

Equivalent to PATCH with status only; kept for clients that prefer a verb-style RPC shape.

Add a note

POST/external/v1/accounts/:id/note

Free-text note (≤ 5000 chars) appended to the account audit trail.

List settlement offers for an account

GET/external/v1/accounts/:id/offers

Returns every settlement offer attached to the account across all statuses (pending, countered, accepted, paid, rejected, expired).

List active webhooks

GET/external/v1/webhooks

Read-only mirror of the creditor portal webhook list. Signing secrets are never included; manage webhooks (create, test, delete) from the Settings tab.

Schema: the full OpenAPI 3.0 spec is at docs/openapi.yaml in the repo. Every /external/v1/* path lives under the Public API v1 tag, secured by the ApiKeyAuth scheme.

Accounts

List accounts

GET/api/v1/creditor/accounts?limit=25&cursor=&status=&search=

Paginated list. cursor for page-forward, status filter, search matches account ID or consumer last-4 SSN.

Place a single account

POST/api/v1/creditor/place-account
Sample · request body
{
  "consumerEmail": "member@example.com",
  "originalCreditor": "Riverbend Federal Credit Union",
  "originalBalance": 5280.00,
  "principalBalance": 5280.00,
  "chargeOffDate": "2026-05-15"
}

The consumer profile must already exist. To establish a new or unclaimed consumer profile, use verified receivable activation. A successful placement returns 201 with a nested account object; use account.id as the placed account identifier. The response also includes validation-notice metadata. Placement generates and queues the validation notice, but it does not send outreach; delivery requires the separate creditor-initiated send action.

Bulk upload

POST/api/v1/creditor/upload-accounts

Send an object whose accounts property is a non-empty array of up to 1,000 accounts per batch. Returns row-level success / failure information. See CSV schema for column definitions.

Sample · bulk request body
{
  "accounts": [
    {
      "consumerEmail": "member@example.com",
      "originalCreditor": "Riverbend Federal Credit Union",
      "originalBalance": 5280.00,
      "principalBalance": 5280.00,
      "chargeOffDate": "2026-05-15"
    }
  ]
}

Add account note

POST/api/v1/creditor/account-note

Settlement

Configure settlement matrix

PUT/api/creditor/settlement-matrix

Per-tier discount floors: lump_sum, 12_month, 24_month, 36_month. Values as decimals (0.70 = 70% of principal).

Accept a firm offer

POST/api/creditor/accept-settlement

Consumer resolutions

The borrower's side of the bilateral negotiation rail. An authenticated consumer can read the resolution options their creditor has authorized, propose a resolution, counter a standing offer, and accept one. Consumer actions write to the same resolutions state machine the creditor portal reads, so proposals and counters land in the creditor's review queue with no extra integration work.

Authentication: consumer JWT (borrower portal session). Every endpoint is scoped to the authenticated consumer's own accounts. An account or resolution ID that belongs to another consumer returns 404, identical to a non-existent ID.

Guardrails built into the surface:

  • Resolution types are gated per lifecycle stage by the creditor's policy. A type not permitted at the account's stage returns 403.
  • An offer amount cannot exceed the balance owed, and the stored discount is derived from the actual dollars offered, so an understated discountPct can never force a favorable auto-accept.
  • Proposals and counters are each rate-limited to 60 per hour per consumer.
  • A consumer cannot counter or accept their own still-open proposal (403).

List resolution options

GET/api/consumer/account/:id/resolution-options

Auth: consumer JWT. The creditor-authorized chooser (pay in full, payment plan, settlement) with the figures the creditor has approved for this account. Returns 200 with { "options": [...], "count": n }.

List resolutions on an account

GET/api/consumer/account/:id/resolutions

Auth: consumer JWT. The negotiation thread: every proposal, counter, and acceptance on the account. Returns 200 with { "resolutions": [...], "count": n }.

Propose a resolution

POST/api/consumer/account/:id/resolutions/propose

Auth: consumer JWT. The consumer makes an offer of record. resolutionType is required and must be one of bif, ppa, settlement_lump, settlement_struct. Optional bounds-checked fields: discountPct (0 to 100), totalAmount (greater than zero, at most the balance owed), termMonths (integer, 1 to 600), monthlyAmount (greater than zero).

Sample · request body
{
  "resolutionType": "settlement_lump",
  "totalAmount": 3675.00,
  "discountPct": 30
}
Sample · 201 response
{
  "resolution": {
    "id": "7f0a2c9e-31b4-4c8a-9f6d-2e8b5a1d4c3f",
    "account_id": "b2c4e6a8-0d1f-4a3b-8c5e-7f9a1b3d5e7c",
    "resolution_type": "settlement_lump",
    "status": "proposed",
    "proposed_by": "consumer",
    "discount_pct": 30,
    "total_amount": 3675.00,
    "auto_accepted": false,
    "matrix_floor_passed": false
  },
  "matrix": {
    "withinFloor": false,
    "autoAccepted": false,
    "reasons": ["discount_exceeds_floor"]
  }
}

Returns 201. An offer at or above the creditor's configured floor may auto-accept (status comes back accepted with auto_accepted: true); a below-floor offer stays proposed and routes to the creditor's review panel. The matrix object reports which path was taken.

Counter a standing offer

POST/api/consumer/resolutions/:id/counter

Auth: consumer JWT. Responds to the other party's standing offer with new terms. Body fields match propose; all are optional except that the same bounds checks apply. Countering your own still-open proposal returns 403.

Sample · 200 response
{
  "resolution": {
    "id": "7f0a2c9e-31b4-4c8a-9f6d-2e8b5a1d4c3f",
    "status": "countered",
    "proposed_by": "consumer",
    "discount_pct": 25,
    "total_amount": 3937.50
  }
}

Accept a standing offer

POST/api/consumer/resolutions/:id/accept

Auth: consumer JWT. Accepts the other party's standing offer as proposed. Accepting your own proposal returns 403; an offer that is no longer open returns 409.

Sample · 200 response
{
  "resolution": {
    "id": "7f0a2c9e-31b4-4c8a-9f6d-2e8b5a1d4c3f",
    "status": "accepted",
    "accepted_by": "consumer",
    "accepted_at": "2026-07-11T15:04:05.000Z"
  }
}

Webhooks

Register one or more creditor-managed HTTPS endpoints and select which canonical events each endpoint receives. Debt Digest POSTs signed JSON payloads; the signing secret is returned exactly once when an endpoint is registered. See the verification guide.

List and register endpoints

GET/api/v1/creditor/webhooks
POST/api/v1/creditor/webhooks

Event catalog

GET/api/v1/creditor/webhooks/event-types

Test, inspect, retry, or deactivate an endpoint

POST/api/v1/creditor/webhooks/:id/test
GET/api/v1/creditor/webhooks/:id/deliveries
POST/api/v1/creditor/webhooks/:id/deliveries/:deliveryId/retry
DELETE/api/v1/creditor/webhooks/:id
GET/api/v1/creditor/webhooks/log

Subscribable event types

  • account.imported
  • account.resolved
  • account.settled
  • account.status_changed
  • account.assigned
  • payment.received
  • offer.sent
  • offer.accepted
  • offer.declined
  • offer.expired
  • dispute.received
  • dispute.filed
  • hardship.submitted
  • cease_comm.received
  • payment_plan.accepted
  • plan.proposed
  • plan.approved
  • trust.disbursed

Audit log

View

GET/api/v1/creditor/audit?startDate=&endDate=&limit=

Export (CSV)

GET/api/v1/creditor/audit/export?startDate=&endDate=

Returns CSV suitable for NCUA exam submission or internal SOC 2 evidence.