> This page is for version v2 (default).
> For other versions, use one of these documentation indexes:
> - v2 (default): https://docs.paid.ai/v-2/llms.txt
> - v1: https://docs.paid.ai/v-1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.paid.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paid.ai/_mcp/server.

# Customer units and credit caps

> Model a customer's own hierarchy of teams, tenants or departments, attribute credit spend to it, and cap spend per unit and period.

## Overview

A **customer unit** is a node in a customer's own hierarchy: a department, a tenant, a team, a project. Units let you see where a customer's credits are being spent and cap the spend of any part of that hierarchy for a period.

Units are structure only. They hold no balance and no orders; the customer does. A signal names the unit it should count toward, and a **credit cap** on a unit limits how many credits of one currency that unit and everything under it may spend per period.

Customer units are a different feature from [multi-entity customers](/documentation/customers-users/multi-entity-customers). That page is about billing several customer accounts through a parent account. This page is about the hierarchy *inside* one customer.

> **Note**
>
> Customer units are enabled per organization. Until they are enabled for yours,
> the unit and cap routes answer `403` with the code `FEATURE_NOT_ENABLED`.

## Addressing

Every route lives under the customer, addressed as on the other customer routes: by Paid's customer id, or by your own id under `/external/`.

```
/api/v2/customers/{id}/customer-units
/api/v2/customers/external/{externalId}/customer-units
```

Within a customer, a unit is identified by its **`externalId`**: your own key for it. It is required when you create the unit, unique within the customer, and immutable. Every unit route, every parent reference and every signal names a unit by it. Paid also gives each unit an id of its own (`cu_…`), which the customer-scoped unit routes return as `id`; no unit route accepts it. Signals and the organisation-level unit routes name a unit by its key only and never return that id. The same holds for the `_billing` mark that ingest adds to a signal from a unit that is not billable: it carries the unit's key only. Billing records that usage without billing it: see [Tracking usage without billing it](#tracking-usage-without-billing-it).

A unit key is any non-empty string of at most 255 characters (Unicode code points) other than `.` and `..`. The same bound applies to the key a signal carries.

## The tree

Each customer's units form a tree: one root, and under it units that each name an existing parent.

* **The root.** Paid creates it together with the customer when that customer has an `externalId` usable as a unit key, under a copy of that id. A customer without one starts with no units, and you create the root yourself by creating a unit with no `parentExternalId`. Existing customers follow the same rule. Paid creates no other unit.
* **One root at a time.** A second parentless unit answers `409 ROOT_EXISTS`; list the customer's units and look for `isRoot` if you are unsure. The root cannot be re-parented (`ROOT_UNIT_IMMOVABLE`) but is otherwise an ordinary unit, and deleting it, once it has no cap and no active children, frees the slot for a new one.
* **Parents.** A parent must exist and be active (`PARENT_NOT_FOUND`, `PARENT_NOT_ACTIVE`). A unit cannot move under itself or its own descendant (`PARENT_CYCLE`), and the tree is at most 16 levels deep (`DEPTH_EXCEEDED`).
* **Seat-billed customers.** Customers on seat-based billing cannot have units: every write answers `409 SEAT_BILLING`. Ending a cap is the exception, so a customer that moved to seat billing can still close one.

### Creating units

```bash
curl -X POST https://api.agentpaid.io/api/v2/customers/external/acme/customer-units \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "eu-tenant",
    "name": "EU tenant",
    "externalType": "tenant",
    "parentExternalId": "acme",
    "metadata": { "region": "eu-west-1" }
  }'
```

The same call from the [Paid CLI](/documentation/cli/command-reference) (`@paid-ai/cli` 1.2.0 or later):

```bash
paid customer-unit create-external --external-id acme externalId=eu-tenant name="EU tenant" externalType=tenant parentExternalId=acme metadata.region=eu-west-1
```

`externalId` is the only required field. `name` (up to 255 characters) defaults to it and is never used to address the unit, `externalType` (up to 64 characters) is a free label for your own structural vocabulary, and `metadata` is any JSON object. Here `parentExternalId` is `acme`, the root Paid created when the customer was created with the external id `acme`.

Every unit response carries `externalId`, `customerId`, `customerExternalId`, `name`, `externalType`, `status` (`ACTIVE` or `DELETED`), `parentExternalId` (`null` on the root), `isRoot`, `billable`, `effectiveBillable`, `metadata`, `createdAt` and `updatedAt`. `billable` is the unit's own setting and `effectiveBillable` whether it is billable once the units above it are counted. Usage from a unit that is not billable in effect is tracked but not billed, with the exceptions under [Tracking usage without billing it](#tracking-usage-without-billing-it). Responses from the customer-scoped unit routes also carry `id`, Paid's id for the unit (`cu_…`). The organisation-level unit routes return no Paid unit id.

### Listing and reading

`GET …/customer-units` lists a customer's units flat, in creation order, with `parentExternalId` on each so you can assemble the tree. It is paginated with the usual `limit` and `offset` query parameters and shows active units by default; add `status=DELETED` to see deleted ones, `externalType=` to filter by type, or `parentExternalId=` to list one unit's direct children. Unknown query parameters are a `400`. `GET …/customer-units/{externalCustomerUnitId}` returns one unit, deleted or not.

### Changing units

`PATCH …/customer-units/{externalCustomerUnitId}` renames, re-types, re-parents or annotates a unit. Send only the fields you want to change, at least one; `externalId` cannot change. Set `externalType` or `metadata` to `null` to clear them.

Re-parenting moves the unit with everything under it. Spend already recorded keeps naming the unit it landed on. Caps, however, are evaluated on the tree as it is now: from the move on, the unit's spend in the running cap period counts toward its new ancestors' caps and no longer toward the old ones.

### Tracking usage without billing it

Set `billable: false` on a unit to track its usage without billing it: a QA or dev environment, say. Send it when you create the unit, or `PATCH` an existing one. Every unit under it is then non-billable too (`effectiveBillable: false`). The root is always billable (`409 ROOT_UNIT_ALWAYS_BILLABLE`): a customer that should never be billed simply has no order. Non-billable units must be enabled for your organization first: until they are, `billable: false` answers `403 FEATURE_NOT_ENABLED` and all usage is billed.

```bash
curl -X PATCH https://api.agentpaid.io/api/v2/customers/external/acme/customer-units/acme-qa \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"billable": false}'
```

Usage from a non-billable unit is still ingested, stored and listed with your signals, and the signals billing leaves unbilled carry a `_billing` mark naming the unit's key. For those, billing records the quantity but burns no credits, adds nothing to invoices or overage, and counts toward no cap.

A meter's derived signals follow the signals they come from. Each derived signal takes its unit from one source signal: the signal that closed the window, or the one that opened it when the window timed out. It carries that signal's `_billing` mark too, so it is tracked without billing when that signal was, and billed when it wasn't. A window that mixes signals from a billable unit and a non-billable one follows that one signal, as its unit does.

A price whose usage comes from a meter reads your signals when the invoice closes, and leaves the marked ones out the same way. A meter that counts or adds up signals skips each marked signal. A correlation-window meter counts whole windows, so it leaves out a window when the signal it takes its unit from was marked, by the same rule as derived signals: a window that opened on `acme-prod` and closed on `acme-qa` is left out in full, and one that opened on `acme-qa` and closed on `acme-prod` is billed in full. A credit price on a meter burns nothing for a marked signal, and the meter's preview shows the quantity without them.

Some of it is still billed:

* signals sent with a customer API key, and the derived signals and windows that take their unit from one;
* cost-plus pricing, which bills from costs that carry no unit;
* signals that don't resolve to an active unit, by its key or through an alias on it: a signal on a deleted unit, one naming a unit that doesn't exist yet, and any signal not sent to `/api/v2/signals`. The same goes for a derived signal whose unit was deleted by the time its window closed.

A change applies to signals received afterwards: switching a unit either way never changes usage already received. Nothing switches a unit for you, after an allowance say: `PATCH` it when its usage should be billed.

### Deleting units

`DELETE …/customer-units/{externalCustomerUnitId}` soft-deletes a unit. It stays readable with `status: DELETED`, keeps the spend attributed to it, still receives the spend of signals that name its key, and cannot be reactivated. A delete is refused while the unit still has active children (`HAS_ACTIVE_CHILDREN`) or a cap that is in force or scheduled (`HAS_ACTIVE_CAP`): end the cap first. A deleted unit cannot be changed or capped (`409 CUSTOMER_UNIT_DELETED`).

## Attributing spend to a unit

Add a `customerUnit` node to a signal to say which unit its spend belongs to:

```json
{
  "signals": [
    {
      "eventName": "tokens_used",
      "customer": { "externalCustomerId": "acme" },
      "customerUnit": { "externalCustomerUnitId": "eu-tenant" },
      "attribution": { "externalProductId": "assistant" },
      "data": { "tokens": 1200 }
    }
  ]
}
```

The unit does not have to exist yet. Paid records the spend under the customer and the unit's key; it counts toward the unit, and toward the caps above it, as soon as a unit with that key exists. That spend is billed even if you then create the unit with `billable: false`: create a non-billable unit before you send its usage. With draft units enabled for your organization, the first signal naming a new key creates a `DRAFT` unit under the customer's root: the key is held for that customer and the spend counts toward the root's cap from the start. A draft is listed with `status=DRAFT`, cannot be a parent or carry a cap, and becomes an ordinary unit when you create the unit with that key. Without draft units, a signal never creates a unit. A key longer than 255 characters fails validation (`400`).

Spend from a signal that names no unit stays on the customer and counts toward no cap, not even the root's.

This holds only while customer units are enabled for your organization. Before that, signals are accepted but their `customerUnit` node is ignored and no key is recorded, so spend sent before enablement is never attributed to a unit afterwards.

## Credit caps

A cap limits the credits of one currency that a unit and everything under it may spend per period. A cap on the root covers every unit of the customer, which makes it the customer-wide cap for unit-attributed spend: only spend whose signal named a unit counts toward any cap, including the root's. Spend that named no unit counts toward none, and usage on a non-billable unit spends nothing, so no cap limits a dev or QA environment. A cap is addressed by the unit and a credits currency; cap versions have no id of their own.

### Setting a cap

```bash
curl -X PUT https://api.agentpaid.io/api/v2/customers/external/acme/customer-units/eu-tenant/cap \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 10000, "frequency": "MONTHLY" }'
```

From the CLI: `paid customer-unit-cap set-external --external-id acme --external-customer-unit-id eu-tenant amount=10000 frequency=MONTHLY`.

* `amount` is in credits of the cap's currency, per period, and must be positive.
* `creditsCurrencyId` may be omitted only when your organization has exactly one credits currency, which is then used; otherwise it is required (`400 CREDITS_CURRENCY_REQUIRED`). A currency that does not exist is `404 CREDITS_CURRENCY_NOT_FOUND`.
* `frequency` is `MONTHLY`, the default. Periods use the schedule’s monthly UTC boundaries.
* `effectiveFrom` is an ISO 8601 timestamp and defaults to now. Spend earlier in the period that contains it still counts toward the cap. A future value schedules the cap; it starts applying then.

Use `schedule: { "type": "ANCHORED", "anchorDay": 15 }` for a day-of-month boundary (days 1-31, clamped in shorter months), or `schedule: { "type": "CALENDAR" }` for calendar months. Omit `schedule` to inherit the version covering `effectiveFrom`; if none applies, the creation-time UTC day is used. `effectiveFrom` only controls when the policy applies. A day-15 schedule effective September 20 counts September 15 through October 15, including earlier spend. An amount edit on September 25 keeps that same window.

Caps are versioned and never edited in place. A changed save records a new version and touches none of the existing ones; where versions overlap, the newest applies. An unchanged save returns `changed: false` with the existing `policy` and creates no version. Comparison includes future versions and finite ends: extending an ended cap or replacing a different scheduled amount is a change. A changed save returns `changed: true` and an open version. `policy` includes `amount`, `frequency`, `schedule`, `effectiveFrom`, `effectiveUntil`, `createdAt` and `source`. `currentPeriod` contains usage only when it belongs to that returned version and is available; otherwise it is `null`. Setting a cap on a deleted unit is `409 CUSTOMER_UNIT_DELETED`.

### Reading usage

`GET …/customer-units/{externalCustomerUnitId}/cap?creditsCurrencyId=…` returns the version in force as `policy` and its `currentPeriod`: the `window` (`start` inclusive, `end` exclusive, UTC), `used` (the credits of that currency spent by the unit and its descendants in the window) and `remaining` (`amount - used`, negative once the cap is exceeded). Usage is computed on request from the ledger by the billing service; the read is advisory, since spend may land between this read and the next one. No cap in force for that unit and currency is `404 CAP_NOT_FOUND`; when the billing service cannot answer, the read fails with `500 BILLING_SERVICE_UNAVAILABLE` and can be retried.

### Reading spend without a cap

`GET …/customer-units/{externalCustomerUnitId}/spend?creditsCurrencyId=…&start=…&end=…` returns what a unit and every unit beneath it spent in a window, whether or not the unit has a cap. Use it to see what a user or team is burning before you decide on a cap, or to break a capped parent's `used` down by child.

```json
{
  "externalCustomerUnitId": "peter",
  "customerId": "cus_abc123",
  "creditsCurrencyId": "7f4f5d4c-55e9-4d5b-a3e7-c9eb3d2d01bf",
  "window": {
    "start": "2026-10-05T00:00:00.000Z",
    "end": "2026-11-05T00:00:00.000Z"
  },
  "spent": 230
}
```

* `start` (inclusive) and `end` (exclusive) are ISO 8601 timestamps. Give both, at most 366 days apart, or omit both for the current calendar month in UTC. One without the other, a `start` that is not before `end`, or a longer window is `400 INVALID_SPEND_WINDOW`.
* `spent` is the same sum as a cap's `used`, net of reversals: for a capped unit over its cap's current `window`, the two are equal. Pass a parent cap's `window` to see how much of it each child spent.
* Spend from a signal that names no unit counts toward no unit. A deleted unit still answers with the spend it received.
* The currency rules and the `404` and `500` answers are the same as for the cap read.

### Ending a cap

`DELETE …/customer-units/{externalCustomerUnitId}/cap?creditsCurrencyId=…` ends the cap: every open version of that currency on the unit, including scheduled ones and older overlapping ones, stops applying now, so nothing can resurface or activate later. The response carries `endedVersions` (how many were closed, at least one) and `policy`, the version that was in force at the time with its `effectiveUntil` set, or `null` when only scheduled versions were ended. To start over, end the cap and set a new one.

### When a cap is reached

Paid evaluates caps on a schedule, about every three minutes, and sends the `billing-credit-cap-reached` webhook once per policy version and period when a unit's spend reaches its cap. See [Webhooks](/documentation/billing/webhooks#credit-cap-reached) for the payload. A new policy version can notify when breached, even if an earlier version had the same amount. An unchanged save creates no new version. Reaching a cap does not stop Paid from recording spend; it tells you, so that you can act.

## Error codes

| Code                                    | Status | Meaning                                                                                              |
| --------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------- |
| `FEATURE_NOT_ENABLED`                   | 403    | Customer units are not enabled for your organization.                                                |
| `CUSTOMER_UNIT_NOT_FOUND`               | 404    | No customer, or no unit with that key on this customer.                                              |
| `CUSTOMER_UNIT_EXISTS`                  | 409    | A unit with this `externalId` already exists on the customer.                                        |
| `ROOT_EXISTS`                           | 409    | The customer already has an active root; name a parent.                                              |
| `PARENT_NOT_FOUND`, `PARENT_NOT_ACTIVE` | 409    | The parent does not exist, or is deleted.                                                            |
| `ROOT_UNIT_IMMOVABLE`                   | 409    | The root cannot be re-parented.                                                                      |
| `PARENT_CYCLE`                          | 409    | The new parent is the unit itself or one of its descendants.                                         |
| `DEPTH_EXCEEDED`                        | 409    | The tree would exceed 16 levels.                                                                     |
| `CUSTOMER_UNIT_DELETED`                 | 409    | The unit is deleted; it cannot be changed or capped.                                                 |
| `HAS_ACTIVE_CHILDREN`, `HAS_ACTIVE_CAP` | 409    | The unit cannot be deleted yet.                                                                      |
| `SEAT_BILLING`                          | 409    | The customer is on seat-based billing; units are not available.                                      |
| `CAP_NOT_FOUND`                         | 404    | No cap in force for that unit and currency.                                                          |
| `CREDITS_CURRENCY_REQUIRED`             | 400    | Your organization has several credits currencies; name one.                                          |
| `CREDITS_CURRENCY_NOT_FOUND`            | 404    | The credits currency named does not exist.                                                           |
| `INVALID_SPEND_WINDOW`                  | 400    | Give `start` and `end` together, with `start` before `end` and at most 366 days apart, or omit both. |
| `BILLING_SERVICE_UNAVAILABLE`           | 500    | The cap usage or spend could not be computed right now; retry.                                       |

## Also available

The same operations are exposed as MCP tools generated from the API reference: `paid_list_customer_units`, `paid_create_customer_unit`, `paid_get_customer_unit`, `paid_update_customer_unit`, `paid_delete_customer_unit`, `paid_get_customer_unit_cap`, `paid_set_customer_unit_cap`, `paid_end_customer_unit_cap` and `paid_get_customer_unit_spend`, each with a `…_by_external_id` twin that addresses the customer by your own id.