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 keeps an internal id for each unit but never returns or accepts it.
A unit key you create 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. Roots that Paid created for customers predating the feature (see below) copied the customer’s identifier verbatim, so such a root’s key may be longer; every route still accepts it in the path.
The tree
The units of a customer form a tree: one root, and every other unit names a parent that exists.
- The root. When you create a customer with an
externalIdthat is usable as a unit key (non-empty, at most 255 characters, not.or..), Paid creates the root unit at the same moment, keyed by a copy of that external id. The root’s key is immutable from then on, whatever later happens to the customer’s external id. A customer created without a usable external id has no units until you create the root yourself, by creating a unit with noparentExternalId, under a key of your choosing. Paid creates no other unit. - Customers that predate customer units already have a root. When the feature shipped, Paid created one root for every existing customer, keyed by the first of these the customer had: its
externalId, its Paid display id (cus_…), or its Paid uuid. ItsexternalTypeiscustomerand itsnameis the customer’s name. List the customer’s units and look forisRoot: truebefore creating a root; creating one on such a customer answers409 ROOT_EXISTS. - One active root. Creating a unit without a parent while the customer already has an active root answers
409 ROOT_EXISTS. The root cannot be moved (ROOT_UNIT_IMMOVABLE), but it is ordinary in everything else: rename, type, cap, annotate or delete it like any other unit. Once the root is deleted (childless, no cap), the customer can be given a new root. - Parents. A parent must exist and be active (
PARENT_NOT_FOUND,PARENT_NOT_ACTIVE). A unit cannot be moved under itself or one of its descendants (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. The one exception is ending a cap, which stays available as clean-up for a customer that moved to seat billing while a cap was still open.
Creating units
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, name, externalType, status (ACTIVE or DELETED), parentExternalId (null on the root), isRoot, metadata, createdAt and updatedAt. There is no unit id in any response.
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.
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. A signal never creates a unit: that is always your call through the API. 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. A cap is addressed by the unit and a credits currency; cap versions have no id of their own.
Setting a cap
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 start on the day of the month ofeffectiveFrom, in UTC, clamped in shorter months.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.
Caps are versioned and never edited in place. Setting a cap records a new version and touches none of the existing ones; where versions overlap, the newest applies. The response is the version just recorded as policy (amount, frequency, effectiveFrom, effectiveUntil: null, createdAt, source), with currentPeriod when it is already in force and usage is available, null otherwise. 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.
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 cap and period when a unit’s spend reaches its cap. See Webhooks for the payload. 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 and paid_end_customer_unit_cap, each with a …_by_external_id twin that addresses the customer by your own id.