# Bills API

Retrieve normalized bills and statement metadata for connected accounts.

> **Scope of this file.** It is the OPERATION reference for THIS resource, generated from the BillerAPI OpenAPI document served at [/docs/openapi.json](/docs/openapi.json): each API-key endpoint the resource owns, with its parameters, request body, responses and schemas. Narrative and catalog sections the rendered page also carries (walkthroughs, event taxonomies, payload catalogs) are NOT reproduced here — read [/docs/api/bills](/docs/api/bills) for those. Endpoints the hosted flow drives from inside the iframe, and dashboard-session endpoints, are absent by design: they are not API-key operations. [/docs/api.md](/docs/api.md) enumerates the full API-key surface, including the endpoints that have no reference page yet.

All request and response field names are snake_case. Errors use the coded envelope described in [/docs/concepts/errors.md](/docs/concepts/errors.md).

## Endpoints

- [`GET /v1/bills`](#get-v1bills)
- [`POST /v1/bills`](#post-v1bills)
- [`POST /v1/bills/backfill-payment-matches`](#post-v1billsbackfill-payment-matches)
- [`GET /v1/bills/{id}/transitions`](#get-v1billsidtransitions)
- [`GET /v1/bills/links/{linkId}/accounts/{accountId}/outstanding`](#get-v1billslinkslinkidaccountsaccountidoutstanding)
- [`GET /v1/bills/{id}/statement`](#get-v1billsidstatement)
- [`POST /v1/bills/{id}/statement/refresh`](#post-v1billsidstatementrefresh)
- [`GET /v1/bills/{id}/statement/refresh/{request_id}`](#get-v1billsidstatementrefreshrequest_id)
- [`POST /v1/bills/webhooks/test-events`](#post-v1billswebhookstest-events)
- [`POST /v1/bills/{id}/attest_external_payment`](#post-v1billsidattest_external_payment)

## GET /v1/bills

Get bills for account link

Retrieve bills filtered by account link and date range. Requires client credentials.

Operation ID: `getBills`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `account_link_id` | query | yes | string | — | Account link ID |
| `start_date` | query | no | string | — | Start date (ISO format) |
| `end_date` | query | no | string | — | End date (ISO format) |
| `limit` | query | no | number | — | Maximum number of bills to return (1-500) |
| `cursor` | query | no | string | — | Pagination cursor |
| `Idempotency-Key` | header | yes | string | — | Unique key for safely retrying this billable bill-fetch request. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | List of bills | `BillListResponseDto` |

Response body — `BillListResponseDto` (200)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `bills` | yes | array<PublicBillResponseDto> | — | — |
| `total_count` | yes | number | minimum `0` | Number of bills in this response page. This is page-bound, not the collection-wide total. |
| `has_more` | yes | boolean | — | Whether another page is available. |
| `next_cursor` | yes | string | — | Opaque cursor for the next page, or an empty string on the final page. |

## POST /v1/bills

Create an owner-scoped manual bill

Operation ID: `createClientBill`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `idempotency-key` | header | yes | string | — | — |

**Request body** (required)

Schema: `CreateClientBillDto`

| Field | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `client_user_id` | yes | string | — | — |
| `payee_name` | yes | string | — | — |
| `amount_due` | no | number | **deprecated** | Legacy major-unit amount. Prefer total_amount. |
| `currency` | no | string | **deprecated** | Legacy currency companion to amount_due. Prefer total_amount.currency. |
| `total_amount` | no | IsoMoneyInputDto | — | — |
| `due_date` | no | string | **deprecated** | Legacy date field. Prefer due_date_iso. |
| `due_date_iso` | no | string | — | Canonical ISO 8601 calendar date (YYYY-MM-DD). |
| `bill_number` | no | string | — | — |
| `account_number` | no | string | — | — |
| `reference_number` | no | string | — | — |
| `payment_url` | no | string | — | — |
| `description` | no | string | — | — |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 201 | Owner-scoped Bill created. | `CreateClientBillResponseDto` |

Response body — `CreateClientBillResponseDto` (201)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `bill` | yes | PublicBillResponseDto | — | — |
| `replayed` | yes | boolean | — | — |

## POST /v1/bills/backfill-payment-matches

Backfill bill-paid signals

One-shot replay of historical ObservedPayments + Statements through the matcher pipeline for an accountLinkId. Idempotent — safe to re-run.

Operation ID: `backfillBillPaymentMatches`

Auth: `client-api-key`

**Request body** (required)

Schema: `BackfillBillPaymentMatchesDto`

| Field | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `account_link_id` | yes | string | — | Account link to replay ObservedPayments + Statements for. |
| `account_id` | no | string | — | Narrow the replay to a single account on the link. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Backfill complete | `object` |

Response body — `object` (200)

Example:

```json
{
  "success": true,
  "observed_payments_processed": 24,
  "observed_payments_applied": 18,
  "statements_processed": 6,
  "statement_credits_applied": 3
}
```


## GET /v1/bills/{id}/transitions

List bill audit transitions

Returns the append-only timeline of every status flip on a bill, with the signal that drove each transition and an evidence reference (payment_id / statement_id / attempt_id).

Operation ID: `listBillTransitions`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Transition list | `object` |

Response body — `object` (200)

Example:

```json
{
  "transitions": [
    {
      "transition_id": "...",
      "bill_id": "b-1",
      "from_status": "PENDING",
      "to_status": "PAID",
      "signal": "OBSERVED_PAYMENT",
      "paid_reason": "OBSERVED_PAYMENT",
      "evidence_payment_id": "op-1",
      "occurred_at": "2026-05-29T14:00:00Z"
    }
  ],
  "next_cursor": "",
  "has_more": false
}
```


## GET /v1/bills/links/{linkId}/accounts/{accountId}/outstanding

Get carry-over-safe outstanding balance for an account

Anchors on the latest fresh Statement; falls back to sum-of-unpaid when no fresh statement exists. Avoids double-counting prior-balance carry-over on utility-style billers.

Operation ID: `getAccountOutstandingSummary`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `linkId` | path | yes | string | — | — |
| `accountId` | path | yes | string | — | — |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Outstanding summary | `object` |

Response body — `object` (200)

Example:

```json
{
  "outstanding_balance": 170,
  "currency": "USD",
  "as_of_statement_date": "2026-04-15",
  "freshness": "fresh",
  "freshness_hint_reason": "",
  "freshness_hint_action": "",
  "freshness_hint_retry_after": 0
}
```


## GET /v1/bills/{id}/statement

Get the statement for a bill

Returns the structured statement (period, balance breakdown, line items, biller metadata) for the bill. Returns 425 STATEMENT_NOT_EXTRACTED when extraction has not yet succeeded — call POST /:id/statement/refresh to trigger.

Operation ID: `getStatement`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Statement payload | `StatementDto` |
| 401 | Unauthorized | `ApiErrorDto` |
| 404 | Bill not found | `ApiErrorDto` |
| 425 | Statement not yet extracted | `ApiErrorDto` |

Response body — `StatementDto` (200)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `bill_id` | yes | string | — | — |
| `client_id` | yes | string | — | — |
| `version` | yes | number | — | Optimistic concurrency version. Increments on every refresh. |
| `extracted_at` | yes | string | — | When BillerAPI extracted this snapshot from the source document. Use this, not fetched_at — fetched_at was renamed pre-launch. |
| `stale` | yes | boolean | — | true when the statement is older than its TTL or upstream marked it stale; recipients should call POST /refresh. |
| `statement_period` | yes | StatementPeriodDto | — | — |
| `balance_breakdown` | yes | BalanceBreakdownDto | — | — |
| `biller_metadata` | yes | BillerMetadataDto | — | — |
| `line_items` | yes | array<LineItemDto> | — | Capped at 500 per statement for v1. |
| `completeness` | yes | StatementCompletenessDto | — | — |
| `overall_confidence` | yes | number | — | Aggregate confidence in [0, 1]. |

Response body — `ApiErrorDto` (401)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

Response body — `ApiErrorDto` (404)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

Response body — `ApiErrorDto` (425)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

## POST /v1/bills/{id}/statement/refresh

Refresh a bill statement

Triggers asynchronous statement extraction for the bill. Returns 202 immediately with a request_id; clients poll the request_id for completion (status endpoint TBD in a follow-up PR) or subscribe to bill.statement.refreshed webhooks. Idempotent: identical Idempotency-Key headers within 5 minutes return the same in-flight request_id.

Operation ID: `refreshStatement`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |

**Request body** (optional)

Schema: `RefreshStatementBodyDto`

| Field | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `source_hint` | no | `CREDENTIALED` \| `EMAIL` \| `BANK_FEED` \| `USER_UPLOAD` | — | Optional hint about which pipeline to prefer for the refresh. Best-effort. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 202 | Refresh accepted | `RefreshStatementResponseDto` |
| 401 | Unauthorized | `ApiErrorDto` |
| 404 | Bill not found | `ApiErrorDto` |
| 422 | Biller does not support refresh | `ApiErrorDto` |
| 429 | Rate limit exceeded | `ApiErrorDto` |

Response body — `RefreshStatementResponseDto` (202)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `request_id` | yes | string | — | — |
| `status` | yes | `pending` \| `complete` \| `failed` | — | — |
| `started_at` | yes | string | — | — |

Response body — `ApiErrorDto` (401)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

Response body — `ApiErrorDto` (404)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

Response body — `ApiErrorDto` (422)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

Response body — `ApiErrorDto` (429)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

## GET /v1/bills/{id}/statement/refresh/{request_id}

Get refresh status by request_id

Returns the lifecycle of a refresh dispatched via POST /:id/statement/refresh. Status is "pending" until the extraction worker completes; then "complete" or "failed". Records expire 24h after creation. Cross-tenant lookups return 404 (no existence leak).

Operation ID: `getRefreshStatus`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |
| `request_id` | path | yes | string | — | — |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | Refresh request status | — |
| 401 | Unauthorized | `ApiErrorDto` |
| 404 | Request not found or expired | `ApiErrorDto` |

Response body — `ApiErrorDto` (401)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

Response body — `ApiErrorDto` (404)

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

## POST /v1/bills/webhooks/test-events

Fire a synthetic webhook event (test-only)

Publishes a synthetic bill.paid / bill.partially_paid / bill.status_reverted event to the caller's registered webhook URL through the standard delivery pipeline (signing, retries, subscription filter).

Operation ID: `fireTestWebhookEvent`

Auth: `client-api-key`

**Request body** (required)

Schema: `FireTestWebhookEventDto`

| Field | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `event_type` | yes | string | — | bill.paid \| bill.partially_paid \| bill.status_reverted (dot.lowercase). |
| `bill_id` | yes | string | — | Bill the synthetic event refers to. |
| `link_id` | yes | string | — | Link the synthetic event refers to. |
| `biller_id` | no | string | — | Biller stamped onto the synthetic envelope. |
| `account_id` | no | string | — | Account stamped onto the synthetic envelope. |
| `revert_reason` | no | string | — | NEXT_STATEMENT_SHOWS_BALANCE \| MANUAL_ADMIN \| OBSERVED_PAYMENT_REVERSED (bill.status_reverted only). |
| `previous_status` | no | string | — | Prior bill status stamped onto the synthetic envelope. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 202 | Event published | `object` |

Response body — `object` (202)

Example:

```json
{
  "event_id": "evt_synth_1234",
  "event_type": "bill.paid"
}
```


## POST /v1/bills/{id}/attest_external_payment

Attest that an EXTERNAL_ONLY bill was paid elsewhere

Operation ID: `attestExternalPayment`

Auth: `client-api-key`

**Parameters**

| Parameter | In | Required | Type | Notes | Description |
| --- | --- | --- | --- | --- | --- |
| `id` | path | yes | string | — | — |
| `idempotency-key` | header | yes | string | — | — |

**Request body** (required)

Schema: `AttestExternalPaymentDto`

> The OpenAPI document declares this body but does not describe its fields, so this file cannot list them and will not guess. Read [/docs/api/bills](/docs/api/bills) for the fields this endpoint expects.

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 201 | — | — |

## Schemas

### ApiErrorDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `error_code` | yes | `BILL_NOT_FOUND` \| `INVALID_CURSOR` \| `ACCOUNT_LINK_NOT_FOUND` \| `STATEMENT_NOT_EXTRACTED` \| `EXTRACTION_FAILED` \| `EXTRACTION_UNSUPPORTED_FOR_BILLER` \| `STATEMENT_TOO_LARGE` \| `RATE_LIMITED` \| `UNAUTHORIZED` \| `CLIENT_CREATION_NOT_AVAILABLE` \| `COOKIE_MUTATION_POLICY_REJECTED` \| `SERVICE_UNAVAILABLE` \| `IDEMPOTENCY_KEY_MISMATCH` \| `IDEMPOTENCY_KEY_NOT_REPLAYABLE` \| `PAYMENT_EXECUTION_NOT_AVAILABLE` \| `FEEDBACK_RUN_NOT_FOUND` \| `FEEDBACK_RUN_NOT_OWNED` \| `FEEDBACK_RUN_EXPIRED` \| `FEEDBACK_INVALID_CATEGORY` \| `FEEDBACK_INVALID_SIGNAL` \| `FEEDBACK_ALREADY_SUBMITTED` \| `FEEDBACK_RATE_LIMITED` \| `MESSAGING_CONSENT_NOT_GRANTED` \| `MESSAGING_LIVE_ACCESS_REQUIRED` \| `MESSAGING_SENDER_NOT_AUTHORIZED` \| `MESSAGING_CONTENT_FLAGGED` \| `MESSAGING_RATE_LIMITED` \| `MESSAGING_SUPPRESSED` \| `MESSAGING_AUP_NOT_ACCEPTED` \| `MESSAGING_AUP_REACCEPT_REQUIRED` \| `MESSAGING_INVALID_PAYLOAD` \| `MESSAGING_INVALID_CATEGORY` \| `MESSAGING_PERSIST_FAILED` \| `MESSAGING_INTERNAL_ERROR` \| `MESSAGING_NOT_FOUND` \| `LINK_TOKEN_NOT_FOUND` \| `BACKGROUND_NOT_ELIGIBLE` \| `BILLER_UNSUPPORTED` \| `UPDATE_BILLER_MISMATCH` \| `INVALID_UPDATE_REASON` \| `LINK_UPDATE_FORBIDDEN` \| `LINK_NOT_FOUND` \| `BILLER_NOT_FOUND` \| `BILLER_CORRECTION_NOT_AVAILABLE` \| `REDIRECT_URI_NOT_REGISTERED` | — | Stable identifier — third parties branch on this. Additive-only contract. |
| `error_type` | yes | `invalid_request` \| `rate_limit` \| `auth` \| `upstream` \| `api_error` | — | Coarse category clients branch on for retry/backoff. |
| `error_message` | yes | string | — | Human-readable explanation. Safe to show end users in dev contexts. |
| `hint` | yes | string | — | Actionable next step for resolving this error. |
| `request_id` | yes | string | — | Correlation id (UUID) for support tickets. Also returned as the X-Request-Id response header on every response. |
| `docs_url` | yes | string | — | Canonical, LIVE docs page for this error code. |
| `retryable` | no | boolean | — | Whether retrying the same operation is supported. |
| `documentation_url` | no | string | **deprecated** | DEPRECATED alias for docs_url, retained for back-compat during the DX P0 migration window. Read docs_url instead — this field will be removed. |
| `retry_after` | no | number | — | On 429, the seconds the client should wait before retrying. |
| `errors` | no | array<unknown> | — | On 400 validation failures, the per-field problems. |

### AttestExternalPaymentDto

No object properties are declared for this schema in the OpenAPI document.

### BackfillBillPaymentMatchesDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `account_link_id` | yes | string | — | Account link to replay ObservedPayments + Statements for. |
| `account_id` | no | string | — | Narrow the replay to a single account on the link. |

### BalanceBreakdownDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `previous_balance` | no | number | — | — |
| `payments_credits` | no | number | — | Negative for credits. |
| `new_charges` | no | number | — | — |
| `fees` | no | number | — | — |
| `interest` | no | number | — | — |
| `taxes` | no | number | — | — |
| `current_balance` | yes | number | — | New balance / total currently due. |
| `minimum_payment_due` | yes | number | — | — |
| `due_date` | yes | string | — | Payment due date (YYYY-MM-DD). |
| `currency` | yes | string | — | #4040 PR4 — ISO-4217 currency for the whole statement. Every line item inherits this currency. |
| `reconciled` | no | boolean | — | #4040 PR3 — COMPUTED reconciliation flag (never the model self-report). true when the balance identity (previous_balance − \|payments_credits\| + new_charges + fees + interest + taxes ≈ current_balance) held within $0.01; false when it broke (the statement is downgraded to data_quality=PARTIAL + needs_review). Absent when there were not enough signed fields to compute it. |

### BillListResponseDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `bills` | yes | array<PublicBillResponseDto> | — | — |
| `total_count` | yes | number | minimum `0` | Number of bills in this response page. This is page-bound, not the collection-wide total. |
| `has_more` | yes | boolean | — | Whether another page is available. |
| `next_cursor` | yes | string | — | Opaque cursor for the next page, or an empty string on the final page. |

### BillerMetadataDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `masked_account_number` | yes | string | — | — |
| `autopay_enabled` | no | boolean | — | — |
| `paperless` | no | boolean | — | — |
| `plan_name` | no | string | — | — |

### CreateClientBillDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `client_user_id` | yes | string | — | — |
| `payee_name` | yes | string | — | — |
| `amount_due` | no | number | **deprecated** | Legacy major-unit amount. Prefer total_amount. |
| `currency` | no | string | **deprecated** | Legacy currency companion to amount_due. Prefer total_amount.currency. |
| `total_amount` | no | IsoMoneyInputDto | — | — |
| `due_date` | no | string | **deprecated** | Legacy date field. Prefer due_date_iso. |
| `due_date_iso` | no | string | — | Canonical ISO 8601 calendar date (YYYY-MM-DD). |
| `bill_number` | no | string | — | — |
| `account_number` | no | string | — | — |
| `reference_number` | no | string | — | — |
| `payment_url` | no | string | — | — |
| `description` | no | string | — | — |

### CreateClientBillResponseDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `bill` | yes | PublicBillResponseDto | — | — |
| `replayed` | yes | boolean | — | — |

### FireTestWebhookEventDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `event_type` | yes | string | — | bill.paid \| bill.partially_paid \| bill.status_reverted (dot.lowercase). |
| `bill_id` | yes | string | — | Bill the synthetic event refers to. |
| `link_id` | yes | string | — | Link the synthetic event refers to. |
| `biller_id` | no | string | — | Biller stamped onto the synthetic envelope. |
| `account_id` | no | string | — | Account stamped onto the synthetic envelope. |
| `revert_reason` | no | string | — | NEXT_STATEMENT_SHOWS_BALANCE \| MANUAL_ADMIN \| OBSERVED_PAYMENT_REVERSED (bill.status_reverted only). |
| `previous_status` | no | string | — | Prior bill status stamped onto the synthetic envelope. |

### Iso20022AmountResponseDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `value` | yes | string | — | — |
| `currency` | yes | string | — | — |

### IsoMoneyInputDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `value` | yes | string | — | Decimal string using the currency minor-unit exponent. |
| `currency` | yes | string | — | ISO 4217 currency code. |

### LineItemDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `description` | yes | string | — | Cleaned (trimmed, whitespace-collapsed). |
| `description_raw` | yes | string | — | #4040 PR4 — verbatim source text. |
| `posted_date` | yes | string | — | — |
| `amount` | yes | number | — | #4040 PR4 — ABSOLUTE value, always >= 0. The signed value is derived from `kind` (CREDIT/PAYMENT/ADJUSTMENT read negative). |
| `kind` | yes | `CHARGE` \| `USAGE_CHARGE` \| `FEE` \| `REGULATORY_FEE` \| `TAX` \| `INTEREST` \| `ADJUSTMENT` \| `CREDIT` \| `PAYMENT` \| `LATE_FEE` \| `INSTALLMENT` \| `DEPOSIT` | — | #4040 PR4 — closed line-item kind vocabulary; the sole source of sign. CREDIT/PAYMENT/ADJUSTMENT make the signed amount negative. |
| `currency` | yes | string | — | #4040 PR4 — ISO-4217, inherited from the statement. |
| `category` | no | `USAGE` \| `SERVICE` \| `EQUIPMENT` \| `PLAN` \| `PAYMENT` \| `CREDIT` \| `REFUND` \| `PRINCIPAL` \| `INTEREST` \| `ESCROW` \| `FEE` \| `TAX` \| `REGULATORY` \| `LATE_FEE` \| `GROCERIES` \| `DINING` \| `FUEL` \| `TRAVEL` \| `SHOPPING` \| `SUBSCRIPTION` \| `INSURANCE` \| `OTHER` | — | #4040 PR4 — closed line-item category vocabulary (nullable). |
| `reference_id` | no | string | — | — |
| `quantity` | no | number | — | — |
| `usage` | no | LineItemUsageDto | — | #4040 PR4 — metered usage (utilities/telecom). |
| `confidence` | yes | number | — | Per-line confidence in [0, 1]. |

### LineItemUsageDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `quantity` | yes | number | — | Quantity consumed (e.g. kWh, GB, minutes). |
| `unit` | yes | string | — | — |
| `rate` | no | number | — | Per-unit rate. |
| `tier` | no | string | — | — |
| `service_period` | no | ServicePeriodDto | — | — |

### PublicBillResponseDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `total_amount` | no | Iso20022AmountResponseDto | — | — |
| `due_date_iso` | no | string \| null | — | — |
| `amount` | yes | number | **deprecated** | Legacy major-unit amount. Use total_amount.value. |
| `currency` | yes | string | **deprecated** | Legacy currency. Use total_amount.currency. |
| `due_date` | no | string \| null | **deprecated** | Legacy due date. Use due_date_iso. |
| `id` | yes | string | — | — |

### RefreshStatementBodyDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `source_hint` | no | `CREDENTIALED` \| `EMAIL` \| `BANK_FEED` \| `USER_UPLOAD` | — | Optional hint about which pipeline to prefer for the refresh. Best-effort. |

### RefreshStatementResponseDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `request_id` | yes | string | — | — |
| `status` | yes | `pending` \| `complete` \| `failed` | — | — |
| `started_at` | yes | string | — | — |

### ServicePeriodDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `start` | yes | string | — | — |
| `end` | yes | string | — | — |

### StatementCompletenessDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `fields_present` | yes | array<string> | — | Top-level field paths populated in this statement. |
| `fields_missing` | yes | array<string> | — | Top-level field paths the agent could not populate. |
| `reasons` | yes | map<string, string> | — | Per-missing-field reason codes (e.g. "biller_does_not_disclose"). |
| `refreshable` | yes | boolean | — | Whether a future POST /refresh might fill the gaps. False if the biller permanently does not disclose this data. |
| `data_quality` | yes | `VERIFIED` \| `INFERRED` \| `PARTIAL` | — | VERIFIED = all required fields exact-match. INFERRED = some derived from indirect signals. PARTIAL = at least one required field missing. |
| `extraction_method` | yes | `CREDENTIALED` \| `EMAIL` \| `BANK_FEED` \| `USER_UPLOAD` | — | How BillerAPI obtained the underlying document. User-comprehensible category — does NOT leak internal pipeline mechanics (scrape vs IMAP vs OFX). |
| `needs_review` | no | boolean | — | true when this statement was persisted below the extraction confidence threshold (or otherwise flagged) and should be re-checked. Always paired with data_quality=PARTIAL. Recipients may call POST /refresh to attempt a higher-confidence re-extraction. |
| `needs_review_reason` | no | string | — | Short machine-readable reason for needs_review, when set. |
| `truncated` | no | boolean | — | #4040 PR4 — true when the source emitted more than 500 line items and the tail was dropped (the first 500 are kept). Computed at ingest, never the model self-report. |

### StatementDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `bill_id` | yes | string | — | — |
| `client_id` | yes | string | — | — |
| `version` | yes | number | — | Optimistic concurrency version. Increments on every refresh. |
| `extracted_at` | yes | string | — | When BillerAPI extracted this snapshot from the source document. Use this, not fetched_at — fetched_at was renamed pre-launch. |
| `stale` | yes | boolean | — | true when the statement is older than its TTL or upstream marked it stale; recipients should call POST /refresh. |
| `statement_period` | yes | StatementPeriodDto | — | — |
| `balance_breakdown` | yes | BalanceBreakdownDto | — | — |
| `biller_metadata` | yes | BillerMetadataDto | — | — |
| `line_items` | yes | array<LineItemDto> | — | Capped at 500 per statement for v1. |
| `completeness` | yes | StatementCompletenessDto | — | — |
| `overall_confidence` | yes | number | — | Aggregate confidence in [0, 1]. |

### StatementPeriodDto

| Field | Always present | Type | Notes | Description |
| --- | --- | --- | --- | --- |
| `start` | yes | string | — | First day of the statement window (YYYY-MM-DD). |
| `end` | yes | string | — | Last day of the statement window (YYYY-MM-DD). |
| `statement_date` | yes | string | — | When the statement was issued (YYYY-MM-DD). Always >= end. |


## See also

- [API reference index](/docs/api.md)
- [Authentication](/docs/guides/authentication.md)
- [Pagination](/docs/concepts/pagination.md)
- [Error handling](/docs/concepts/errors.md)
