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.
- Portal alias:
/api/v1/ - Integration API:
/external/v1 - Replay protection:
Idempotency-Keywhere supported - HMAC-signed webhooks
- RLS-enforced per creditor
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:
{
"error": "Insufficient permissions",
"code": "FORBIDDEN",
"requestId": "01h8k2rjm7c3q4v5w6x7y8z9ab"
}
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body or query failed validation. See details field. |
| 401 | AUTH_ERROR | Missing, invalid, or expired authentication. |
| 403 | FORBIDDEN | Authenticated but not permitted. |
| 404 | NOT_FOUND | Resource does not exist or you cannot see it. |
| 409 | CONFLICT | State conflict (e.g., settlement already accepted). |
| 422 | Varies | The code identifies the business-rule or screening failure; inspect the response rather than assuming one standard code. |
| 429 | RATE_LIMITED | Too many requests. Honor Retry-After header. |
| 500 | INTERNAL_ERROR | An 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
limitquery 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.
| Metric | Target | What happens if we miss |
|---|---|---|
p50 latency (median) | < 150ms | Logged internally, reviewed weekly |
p95 latency | < 500ms | Flags degraded status on /status |
p99 latency | < 1500ms | Triggers slow-query investigation |
| 5xx error rate | < 0.5% of requests | Email to pilot contact within 24h |
| Uptime | 99.9% monthly | See /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
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
Aggregate counts, total balance, resolution count, resolution rate, and a status histogram across every account.
List accounts
limit is clamped to 100. status is optional and filters to one of the public account statuses.
Get one account
Returns the account row plus every note attached to it.
Activate a verified receivable
{
"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
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.
{
"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
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.
{
"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
{ "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
Equivalent to PATCH with status only; kept for clients that prefer a verb-style RPC shape.
Add a note
Free-text note (≤ 5000 chars) appended to the account audit trail.
List settlement offers for an account
Returns every settlement offer attached to the account across all statuses (pending, countered, accepted, paid, rejected, expired).
List active 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
Paginated list. cursor for page-forward, status filter, search matches account ID or consumer last-4 SSN.
Place a single account
{
"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
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.
{
"accounts": [
{
"consumerEmail": "member@example.com",
"originalCreditor": "Riverbend Federal Credit Union",
"originalBalance": 5280.00,
"principalBalance": 5280.00,
"chargeOffDate": "2026-05-15"
}
]
}
Add account note
Settlement
Configure 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
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
discountPctcan 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
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
Auth: consumer JWT. The negotiation thread: every proposal, counter, and acceptance on the account. Returns 200 with { "resolutions": [...], "count": n }.
Propose a resolution
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).
{
"resolutionType": "settlement_lump",
"totalAmount": 3675.00,
"discountPct": 30
}
{
"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
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.
{
"resolution": {
"id": "7f0a2c9e-31b4-4c8a-9f6d-2e8b5a1d4c3f",
"status": "countered",
"proposed_by": "consumer",
"discount_pct": 25,
"total_amount": 3937.50
}
}
Accept a standing offer
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.
{
"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
Event catalog
Test, inspect, retry, or deactivate an endpoint
Subscribable event types
account.importedaccount.resolvedaccount.settledaccount.status_changedaccount.assignedpayment.receivedoffer.sentoffer.acceptedoffer.declinedoffer.expireddispute.receiveddispute.filedhardship.submittedcease_comm.receivedpayment_plan.acceptedplan.proposedplan.approvedtrust.disbursed
Audit log
View
Export (CSV)
Returns CSV suitable for NCUA exam submission or internal SOC 2 evidence.