# Client billing

Generated from contract version `1.0.0` — the same document [`/v1/api/openapi.json`](/api/openapi/) serves.

## Prerequisites

- An API key in `x-api-key`, or a browser session cookie
- `X-Organization-Id` naming the workspace to act in, unless the session already has an active one
- The base URL for your deployment, `https://server.hlix.ai` by default

## Operations

| Method | Path | Operation |
| --- | --- | --- |
| `GET` | `/v1/api/client-billing/billables` | [List the ledger](#list-the-ledger) |
| `POST` | `/v1/api/client-billing/billables` | [Record a billable](#record-a-billable) |
| `GET` | `/v1/api/client-billing/billables/{id}` | [Get one ledger row](#get-one-ledger-row) |
| `POST` | `/v1/api/client-billing/billables/{id}/adjust` | [Append a correction to a charge](#append-a-correction-to-a-charge) |
| `POST` | `/v1/api/client-billing/billables/{id}/void` | [Reverse a charge](#reverse-a-charge) |

## List the ledger

`GET /v1/api/client-billing/billables` · operation ID `listClientBillables`

**Parameters**

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `clientAccountId` | query | no | string | Only this customer's rows. |
| `projectId` | query | no | string | Only this project's rows. A client collaborator asking about a project it cannot reach is REFUSED rather than answered with an empty list — 'not yours' must not read as 'nothing billed yet'. |
| `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. |

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `200` | ClientBillableView[] | The matching rows, each with the derived `netAmountMicros` and any void that reverses it. |
| `400` | ApiError | The request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. |
| `404` | ApiError | A `projectId` was named that this caller cannot reach. THE ONE PLACE THIS FAMILY REFUSES A READ OUTRIGHT: everywhere else an unreachable row is simply absent, but on a filtered list an empty 200 would make "not yours" indistinguishable from "nothing billed yet". |

## Record a billable

`POST /v1/api/client-billing/billables` · operation ID `createClientBillable`

**Parameters**

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. |

**Request body** — `application/json`, `CreateBillable` (required)

The contract does not pin this body's properties.

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `201` | ClientBillable | The written row. Its price is SNAPSHOTTED here and never recomputed; for `agent_usage` the tokens and model come from the run, not from the request, so a caller cannot name its own price. |
| `400` | ValidationError \| ApiError | The JSON body failed schema validation (`ValidationError`), or the request named no active organization (`ApiError`). |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | The caller is a client collaborator; the billable ledger is managed by the agency. |
| `404` | ApiError | No such client account or project in this workspace. Never a 403 — that would confirm it exists somewhere. |
| `409` | ApiError | This source was already billed. A retried request is the usual cause. |
| `422` | ApiError | A database constraint refused the row, and the answer names it. Reserved for a rule added after this contract — no request reaching the route trips one today. |

## Get one ledger row

`GET /v1/api/client-billing/billables/{id}` · operation ID `getClientBillable`

**Parameters**

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | **yes** | string |  |
| `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. |

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `200` | ClientBillableView | The row, with its derived net and void. |
| `400` | ApiError | The request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | Authenticated, but not a member of the named organization — or a client collaborator without the required access on this project. |
| `404` | ApiError | No such billable this caller may read. |

## Append a correction to a charge

`POST /v1/api/client-billing/billables/{id}/adjust` · operation ID `adjustClientBillable`

**Parameters**

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | **yes** | string |  |
| `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. |

**Request body** — `application/json`, `AdjustBillable` (required)

| Property | Type | Required | Notes |
| --- | --- | --- | --- |
| `description` | string | **yes** | min length 1, max length 500 |
| `note` | string | null | no |  |
| `quantity` | number | **yes** |  |
| `unit` | string | **yes** | min length 1, max length 50 |
| `unitPriceMicros` | integer | **yes** | min 0, max 1000000000000 |

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `201` | ClientBillable | The correction — a NEW row naming the charge. The charge itself is untouched. |
| `400` | ValidationError \| ApiError | The JSON body failed validation, or the request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | The caller is a client collaborator; the billable ledger is managed by the agency. |
| `404` | ApiError | No such billable this caller may reach. |
| `409` | ApiError | A correction cannot correct another correction, and a voided charge cannot be corrected. The answer names the rule that refused. |
| `422` | ApiError | A database constraint refused the row, and the answer names it. Reserved for a rule added after this contract — no request reaching the route trips one today. |

## Reverse a charge

`POST /v1/api/client-billing/billables/{id}/void` · operation ID `voidClientBillable`

**Parameters**

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | **yes** | string |  |
| `X-Organization-Id` | header | no | string | The workspace (organization) to act in. Omitted, the session's active organization is used. |

**Request body** — `application/json`, `VoidBillable` (required)

| Property | Type | Required | Notes |
| --- | --- | --- | --- |
| `reason` | string | **yes** | min length 1, max length 500 |

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `201` | ClientBillable | The reversal — a NEW row. Its amount is the charge PLUS every correction already naming it, so a void can never leave a partial credit behind. |
| `400` | ValidationError \| ApiError | The JSON body failed validation, or the request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | The caller is a client collaborator; the billable ledger is managed by the agency. |
| `404` | ApiError | No such billable this caller may reach. |
| `409` | ApiError | It was already voided, or the rule that refuses a void names itself in the answer. A charge can be voided exactly once. |
| `422` | ApiError | A database constraint refused the row, and the answer names it. Reserved for a rule added after this contract — no request reaching the route trips one today. |

## If a request fails

These apply to every operation on this page.

- `400` — the body failed schema validation, or the request named no active organization. A `ValidationError` body carries the failing fields; an `ApiError` body carries only `error`.
- `401` — no valid session or API key.
- `403` — authenticated, but not a member of the named organization, or a client collaborator without the required access.
- `404` — no such resource **in this workspace**. Absent and not-visible are deliberately the same answer; do not infer cross-tenant existence from it.
- `409` — the expected revision or state is stale, or another operation currently owns the transition.
- `500` — the operation failed server-side. Retry only where an idempotency key makes that safe.

## Next steps

[API reference](/api/reference/)
  [API & OpenAPI](/api/openapi/)
  [TypeScript SDK](/sdk/typescript/)
  [CLI reference](/reference/cli/)