# Client invoices

**Client invoices** are the agency-billing surface: draft, issue, pay, void, and render an invoice for a client account.

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/invoices` | [List invoices](#list-invoices) |
| `POST` | `/v1/api/client-billing/invoices` | [Assemble a draft invoice from billables](#assemble-a-draft-invoice-from-billables) |
| `GET` | `/v1/api/client-billing/invoices/{id}` | [Get one invoice with its lines](#get-one-invoice-with-its-lines) |
| `POST` | `/v1/api/client-billing/invoices/{id}/issue` | [Issue a draft, assigning its number](#issue-a-draft-assigning-its-number) |
| `GET` | `/v1/api/client-billing/invoices/{id}/logo` | [Download the logo frozen onto this invoice](#download-the-logo-frozen-onto-this-invoice) |
| `POST` | `/v1/api/client-billing/invoices/{id}/pay` | [Record an out-of-band settlement (bank transfer, cheque)](#record-an-out-of-band-settlement-bank-transfer-cheque) |
| `POST` | `/v1/api/client-billing/invoices/{id}/payment-link` | [Open a provider-hosted checkout for an issued invoice](#open-a-provider-hosted-checkout-for-an-issued-invoice) |
| `GET` | `/v1/api/client-billing/invoices/{id}/pdf` | [Download the invoice as a PDF](#download-the-invoice-as-a-pdf) |
| `POST` | `/v1/api/client-billing/invoices/{id}/void` | [Withdraw an invoice, or discard a draft](#withdraw-an-invoice-or-discard-a-draft) |

## List invoices

`GET /v1/api/client-billing/invoices` · operation ID `listClientInvoices`

**Parameters**

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `clientAccountId` | query | no | string | Only this customer's invoices. |
| `status` | query | no | string | One of `draft`, `final`, `void`, `paid`. |
| `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` | ClientInvoice[] | Invoice headers, newest first. `lines` is empty on this projection — read one invoice to get them. |
| `400` | ApiError | The request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. |

## Assemble a draft invoice from billables

`POST /v1/api/client-billing/invoices` · operation ID `createClientInvoiceDraft`

**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`, `CreateInvoiceDraft` (required)

| Property | Type | Required | Notes |
| --- | --- | --- | --- |
| `billableIds` | string[] | **yes** |  |
| `buyerAddress` | string | **yes** | min length 1, max length 500 |
| `buyerBillingEmail` | string (email) | null | no |  |
| `buyerLegalName` | string | no | min length 1, max length 200 |
| `dueAt` | string (date-time) | **yes** |  |
| `paymentTerms` | string | no | min length 1, max length 200 |
| `replacesInvoiceId` | string | null | no |  |
| `sellerAddress` | string | no | min length 1, max length 500 |
| `sellerLegalName` | string | no | min length 1, max length 200 |
| `taxRatesBps` | object | no |  |

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `201` | ClientInvoice | The draft, with its lines snapshotted from the ledger and its totals computed once and stored. |
| `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. Invoices are agency-only, reads included. |
| `404` | ApiError | A named billable or the client account is not this workspace's. |
| `409` | ApiError | The set cannot be invoiced: it spans two customers or two currencies, a charge nets to zero, a tax class has no rate, a correction's charge was never invoiced, or the total would exceed what an invoice can represent. The message names which. |
| `422` | ApiError | A database constraint refused the draft. |

## Get one invoice with its lines

`GET /v1/api/client-billing/invoices/{id}` · operation ID `getClientInvoice`

**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` | ClientInvoice | The invoice and every line it was built from. |
| `400` | ApiError | The request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. |
| `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. |

## Issue a draft, assigning its number

`POST /v1/api/client-billing/invoices/{id}/issue` · operation ID `issueClientInvoice`

**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`, `IssueInvoice` (required)

The contract does not pin this body's properties.

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `200` | ClientInvoice | The issued invoice. `invoiceNumber` and `invoiceSequence` are assigned by the database at this moment and are gapless per workspace. |
| `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. Invoices are agency-only, reads included. |
| `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. |
| `409` | ApiError | Not issuable: already issued, no lines, the header does not reconcile with its lines, a charge on it is already invoiced, or a correction of one is missing from it. |
| `422` | ApiError | A database constraint refused the issue. |

## Download the logo frozen onto this invoice

`GET /v1/api/client-billing/invoices/{id}/logo` · operation ID `getClientInvoiceLogo`

**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` | string (binary) | The agency's logo AS IT WAS AT ISSUE — not the workspace's current one. The `Content-Type` is the format recorded on the invoice, accompanied by `X-Content-Type-Options: nosniff`. |
| `400` | ApiError | The request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. |
| `404` | ApiError | No such invoice, or it carries no logo. |

## Record an out-of-band settlement (bank transfer, cheque)

`POST /v1/api/client-billing/invoices/{id}/pay` · operation ID `payClientInvoice`

**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`, `PayInvoice` (required)

The contract does not pin this body's properties.

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `200` | ClientInvoice | The settled invoice. This is an OUT-OF-BAND settlement — an agency member asserting money arrived somewhere hlix cannot see — recorded under its own audit action so it is never confused with provider-verified payment. Any outstanding hosted checkout is cancelled, so the invoice cannot also be paid through the provider. |
| `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. Invoices are agency-only, reads included. |
| `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. |
| `409` | ApiError | Only an issued, unpaid invoice can be settled. |
| `422` | ApiError | A database constraint refused the payment. |

## Open a provider-hosted checkout for an issued invoice

`POST /v1/api/client-billing/invoices/{id}/payment-link` · operation ID `createClientInvoicePaymentLink`

**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`, `PaymentLink` (required)

| Property | Type | Required | Notes |
| --- | --- | --- | --- |
| `successUrl` | string (uri) | no | max length 2000 |

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `200` | ClientPaymentLink | A PROVIDER-HOSTED checkout URL and the provider's reference for it — and nothing else. No card field, no payment token and no credential of any kind crosses this boundary: the payer enters those on the provider's own page. Calling this does NOT mark the invoice paid. A PROVIDER payment settles the invoice only through the provider's signed webhook, reconciled on amount, currency and invoice id; the one other route to `paid` is `POST /:id/pay`, an audited out-of-band settlement recorded by an agency member under its own audit action. |
| `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 attempting something other than reading or paying its own invoice. |
| `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. |
| `409` | ApiError | Not payable: the invoice is a draft, void or already paid, it already has a checkout open with a different provider, or its total is above what the provider will take in one payment. |
| `501` | ApiError | No payment provider is connected for this deployment. The invoice can still be issued, sent and paid outside hlix. |

## Download the invoice as a PDF

`GET /v1/api/client-billing/invoices/{id}/pdf` · operation ID `getClientInvoicePdf`

**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` | string (binary) | The rendered invoice, every line included. |
| `400` | ApiError | The request named no active organization. |
| `401` | ApiError | No valid session or API key. |
| `403` | ApiError | The caller is a `client` collaborator. Invoices are agency-only, reads included. |
| `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. |
| `409` | ApiError | The invoice is a draft, or its stored totals no longer reconcile with its stored lines. Rendering a document that does not add up is refused rather than attempted. |
| `415` | ApiError | The invoice is valid but this renderer cannot encode it — a party name or line description outside Latin-1. Retrying will not help; read the invoice through `getClientInvoice` instead. |

## Withdraw an invoice, or discard a draft

`POST /v1/api/client-billing/invoices/{id}/void` · operation ID `voidClientInvoice`

**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`, `VoidInvoice` (required)

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

**Responses**

| Status | Body | Description |
| --- | --- | --- |
| `200` | ClientInvoice | The voided invoice. An issued one KEEPS its number — a hole in the sequence reads as a deleted document. Corrections are a replacement invoice naming this one, never an edit. |
| `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. Invoices are agency-only, reads included. |
| `404` | ApiError | No such invoice in this workspace. Never a 403 — 'forbidden' would confirm that an invoice exists for a customer the caller was never shown. |
| `409` | ApiError | The invoice is already void. |
| `422` | ApiError | A database constraint refused the void. |

## 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/)