Customer units and credit caps
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. That page is about billing several customer accounts through a parent account. This page is about the hierarchy inside one customer.
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/.
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.
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
externalIdusable 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 noparentExternalId. 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 forisRootif 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
The same call from the Paid CLI (@paid-ai/cli 1.2.0 or later):
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. 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.
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:
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
From the CLI: paid customer-unit-cap set-external --external-id acme --external-customer-unit-id eu-tenant amount=10000 frequency=MONTHLY.
amountis in credits of the cap’s currency, per period, and must be positive.creditsCurrencyIdmay 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 is404 CREDITS_CURRENCY_NOT_FOUND.frequencyisMONTHLY, the default. Periods use the schedule’s monthly UTC boundaries.effectiveFromis 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.
start(inclusive) andend(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, astartthat is not beforeend, or a longer window is400 INVALID_SPEND_WINDOW.spentis the same sum as a cap’sused, net of reversals: for a capped unit over its cap’s currentwindow, the two are equal. Pass a parent cap’swindowto 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
404and500answers 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 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
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.