# Billing API reference

> The billing operations of the Fax for Agents API. Read the balance and the ledger, fund the wallet, set auto top-up and budgets, and export usage as CSV.

Page: https://faxforagents.com/docs/api/reference/billing/
Last changed: 2026-11-13

Updated Nov 13, 2026

22 operations of the Fax for Agents API, from the OpenAPI document version 1.20.0, snapshot of 2026-11-13. The base URL is `https://api.faxforagents.com/v1`. This is one group of the [full reference](https://faxforagents.com/docs/api/reference/).

- A top-up session funds the live wallet with whole US dollars, 10 to 1000 per session.
- Auto top-up takes a threshold, an amount and a daily cap, and the card is saved at the first payment.
- The CSV of a usage export is read from a signed URL that takes no API key.

## Operations

- `GET /v1/auto-topups` Auto top-up settings
- `POST /v1/auto-topups` Opt in to auto top-up (threshold, amount, daily cap); the card is saved at the first payment
- `DELETE /v1/auto-topups/{auto_topup_id}` Turn auto top-up off
- `GET /v1/balance` Available and held balance
- `POST /v1/billing-portal-sessions` Open the Stripe customer portal (cards, invoices, the subscription)
- `GET /v1/billing-settings` Billing settings of the organization
- `PATCH /v1/billing-settings` Change the billing email or the low-balance threshold
- `GET /v1/budgets` Spending caps of this environment
- `POST /v1/budgets` Create a daily or monthly cap for the organization, a project or an agent
- `PATCH /v1/budgets/{budget_id}` Change a cap's limit or status
- `GET /v1/invoices` Stripe invoices and receipts (hosted links)
- `GET /v1/invoices/{invoice_id}` One Stripe invoice
- `GET /v1/ledger` Wallet transactions, newest first
- `GET /v1/subscriptions` Subscriptions and the workspace entitlement
- `POST /v1/subscriptions/checkout` Open a hosted Checkout for the Agent workspace subscription
- `GET /v1/topup-sessions` Top-up sessions, newest first
- `POST /v1/topup-sessions` Open a hosted Checkout to fund the live wallet (10 to 1000 USD)
- `GET /v1/topup-sessions/{topup_session_id}` Read a top-up session
- `GET /v1/usage` Usage rollups by day, direction, destination and project
- `POST /v1/usage-exports` Export usage as CSV behind a signed download
- `GET /v1/usage-exports/{export_id}` Read a usage export (fresh link)
- `GET /v1/usage-exports/{export_id}/content` The CSV behind a signed URL (no API key)

## How is money represented in the billing group?

As integers. Every amount is in micro-units of a US dollar, in fields that end in `_micro`. One dollar is 1000000. No field is a decimal number, so a client never rounds. The one exception in a request is `amount_usd` on a top-up session, which takes whole dollars.

The wallet is prepaid. `GET /v1/balance` returns two figures for the environment of the credential. `available_micro` is what a new send can use. `held_micro` is what queued and active faxes have reserved and not yet captured or released.

## How do I fund the wallet?

`POST /v1/topup-sessions` opens a hosted Checkout page and returns its `url`. The amount is whole US dollars, 10 to 1000 per session, and it funds the live wallet. A person pays on that page. `GET /v1/topup-sessions/{topup_session_id}` reads the session with its status, and the list returns sessions newest first.

Auto top-up repeats that without a person. `POST /v1/auto-topups` takes three amounts. `threshold_micro` is the available balance below which a top-up starts. `amount_micro` is how much each one adds. `daily_cap_micro` is the most that can be added in one UTC day. The card is saved at the first payment, and the answer carries a `setup_url` for it. `DELETE /v1/auto-topups/{auto_topup_id}` turns it off.

## What is a budget?

A budget is a spending cap. `POST /v1/budgets` creates a daily or monthly cap for the organization, a project or an agent. The answer shows `limit_micro`, `spent_micro`, `remaining_micro` and `resets_at`. `PATCH /v1/budgets/{budget_id}` changes the limit or the status. `GET /v1/budgets` lists the caps of the environment.

A budget is not a [delegated policy](https://faxforagents.com/docs/policies/). A policy decides whether a send needs a person. A budget stops spending once the cap is reached, whoever allowed the send.

## Where are invoices, the ledger and the subscription?

`GET /v1/ledger` returns the wallet's transactions, newest first, a page at a time. `GET /v1/invoices` returns the payment provider's invoices and receipts with hosted links, and one invoice can be read by id.

`POST /v1/billing-portal-sessions` opens the customer portal for cards, invoices and the subscription. `GET /v1/subscriptions` returns subscriptions and the workspace entitlement. `POST /v1/subscriptions/checkout` opens a hosted Checkout for the Agent workspace subscription.

`GET /v1/billing-settings` and its `PATCH` hold the billing email and the low-balance threshold.

## How do I read and export usage?

`GET /v1/usage` returns rollups between `from` and `to`. `group_by` chooses the grouping, by day, direction, destination or project, and `project_id` narrows it to one project. The answer has the items and the totals.

`POST /v1/usage-exports` writes the same data as CSV. Without dates it covers the last 30 days to today. The answer has a signed `url`. `GET /v1/usage-exports/{export_id}` returns a fresh link when the first has expired. The content route itself takes the signature in the address and no API key.

## Which scopes and errors belong to billing?

Reads need `billing:read`. Everything that moves money or changes a setting needs `billing:write`. An agent key is never issued `billing:write`, so an agent cannot fund the wallet or raise its own cap.

Three statuses appear here more than elsewhere. 402 answers a call the wallet or the payment cannot cover. 409 answers a change that the state forbids. 503 answers while the payment provider is not available.

## Each operation in detail

### GET `/v1/auto-topups`

Auto top-up settings

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Responses

GET /v1/auto-topups responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/auto-topups response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object AutoTopup | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/auto-topups \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/auto-topups`

Opt in to auto top-up (threshold, amount, daily cap); the card is saved at the first payment

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

POST /v1/auto-topups parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/auto-topups request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_micro` | integer | yes | Whole dollars, 10 to 1000 USD. |
| `daily_cap_micro` | integer | yes | Never more than this per UTC day. |
| `threshold_micro` | integer | yes | Top up when the available balance drops below this. |

#### Responses

POST /v1/auto-topups responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `402` | The prepaid balance does not cover the call: insufficient_balance. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
| `503` | Something this call depends on is not available or not set up here, for example billing_unavailable or provider_unavailable. error.retryable says whether to send the same call again. |

#### Response fields

POST /v1/auto-topups response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_micro` | integer | yes |  |
| `created_at` | string | yes |  |
| `daily_cap_micro` | integer | yes |  |
| `disabled_at` | string or null | yes |  |
| `environment` | string (live, test) | yes |  |
| `id` | string | yes |  |
| `last_attempt_at` | string or null | yes |  |
| `payment_method_saved` | boolean | yes |  |
| `setup_session_id` | string or null | yes |  |
| `setup_url` | string or null | no | Where to save the card while status is pending_setup. |
| `status` | string (pending_setup, active, disabled) | yes |  |
| `threshold_micro` | integer | yes |  |
| `updated_at` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/auto-topups \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"threshold_micro":0,"amount_micro":10000000,"daily_cap_micro":10000000}'`
```

### DELETE `/v1/auto-topups/{auto_topup_id}`

Turn auto top-up off

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

DELETE /v1/auto-topups/{auto_topup_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `auto_topup_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

DELETE /v1/auto-topups/{auto_topup_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

DELETE /v1/auto-topups/{auto_topup_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `deleted` | boolean | no |  |
| `id` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/auto-topups/AUTO_TOPUP_ID \ -X DELETE \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/balance`

Available and held balance

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Responses

GET /v1/balance responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/balance response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `available_micro` | integer | yes |  |
| `currency` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `held_micro` | integer | yes |  |
| `top_up_url` | string or null | no | The dashboard page where a person adds funds to the wallet. A key cannot top up. |

#### Example

```
`curl https://api.faxforagents.com/v1/balance \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/billing-portal-sessions`

Open the Stripe customer portal (cards, invoices, the subscription)

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

POST /v1/billing-portal-sessions parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

POST /v1/billing-portal-sessions responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `402` | The prepaid balance does not cover the call: insufficient_balance. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
| `503` | Something this call depends on is not available or not set up here, for example billing_unavailable or provider_unavailable. error.retryable says whether to send the same call again. |

#### Response fields

POST /v1/billing-portal-sessions response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `expires_at` | string | yes |  |
| `url` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/billing-portal-sessions \ -X POST \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/billing-settings`

Billing settings of the organization

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Responses

GET /v1/billing-settings responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/billing-settings response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `billing_email` | string or null | yes |  |
| `disputed` | boolean | yes |  |
| `low_balance_active` | boolean | yes |  |
| `low_balance_threshold_micro` | integer | yes |  |
| `org_id` | string | yes |  |
| `stripe_customer` | boolean | yes |  |
| `workspace` | boolean | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/billing-settings \ -H "Authorization: Bearer ffa_test_..."`
```

### PATCH `/v1/billing-settings`

Change the billing email or the low-balance threshold

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

PATCH /v1/billing-settings parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

PATCH /v1/billing-settings request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `billing_email` | string (email) or null | no |  |
| `low_balance_threshold_micro` | integer or null | no |  |

#### Responses

PATCH /v1/billing-settings responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

PATCH /v1/billing-settings response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `billing_email` | string or null | yes |  |
| `disputed` | boolean | yes |  |
| `low_balance_active` | boolean | yes |  |
| `low_balance_threshold_micro` | integer | yes |  |
| `org_id` | string | yes |  |
| `stripe_customer` | boolean | yes |  |
| `workspace` | boolean | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/billing-settings \ -X PATCH \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```

### GET `/v1/budgets`

Spending caps of this environment

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Responses

GET /v1/budgets responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/budgets response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object Budget | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/budgets \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/budgets`

Create a daily or monthly cap for the organization, a project or an agent

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

POST /v1/budgets parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/budgets request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agent_id` | string or null | no |  |
| `limit_micro` | integer | yes |  |
| `period` | string (daily, monthly) | yes |  |
| `project_id` | string or null | no |  |
| `scope` | string (organization, project, agent) | no |  |

#### Responses

POST /v1/budgets responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/budgets response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agent_id` | string or null | yes |  |
| `created_at` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `id` | string | yes |  |
| `limit_micro` | integer | yes |  |
| `period` | string (daily, monthly) | yes |  |
| `project_id` | string or null | yes |  |
| `remaining_micro` | integer | yes |  |
| `resets_at` | string | yes |  |
| `scope` | string (organization, project, agent) | yes |  |
| `spent_micro` | integer | yes |  |
| `status` | string (active, disabled) | yes |  |
| `updated_at` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/budgets \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"period":"daily","limit_micro":0}'`
```

### PATCH `/v1/budgets/{budget_id}`

Change a cap's limit or status

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

PATCH /v1/budgets/{budget_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `budget_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

PATCH /v1/budgets/{budget_id} request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit_micro` | integer or null | no |  |
| `status` | string (active, disabled) or null | no |  |

#### Responses

PATCH /v1/budgets/{budget_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

PATCH /v1/budgets/{budget_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agent_id` | string or null | yes |  |
| `created_at` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `id` | string | yes |  |
| `limit_micro` | integer | yes |  |
| `period` | string (daily, monthly) | yes |  |
| `project_id` | string or null | yes |  |
| `remaining_micro` | integer | yes |  |
| `resets_at` | string | yes |  |
| `scope` | string (organization, project, agent) | yes |  |
| `spent_micro` | integer | yes |  |
| `status` | string (active, disabled) | yes |  |
| `updated_at` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/budgets/BUDGET_ID \ -X PATCH \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```

### GET `/v1/invoices`

Stripe invoices and receipts (hosted links)

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

GET /v1/invoices parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `cursor` | query | string or null | no |  |
| `limit` | query | integer | no | Default `20` . |

#### Responses

GET /v1/invoices responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
| `503` | Something this call depends on is not available or not set up here, for example billing_unavailable or provider_unavailable. error.retryable says whether to send the same call again. |

#### Response fields

GET /v1/invoices response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object Invoice | yes |  |
| `next_cursor` | string or null | no |  |

#### Example

```
`curl https://api.faxforagents.com/v1/invoices \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/invoices/{invoice_id}`

One Stripe invoice

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

GET /v1/invoices/{invoice_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `invoice_id` | path | string | yes |  |

#### Responses

GET /v1/invoices/{invoice_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
| `503` | Something this call depends on is not available or not set up here, for example billing_unavailable or provider_unavailable. error.retryable says whether to send the same call again. |

#### Response fields

GET /v1/invoices/{invoice_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_due_micro` | integer | yes |  |
| `amount_paid_micro` | integer | yes |  |
| `created_at` | string or null | yes |  |
| `currency` | string | yes |  |
| `hosted_invoice_url` | string or null | yes |  |
| `id` | string | yes |  |
| `invoice_pdf` | string or null | yes |  |
| `number` | string or null | yes |  |
| `status` | string or null | yes |  |
| `subscription_id` | string or null | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/invoices/INVOICE_ID \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/ledger`

Wallet transactions, newest first

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

GET /v1/ledger parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `cursor` | query | string or null | no |  |
| `limit` | query | integer | no | Default `50` . |

#### Responses

GET /v1/ledger responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/ledger response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object LedgerEntry | yes |  |
| `next_cursor` | string or null | no |  |

#### Example

```
`curl https://api.faxforagents.com/v1/ledger \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/subscriptions`

Subscriptions and the workspace entitlement

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Responses

GET /v1/subscriptions responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/subscriptions response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object Subscription | yes |  |
| `next_cursor` | string or null | no |  |
| `workspace` | boolean | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/subscriptions \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/subscriptions/checkout`

Open a hosted Checkout for the Agent workspace subscription

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

POST /v1/subscriptions/checkout parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

POST /v1/subscriptions/checkout responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `402` | The prepaid balance does not cover the call: insufficient_balance. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
| `503` | Something this call depends on is not available or not set up here, for example billing_unavailable or provider_unavailable. error.retryable says whether to send the same call again. |

#### Response fields

POST /v1/subscriptions/checkout response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `product` | string | yes |  |
| `stripe_session_id` | string | yes |  |
| `url` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/subscriptions/checkout \ -X POST \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/topup-sessions`

Top-up sessions, newest first

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

GET /v1/topup-sessions parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `cursor` | query | string or null | no |  |
| `limit` | query | integer | no | Default `50` . |

#### Responses

GET /v1/topup-sessions responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/topup-sessions response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object TopupSession | yes |  |
| `next_cursor` | string or null | no |  |

#### Example

```
`curl https://api.faxforagents.com/v1/topup-sessions \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/topup-sessions`

Open a hosted Checkout to fund the live wallet (10 to 1000 USD)

Credential: an API key or a signed-in session. Scope: `billing:write`.

#### Parameters

POST /v1/topup-sessions parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/topup-sessions request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_usd` | integer | yes | Whole US dollars, 10 to 1000 per session. |

#### Responses

POST /v1/topup-sessions responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `402` | The prepaid balance does not cover the call: insufficient_balance. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |
| `503` | Something this call depends on is not available or not set up here, for example billing_unavailable or provider_unavailable. error.retryable says whether to send the same call again. |

#### Response fields

POST /v1/topup-sessions response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_micro` | integer | yes |  |
| `auto_topup_id` | string or null | yes |  |
| `completed_at` | string or null | yes |  |
| `created_at` | string | yes |  |
| `currency` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `error` | string or null | yes |  |
| `expires_at` | string or null | yes |  |
| `id` | string | yes |  |
| `kind` | string (manual, auto, setup) | yes |  |
| `status` | string (open, completed, expired, canceled, failed) | yes |  |
| `stripe_payment_intent_id` | string or null | yes |  |
| `stripe_session_id` | string or null | yes |  |
| `url` | string or null | no | The hosted Checkout page while the session is open. |

#### Example

```
`curl https://api.faxforagents.com/v1/topup-sessions \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"amount_usd":10}'`
```

### GET `/v1/topup-sessions/{topup_session_id}`

Read a top-up session

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

GET /v1/topup-sessions/{topup_session_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `topup_session_id` | path | string | yes |  |

#### Responses

GET /v1/topup-sessions/{topup_session_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/topup-sessions/{topup_session_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount_micro` | integer | yes |  |
| `auto_topup_id` | string or null | yes |  |
| `completed_at` | string or null | yes |  |
| `created_at` | string | yes |  |
| `currency` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `error` | string or null | yes |  |
| `expires_at` | string or null | yes |  |
| `id` | string | yes |  |
| `kind` | string (manual, auto, setup) | yes |  |
| `status` | string (open, completed, expired, canceled, failed) | yes |  |
| `stripe_payment_intent_id` | string or null | yes |  |
| `stripe_session_id` | string or null | yes |  |
| `url` | string or null | no | The hosted Checkout page while the session is open. |

#### Example

```
`curl https://api.faxforagents.com/v1/topup-sessions/TOPUP_SESSION_ID \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/usage`

Usage rollups by day, direction, destination and project

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

GET /v1/usage parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `from` | query | string or null | no |  |
| `to` | query | string or null | no |  |
| `group_by` | query | string | no | Comma-separated subset. Default `"day,direction,route,project"` . |
| `project_id` | query | string or null | no |  |

#### Responses

GET /v1/usage responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/usage response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `currency` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `from` | string | yes |  |
| `group_by` | array of string | yes |  |
| `items` | array of object UsageItem | yes |  |
| `to` | string | yes |  |
| `totals` | object | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/usage \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/usage-exports`

Export usage as CSV behind a signed download

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

POST /v1/usage-exports parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/usage-exports request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string or null | no | ISO date; default 30 days ago. |
| `to` | string or null | no | ISO date; default today. |

#### Responses

POST /v1/usage-exports responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/usage-exports response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `content_type` | string | yes |  |
| `created_at` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `expires_at` | string | yes |  |
| `id` | string | yes |  |
| `params` | object | yes |  |
| `row_count` | integer | yes |  |
| `status` | string (ready, expired) | yes |  |
| `url` | string or null | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/usage-exports \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```

### GET `/v1/usage-exports/{export_id}`

Read a usage export (fresh link)

Credential: an API key or a signed-in session. Scope: `billing:read`.

#### Parameters

GET /v1/usage-exports/{export_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `export_id` | path | string | yes |  |

#### Responses

GET /v1/usage-exports/{export_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/usage-exports/{export_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `content_type` | string | yes |  |
| `created_at` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `expires_at` | string | yes |  |
| `id` | string | yes |  |
| `params` | object | yes |  |
| `row_count` | integer | yes |  |
| `status` | string (ready, expired) | yes |  |
| `url` | string or null | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/usage-exports/EXPORT_ID \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/usage-exports/{export_id}/content`

The CSV behind a signed URL (no API key)

Credential: the signed URL as it was given.

#### Parameters

GET /v1/usage-exports/{export_id}/content parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `export_id` | path | string | yes |  |
| `exp` | query | string or null | no |  |
| `sig` | query | string or null | no |  |

#### Responses

GET /v1/usage-exports/{export_id}/content responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |

#### Example

```
`curl https://api.faxforagents.com/v1/usage-exports/EXPORT_ID/content`
```
