Billing groups
A billing group is several of one customer's subscriptions billing onto one shared invoice: one document, one invoice number, one amount owed. Each member keeps its own products, its own stable id and its own lifecycle; the group decides only that they are invoiced together, on the group's billing dates, and paid from the group's card when it has one.
A group is not a parent-child
relationship. Parent-child links two customer records so that one customer pays
for another's subscriptions. A group sits inside one customer. The two do not
combine: a customer whose subscriptions are billed to a parent cannot hold a
group, and a customer holding a group cannot be billed to a parent. Either
attempt answers 400 SUBSCRIPTION_BILLING_GROUP_CUSTOMER_BILLED_TO_PARENT.
Over the public API you check whether a set of subscriptions could group, form the group, add and remove members, create a subscription straight onto a group, and read a group and the documents it issues. Every subscription id on this surface is the stable id. The group's card is the buyer's to save on the shared invoice; this API has no card verb. Full schemas live under the Billing Groups tag in the API Reference; the endpoints take the same scopes and license as the rest of the subscription API.
| I want to… | Call |
|---|---|
| Ask whether a set could form or join a group | POST /v1/billing-groups/eligibility |
| Form a group from existing subscriptions | POST /v1/billing-groups |
| Read a group and its current members | GET /v1/billing-groups/{id} |
| Add existing subscriptions to a group | POST /v1/billing-groups/{id}/members |
| Take a subscription off a group | DELETE /v1/billing-groups/{id}/members/{subscriptionId} |
| Create a subscription onto a group, or form a group around an existing one | POST /v1/subscriptions with joinBillingGroupId or formBillingGroupWithSubscriptionId, see Assign a subscription |
Form a group
POST /v1/billing-groups takes a lead subscription and 1 to 19 other
subscriptions of the same customer. The lead's next billing date becomes the
day the set starts billing together, and its payment terms, invoice delivery
and send offset are written onto every member.
Code
Code
billingActiveFrom is in the future: until that day each member keeps billing
on its own. Two optional fields decide how the shared invoice renders:
invoiceTemplateId names a consolidated document template, and
includeMemberDetailsPdf: true attaches one detail document per member to each
send. Both can be left out.
Add subscriptions to a group
POST /v1/billing-groups/{id}/members adds 1 to 19 existing subscriptions of
the group's customer. The group's billing date does not move. Joining changes
the subscription without a separate call:
- Its billing dates move onto the group's. A subscription whose cycles already break where the group's do keeps them.
- Its payment terms, invoice delivery department and contact, and send offset become the group's.
- Its card is discarded and
automaticBillingturns off. The group's card, when the buyer has attached one, pays for it. billingGroupIdon the subscription reports the group at once, before the first shared billing date.
Currency, billing direction, revenue deferral, direct-debit collection and the
payment-method mode are never rewritten. A subscription that differs from the
set on any of them is refused with a SUBSCRIPTION_BILLING_GROUP_*_MISMATCH
code, and the eligibility check below names it.
Check before you write
POST /v1/billing-groups/eligibility asks whether a set could form a group
(mode: "form", with leadSubscriptionId and memberSubscriptionIds) or join
one (mode: "join", with billingGroupId and subscriptionIds) and writes
nothing. It reports every refusal at once, where the write itself stops at the
first: at most one per subscription, named by subscriptionId, plus at most one
about the whole set, with subscriptionId: null.
Code
A subscription refused for two reasons reports the first; the second appears
once the first is fixed. A dissolved group answers eligible: false with a
set-level SUBSCRIPTION_BILLING_GROUP_NOT_JOINABLE; an unknown group is a
404. The check needs only the subscriptions-read scope and ignores
idempotency keys, so a caller can poll it as the set changes.
Take a subscription off a group
DELETE /v1/billing-groups/{id}/members/{subscriptionId} takes one
subscription off the group, effective at once. It keeps the billing date and
the fields the group gave it and bills on its own from its next billing date.
Code
A group needs two members. The departure that leaves fewer dissolves the group,
and the response says so with groupDissolved: true; the remaining subscription
bills on its own from its next billing date. The subscription must be a current
member of the group named in the path: otherwise the request answers
404 SUBSCRIPTION_BILLING_GROUP_MEMBER_NOT_FOUND and nothing changes, wherever
the subscription actually bills. Some members can only be taken off by the
account's operator in the LedgerBee app; for those the request answers
400 SUBSCRIPTION_BILLING_GROUP_MEMBER_NOT_REMOVABLE. While a shared invoice is still being
collected on the group's card, no member can leave: the request answers
400 SUBSCRIPTION_BILLING_GROUP_LEAVE_BLOCKED_BY_UNPAID_INVOICE naming the
invoice in details.invoiceId, because the leaver could no longer see or pay
the document its payment state points at.
A dissolved group
A dissolved group keeps answering GET /v1/billing-groups/{id}, with
dissolvedAt set and members: [], because the invoices and credit notes it
issued keep naming it. Everything that would put a subscription on it is
refused with 400 SUBSCRIPTION_BILLING_GROUP_NOT_JOINABLE: adding members,
creating a subscription onto it, and the eligibility check reports the same
code as a set-level refusal. Form a new group instead.
Recognize a grouped subscription
GET /v1/subscriptions/{id} and GET /v1/subscriptions carry billingGroupId:
the group the subscription bills on, or null when it bills on its own. A
subscription that has joined a group but not yet reached its first shared
billing date already reports the group, because the relationship exists before
it bills.
A grouped subscription reports paymentMethodId: null and
automaticBilling: false. The card is the group's, not the member's, and only
the buyer attaches one. Setting automaticBilling: true on a member answers
400 SUBSCRIPTION_BILLING_GROUP_MEMBER_DETAILS_LOCKED.
Recognize a group's documents
An invoice says who it bills through three fields, described in Invoices:
customerSubscriptionId | billingGroupId | isConsolidated | The document is |
|---|---|---|---|
| set | null | false | one subscription's own invoice |
null | set | true | a billing group's shared invoice |
null | null | true | a parent customer's invoice covering its child customers' subscriptions |
null | null | false | an invoice created by hand |
billingGroupId and customerSubscriptionId are never both set. List a group's
invoices with GET /v1/invoices?billingGroupId=<uuid>; the filter combines with
status, customerId and paging like every other list filter.
A credit note carries the same pair without isConsolidated: one issued for a
group's shared invoice carries billingGroupId, one issued for a single
subscription carries customerSubscriptionId. A group's credit notes render as
one flat document; the id is a payer identity, not a document shape. See
Credit notes.
The invoice's billingGroupId outlives the group. A subscription that has left
its group reads billingGroupId: null, and a dissolved group has no current
members, but every document the group issued keeps naming it. To find which
group a past invoice belonged to, read the invoice, not the subscription.
Reconcile the shared invoice
One shared invoice produces one subscription.billed per member, and every one
of those events carries the same invoiceId and invoiceNumber and the whole
document total as amount, never that member's share. Deduplicate on
invoiceId, attribute the set through billingGroupId, and never sum amount
across the events of one invoice. A three-member group billed 1000.00 emits
three events that each say 1000.00; the invoice is 1000.00.
billing.invoice_sent for a group's document carries subscriptionId: null
and the group in billingGroupId. The credit-note counterpart does the same.
When the group pays by card, each member receives the payment-succeeded event
when the group's charge clears, with the whole charged amount, the shared
billingGroupId, and that member's own paidThroughDate on the shared
invoice. If the group's retries run out, each member receives the
exhausted-retries error. A member never receives the per-decline charge error
and never enters awaiting_payment; the charge is the group's. Gate a member's
access on those two events. The topics and their payloads are on
Webhooks & reconciliation.
What a grouped subscription refuses
Members bill on one shared document, on one set of billing dates, so a change that would give one member its own document or move it off the group's dates is refused. The refusals below are behaviour of the subscription itself; over the public API they surface on cancellation and on the two update fields.
| Change | Response |
|---|---|
Cancel with refundBehavior: last_invoice | 400 SUBSCRIPTION_BILLING_GROUP_REFUND_LAST_INVOICE_NOT_SUPPORTED — the last invoice is the group's and covers other subscriptions. Use prorated or none. |
automaticBilling: true on a member | 400 SUBSCRIPTION_BILLING_GROUP_MEMBER_DETAILS_LOCKED — the card and the automatic charge belong to the group. |
prorationBehavior: always_invoice on a plan replacement or products edit | 400 SUBSCRIPTION_BILLING_GROUP_IMMEDIATE_PRORATION_NOT_ALLOWED — omit the field; the adjustment lands on the group's next shared invoice. |
| Taking on a usage commitment while a member: a new commitment, or a plan replacement onto a plan that commits a quantity | 400 SUBSCRIPTION_BILLING_GROUP_PREPAID_COMMITMENT_NOT_SUPPORTED. A commitment the subscription already holds is a different matter: it only stops the subscription from being prorated onto another billing date (SUBSCRIPTION_BILLING_GROUP_PREPAID_COMMITMENT_BLOCKS_REANCHOR at eligibility), so such a subscription can lead a group, or join one that already bills on its date. |
| Direction or cadence change, pause, resume, merge, billing-anchor reset | Refused with a SUBSCRIPTION_BILLING_GROUP_* code naming the change. These flows are operator-driven today; see Change flows. |
Cancelling a member is allowed with any other strategy. A group that falls below two members dissolves, and the remaining subscription bills on its own from its next billing date.
Errors & failure states
| Condition | What you observe | Fix |
|---|---|---|
| The customer is billed to a parent customer and a group is formed for it, or the reverse | 400 SUBSCRIPTION_BILLING_GROUP_CUSTOMER_BILLED_TO_PARENT | Choose one: bill the customer to its parent, or group its subscriptions. |
| Cancelling a member is blocked by the state of another member of the set | 400 SUBSCRIPTION_BILLING_GROUP_CANCEL_BLOCKED_BY_MEMBER, details.blockingSubscriptionId | Resolve the named subscription's state first, or ask the account's operator. |
| The group's membership changed while a request ran: a member was added, removed or cancelled concurrently, or the subscription being removed had moved to another group | 400 SUBSCRIPTION_BILLING_GROUP_MEMBERSHIP_CHANGED | Nothing was changed. Read the group again and retry. |
| Cancelling with a refund that would credit more than one invoice | 400 SUBSCRIPTION_BILLING_GROUP_CANCEL_REFUND_SPANS_INVOICES | Cancel with refundBehavior: none, or cancel the members separately. |
| The member can only be taken off its group in the LedgerBee app | 400 SUBSCRIPTION_BILLING_GROUP_MEMBER_NOT_REMOVABLE | Ask the account's operator to take it off there. |
| Any other change to the set this API has no verb for | 400 SUBSCRIPTION_BILLING_GROUP_OPERATION_NOT_ALLOWED | The operation is operator-only today. |
N subscription.billed events for one invoice | Expected: one per member, each carrying the whole total | Deduplicate on invoiceId; read the total from the invoice, not from a sum of events. |
| Forming or joining with a subscription of another customer | 400 SUBSCRIPTION_BILLING_GROUP_DIFFERENT_CUSTOMER | A group sits inside one customer. |
| A subscription that already holds a place in a group | 400 SUBSCRIPTION_BILLING_GROUP_ALREADY_IN_A_GROUP | Take it off its group first, or leave it out of this one. |
| Currency, billing direction, revenue deferral, direct-debit collection or payment-method mode differ across the set | 400 SUBSCRIPTION_BILLING_GROUP_CURRENCY_MISMATCH, …_BILLING_DIRECTION_MISMATCH, …_DEFER_REVENUE_MISMATCH, …_LS_COLLECTION_MISMATCH or …_PAYMENT_METHOD_MODE_MISMATCH | Joining never rewrites these. Align them first, or group only the subscriptions that agree; the eligibility check names the odd one out. |
| More than 20 subscriptions in one group | 400 SUBSCRIPTION_BILLING_GROUP_MEMBER_LIMIT | Form a second group. |
| Adding to, or creating a subscription onto, a dissolved group | 400 SUBSCRIPTION_BILLING_GROUP_NOT_JOINABLE, details.dissolvedAt | Form a new group. |
| Removing a subscription that is not a current member of the group named in the path | 404 SUBSCRIPTION_BILLING_GROUP_MEMBER_NOT_FOUND, details.groupDissolvedAt | Read the subscription's billingGroupId for the group it is in. |
| Removing a member while a shared invoice is still being collected on the group's card | 400 SUBSCRIPTION_BILLING_GROUP_LEAVE_BLOCKED_BY_UNPAID_INVOICE, details.invoiceId | Wait for the charge to clear, or for its retries to end. |
| An unknown group id | 404 SUBSCRIPTION_BILLING_GROUP_NOT_FOUND | The id is the billingGroupId a subscription or an invoice reports. |
| The group's billing date moved while a subscription was being created onto it | 409 SUBSCRIPTION_BILLING_GROUP_GRID_MOVED_DURING_ASSIGN | Nothing was created. Retry the request. |