# Add-ons

An add-on is a subscription of its own — its own plan, products, status and
[stable id](/guides/subscriptions/data-model) — that bills on another
subscription's invoice, inside that subscription's section. The subscription it
bills with is its parent. An add-on has no invoices of its own, and cancelling
the parent ends it.

Create add-ons together with a subscription, or add one to a subscription that
already runs. Read the relation from either side, and remove an add-on by
cancelling it. Every endpoint below takes the scopes and license of the rest of
the subscription API (see [Subscriptions](/guides/subscriptions/overview)); full
schemas are in the [API Reference](/api).

| I want to… | Call |
|---|---|
| Create a subscription and its add-ons in one call | `POST /v1/subscriptions` with `addons` |
| Add an add-on to an existing subscription | `POST /v1/subscriptions/{id}/addons` |
| Turn existing subscriptions into add-ons | `POST /v1/subscriptions/{id}/addons/attach` |
| Check first whether they can be attached | `POST /v1/subscriptions/{id}/addons/eligibility` |
| List a subscription's add-ons | `GET /v1/subscriptions/{id}` or `GET /v1/subscriptions`, field `addons` |
| Find an add-on's parent | `GET /v1/subscriptions/{id}` or `GET /v1/subscriptions`, field `addonOfSubscriptionId` |
| See which invoice lines are an add-on's | `GET /v1/invoices/{id}`, line fields `subscriptionId` and `addonOfSubscriptionId` |
| Remove an add-on | `POST /v1/subscriptions/cancel` with the add-on's id |
| End a subscription and its add-ons | `POST /v1/subscriptions/cancel` with the parent's id |

## Read add-ons

`GET /v1/subscriptions/{id}` on a parent lists its add-ons under `addons`, oldest
first. On an add-on, `addonOfSubscriptionId` and `addonOfSubscriptionName` name
the parent and `addons` is empty.

```json
{
  "id": "0197a943-2325-7829-b835-b6c71a293065",
  "subscriptionName": "Premium Plan",
  "addonOfSubscriptionId": null,
  "addonOfSubscriptionName": null,
  "billingGroupId": null,
  "addons": [
    {
      "subscriptionId": "0197a944-1111-7aaa-8bbb-000000000001",
      "subscriptionName": "Extra seats",
      "planId": "550e8400-e29b-41d4-a716-446655440000",
      "status": "active",
      "addedOn": "2026-09-15",
      "effectiveNextBillingDate": "2026-10-01T00:00:00.000Z",
      "monthlyRevenue": "99.00",
      "currency": "DKK"
    }
  ]
}
```

| Field on `addons[]` | Meaning |
|---|---|
| `subscriptionId` | The add-on's own stable id. Read it, or cancel it, by this id. |
| `subscriptionName` | The add-on's current name. |
| `planId` | The plan the add-on runs on, or `null` for an add-on created without a plan. |
| `status` | The add-on's own status. |
| `addedOn` | The day (`YYYY-MM-DD`) the add-on was added to the parent. |
| `effectiveNextBillingDate` | When the add-on's next charge is created. It lands on the parent's invoice of that date. |
| `monthlyRevenue` | The add-on's own monthly recurring revenue. The parent's `monthlyRevenue` does not include it: the parent's full recurring amount is its own plus its add-ons'. |
| `currency` | Always the parent's currency. |

`GET /v1/subscriptions` carries `addonOfSubscriptionId` and
`addonOfSubscriptionName` on every row, and `addons` naming each add-on by
`subscriptionId` and `subscriptionName` only. Read a parent's detail for each
add-on's plan, status and next billing date.

`billingGroupId` is `null` on an add-on, and on a parent whose only companions
are its add-ons: a subscription with add-ons is not a
[billing group](/guides/subscriptions/billing-groups). On an add-on,
`scheduledChurnDate` reports the parent's scheduled cancellation, since that is
the day the add-on ends.

## Create a subscription with add-ons

`POST /v1/subscriptions` takes `addons`, up to 19 entries, each naming a plan,
optional product overrides and an optional `cadence` (see
[Cadence](#cadence)). The subscription and its add-ons are created in
one transaction and billed by one billing run, on one invoice. A refused add-on
creates nothing, the subscription included.

```bash
curl -X POST https://api.ledgerbee.com/api/v1/subscriptions \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "0197a943-0f1e-7a2b-9c3d-4e5f6a7b8c9d",
    "planId": "550e8400-e29b-41d4-a716-446655440100",
    "startDate": "2026-09-15",
    "addons": [
      { "planId": "550e8400-e29b-41d4-a716-446655440000" },
      {
        "planId": "550e8400-e29b-41d4-a716-446655440001",
        "productOverrides": [{ "priceId": "550e8400-e29b-41d4-a716-446655440002", "quantity": 5 }]
      }
    ]
  }'
```

Each add-on takes everything except its plan and product overrides from the
subscription in the request: the customer, `startDate`, `endDate`,
`prorationBehavior`, `billingDirection`, the invoice department and contact, and
the subscription's payment terms, revenue deferral and Leverandørservice
collection. The response is the new subscription, with its add-ons listed under
`addons`. Their first charge rides the subscription's first invoice.

`addons` cannot be combined with `joinBillingGroupId` or
`formBillingGroupWithSubscriptionId`. Either pair answers
`400 SHARED_VALIDATION_FAILED` with
`details.reason: "ADDONS_WITH_BILLING_GROUP_PLACEMENT"` and `details.fields`
naming both fields, and nothing is created.

## Add an add-on to an existing subscription

`POST /v1/subscriptions/{id}/addons` creates one add-on of the subscription
`{id}` names and answers `201` with the new add-on, not the parent. Read the
parent afterwards to see it listed under `addons`.

```bash
curl -X POST https://api.ledgerbee.com/api/v1/subscriptions/0197a943-2325-7829-b835-b6c71a293065/addons \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "planId": "550e8400-e29b-41d4-a716-446655440000",
    "startDate": "2026-09-15",
    "prorationBehavior": "none"
  }'
```

| Field | Required | Effect |
|---|---|---|
| `planId` | yes | The plan the add-on is created from. It must bill in the parent's currency. |
| `productOverrides` | no | Quantity overrides for products in the add-on's plan, the same shape as on [Assign a subscription](/guides/subscriptions/assign#request-fields). |
| `cadence` | no | How often the add-on bills, such as `month-1` or `year-1`. Defaults to the plan's own cadence. See [Cadence](#cadence). |
| `startDate` | no | When the customer gets the add-on. Defaults to today. |
| `prorationBehavior` | no | How a partial period from `startDate` to the add-on's first full cycle is handled. Default `none`. See [Proration](/guides/subscriptions/proration). |

The parent supplies the customer, billing direction, revenue deferral,
Leverandørservice collection, send offset, invoice department and contact,
payment terms, allowed payment providers and payment details.

The add-on bills on the parent's invoices. Its cycles break on the parent's
billing dates, stepped back on the add-on's own cadence to the first one on or
after `startDate`:

- An add-on on the parent's cadence that starts on one of the parent's billing
  dates - for example today, when the parent billed today - bills its first
  full period from that date. The invoice is stamped with the parent and
  carries only the add-on's lines, because the parent's own period is already
  billed.
- An add-on that starts between two of the parent's billing dates has a
  partial period up to its first cycle date, handled by `prorationBehavior`.
- An add-on on a shorter cadence than its parent - a monthly add-on on a
  yearly subscription - bills on its own cycle dates, each on an invoice
  stamped with the parent.

A parent that is itself an add-on answers
`400 SUBSCRIPTION_ADDON_PARENT_IS_AN_ADDON`. A parent with a scheduled
cancellation answers `400 SUBSCRIPTION_ADDON_PARENT_HAS_SCHEDULED_END` (with
`details.endsOn`) or, while the cancellation is still a queued change,
`400 SUBSCRIPTION_BILLING_GROUP_MEMBER_HAS_PENDING_CHANGES`. Either way nothing
is created.

### Cadence

An add-on bills at its plan's own cadence unless the request names another with
`cadence`, written `<interval>-<count>`: `month-1`, `month-3`, `year-1`. The
value must be the plan's own cadence or one of its alternative cadences; any
other answers `400 SHARED_VALIDATION_FAILED` with
`details.reason: "ADDON_CADENCE_NOT_ON_PLAN"` and `details.allowedCadences`
listing the ones the plan offers, and nothing is created.

The cadence can differ from the parent's. A monthly add-on of a yearly
subscription bills every month on invoices stamped with the parent; each
invoice dated when the parent is not due carries only the add-on lines due that
day.

## Turn existing subscriptions into add-ons

`POST /v1/subscriptions/{id}/addons/attach` turns subscriptions the customer
already has into add-ons of `{id}`. Each keeps its own id, plan and products,
and from then on bills on the parent's invoice, inside its section, and ends
when the parent is cancelled.

```bash
curl -X POST https://api.ledgerbee.com/api/v1/subscriptions/0197a943-2325-7829-b835-b6c71a293065/addons/attach \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "subscriptionIds": ["0197a944-1111-7aaa-8bbb-000000000001"] }'
```

**Attaching cannot be undone.** An add-on cannot be detached to bill on its
own again; it ends only by being cancelled. Each attached subscription takes
the parent's invoice terms and payment settings and moves onto the parent's
billing dates; one that bills on other dates is prorated onto them.

An attached subscription loses its own saved card, because its invoice is now
the parent's, which only the parent's card pays. If the parent bills together
with others in a [billing group](/guides/subscriptions/billing-groups) and the
attach leaves that group billing only the parent, the group's card is removed
too, and the buyer is told. The response is the parent's detail, with the
attached subscriptions under `addons`, plus `cardsRemoved`:

```json
{
  "id": "0197a943-2325-7829-b835-b6c71a293065",
  "addons": [{ "subscriptionId": "0197a944-1111-7aaa-8bbb-000000000001", "subscriptionName": "Extra seats" }],
  "cardsRemoved": [
    {
      "subscriptionId": "0197a944-1111-7aaa-8bbb-000000000001",
      "billingGroupId": null,
      "paymentMethodId": "0197a945-2222-7ccc-8ddd-000000000002"
    }
  ]
}
```

A card removed from a billing group names the group in `billingGroupId` and
carries `subscriptionId: null`.

### Check before attaching

`POST /v1/subscriptions/{id}/addons/eligibility` answers what the attach would,
without changing anything: every refusal at once, where the attach stops at the
first, and the cards it would remove. Omit `subscriptionIds` to ask only
whether `{id}` can take add-ons at all.

```bash
curl -X POST https://api.ledgerbee.com/api/v1/subscriptions/0197a943-2325-7829-b835-b6c71a293065/addons/eligibility \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "subscriptionIds": ["0197a944-1111-7aaa-8bbb-000000000001"] }'
```

```json
{
  "eligible": false,
  "refusals": [
    {
      "code": "SUBSCRIPTION_ADDON_PARENT_MISMATCH",
      "subscriptionId": "0197a944-1111-7aaa-8bbb-000000000001",
      "details": { "field": "currency", "value": "EUR", "parentValue": "DKK" }
    }
  ],
  "cardsRemoved": []
}
```

Each refusal carries the code and details the attach would answer with. It
needs the `subscriptions-read` scope; the attach needs `subscriptions-write`.
An unknown subscription is a `404` on both.

## Remove an add-on

An add-on is removed by cancelling it:
`POST /v1/subscriptions/cancel` with the add-on's id and any strategy. It bills
on its parent's invoice, so `refundBehavior: "last_invoice"` answers
`400 SUBSCRIPTION_ADDON_REFUND_LAST_INVOICE_NOT_SUPPORTED`; use
`prorated` or `none`. The same code answers a parent cancelled with
`last_invoice` while it has add-ons and bills on its own invoice, since that
invoice carries their lines; a parent that is a member of a billing group
answers `SUBSCRIPTION_BILLING_GROUP_REFUND_LAST_INVOICE_NOT_SUPPORTED`.

An add-on cannot be detached from its parent to bill on its own. Taking it off a
billing group with `DELETE /v1/billing-groups/{id}/members/{subscriptionId}`
answers `400 SUBSCRIPTION_ADDON_CANNOT_LEAVE_ALONE`.

## Cancel a parent

Cancelling a parent cancels its add-ons with it. The cancel response is the
parent's detail plus `cascadedAddonSubscriptionIds`, the add-ons the
cancellation takes along:

| `strategy` | `cascadedAddonSubscriptionIds` |
|---|---|
| `immediately` | The add-ons that ended now. |
| `end_of_cycle` / `specific_date` | The add-ons that will end with the parent, as they stand when the request is made. |
| `clear_schedule` | Always `[]`. |

```json
{
  "id": "0197a943-2325-7829-b835-b6c71a293065",
  "status": "active",
  "scheduledChurnDate": "2026-12-01T00:00:00.000Z",
  "cascadedAddonSubscriptionIds": ["0197a944-1111-7aaa-8bbb-000000000001"]
}
```

A scheduled cancellation is stamped on the parent alone, and the add-ons to end
are resolved again on the day it takes effect. Until then each add-on keeps its
current status and reports the parent's date as its `scheduledChurnDate`. With
`end_of_cycle`, the parent and its add-ons end together, on the latest period
end among them, so an add-on on another cadence does not bill a stub period.

An immediate cancellation settles the parent and its add-ons on one document,
and has three refusals of its own:

- `400 SUBSCRIPTION_ADDON_CASCADE_MEMBER_NOT_CHURNABLE` — an add-on is in a
  status it cannot be cancelled from. `details.addonSubscriptionId` names it.
  An add-on you cancelled immediately a moment earlier is still settling its
  final charge, and is named with `details.currentStatus: "cancelled"` until
  that settlement has run. Retry shortly. A scheduled cancellation is not
  affected: it leaves such an add-on out.
- `400 SUBSCRIPTION_ADDON_CASCADE_MIXED_CREDITED_INVOICES` — the refund would
  credit more than one invoice. `details.participants` lists each subscription
  with the `creditedInvoiceNumber` it would credit. Cancel with
  `refundBehavior: "none"`, or cancel the add-ons first.
- `400 SUBSCRIPTION_ADDON_CASCADE_SET_CHANGED` — an add-on was added or removed
  while the request ran. Nothing was cancelled; read the parent again and retry.

## Rules

| Rule | What it means |
|---|---|
| One level deep | An add-on cannot have add-ons of its own, and an add-on cannot be a parent. |
| Same terms as the parent | An add-on belongs to the parent's customer, bills in the parent's currency and has the parent's billing direction, revenue deferral and Leverandørservice collection. One that differs is refused with `SUBSCRIPTION_ADDON_PARENT_MISMATCH`, `details.field` naming the setting and `details.value` / `details.parentValue` the two values. |
| Billed on the parent's invoice | The add-on's lines appear inside the parent's section of the parent's invoice, after the parent's own lines. The invoice's `customerSubscriptionId` is the parent's; the add-on has no invoices of its own. See [Find an add-on's invoices](#find-an-add-ons-invoices). |
| Its own cadence | An add-on may bill on a different cadence from its parent. A monthly add-on of a yearly parent bills every month, on the parent's invoice, on its own due dates. An invoice dated when the parent is not due carries only the lines of the add-ons due that day. |
| No card of its own | The add-on is paid with the parent's invoice. `automaticBilling: true` on an add-on answers `400 SUBSCRIPTION_ADDON_DETAILS_FOLLOW_PARENT`. A parent whose only companions are its add-ons keeps its own card, which pays for the add-ons' lines too. |
| The parent's billing grid is fixed | The add-ons are anchored to the parent's billing dates. While a parent has add-ons, a direction or cadence change, pause, resume, billing-anchor reset, send-offset change or merge on it is refused with `SUBSCRIPTION_ADDON_PARENT_GRID_LOCKED`. Cancel the add-ons to lift it. These are operator-driven flows today; see [Change flows](/guides/subscriptions/changes). |
| The add-on's grid and details are its parent's | The same changes on an add-on are refused with `SUBSCRIPTION_ADDON_FOLLOWS_PARENT_GRID`, and a change to its invoice terms or payment settings with `SUBSCRIPTION_ADDON_DETAILS_FOLLOW_PARENT`: change them on the parent, which passes them on to its add-ons. |
| Ends with the parent | Cancelling the parent cancels every add-on, on the parent's date. |
| Not a billing group | A parent with add-ons reports `billingGroupId: null`. It can join or form a [billing group](/guides/subscriptions/billing-groups) like any other subscription, and its add-ons come with it. |

## Add-ons and billing groups

A member of a billing group can have add-ons. They bill inside that member's
section of the group's shared invoice.

- `GET /v1/billing-groups/{id}` lists each member's add-ons under
  `members[].addons`. An add-on is never a member of its own.
- Each add-on uses one of the group's slots. `slotsUsed` counts members plus
  their add-ons against `maxSlots` (20). A write that would exceed it answers
  `400 SUBSCRIPTION_BILLING_GROUP_MEMBER_LIMIT` with `details.slotsUsed`,
  `details.slotsRequested` and `details.maxSlots`.
- Forming a group with, or adding, a subscription that has add-ons brings the
  add-ons along. The form and add responses list them under the member they
  bill with.
- `POST /v1/subscriptions/{id}/addons` on a group member adds the add-on to the
  member's group, where it takes a slot.
- Taking a member off the group takes its add-ons with it. An add-on cannot be
  taken off alone.
- An add-on reports `billingGroupId: null` even when its parent is a member of
  a group; read the parent for the group.

`POST /v1/subscriptions` cannot create add-ons and place the subscription on a
group in the same call. Create the subscription onto the group, then add the
add-ons with `POST /v1/subscriptions/{id}/addons`.

A group whose other members all become add-ons of one of them stops billing
together: `GET /v1/billing-groups/{id}` then answers `billsTogether: false`, and
its card, if it had one, is removed and named in the attach's `cardsRemoved`.

## Find an add-on's invoices

An add-on has no invoices of its own: its lines ride on its parent's invoice,
which is addressed to the parent. Each line on `GET /v1/invoices/{id}` names
the subscription it was billed for in `subscriptionId` (an add-on's lines name
the add-on), and an add-on's lines name the parent in `addonOfSubscriptionId`.
To find every invoice an add-on was billed on, list
`GET /v1/invoices?subscriptionId=<add-on id>`: it returns the parent's invoices
that carry the add-on's lines. See
[Invoices](/guides/billing-documents/invoices).

## Selling add-ons at checkout

A plan item in the catalogue lists the add-ons a buyer may take with it, and the
partner-hosted checkout sells them with the plan, on the plan's invoice. See
[Partner-Hosted Checkout → Add-ons](/guides/partner-checkout#add-ons).

## Support matrix

| | `POST /v1/subscriptions` with `addons` | `POST /v1/subscriptions/{id}/addons` | `POST /v1/subscriptions/{id}/addons/attach` | Partner checkout `addons` |
|---|---|---|---|---|
| Creates | The subscription and its add-ons | One add-on of an existing subscription | Nothing: existing subscriptions become add-ons | The plan's subscription and its add-ons |
| Add-ons per call | Up to 19 | 1 | Up to 19 | Up to 19, each plan once |
| Add-on starts | On the subscription's `startDate` | On `startDate`, default today | Already running | On the session's start date |
| First billed | On the subscription's first invoice | On the parent's invoice on the add-on's first cycle date | On the parent's invoice on its next billing date | On the subscription's first invoice |
| Cadence | `cadence`, default the add-on plan's own | `cadence`, default the add-on plan's own | Kept | The plan item's period |
| Card | None | None | Its own card is removed | None |
| Undo | Cancel the add-on | Cancel the add-on | Cancel the add-on; it cannot be detached | Cancel the add-on |
| Response | The subscription, with `addons` | The new add-on | The parent, with `addons` and `cardsRemoved` | The confirm names the plan's subscription |

## Errors & failure states

| Condition | What you observe | Fix |
|---|---|---|
| `addons` sent with `joinBillingGroupId` or `formBillingGroupWithSubscriptionId` | `400 SHARED_VALIDATION_FAILED`, `details.reason: "ADDONS_WITH_BILLING_GROUP_PLACEMENT"` | Create the subscription onto the group first, then add the add-ons. |
| More than 19 entries in `addons` | `400 SHARED_VALIDATION_FAILED` | Send at most 19. |
| The parent named in `/v1/subscriptions/{id}/addons` is itself an add-on | `400 SUBSCRIPTION_ADDON_PARENT_IS_AN_ADDON`, `details.subscriptionId` (the add-on you named) | Add the add-on to that add-on's parent instead. |
| The parent has a scheduled cancellation | `400 SUBSCRIPTION_ADDON_PARENT_HAS_SCHEDULED_END`, `details.endsOn` | Clear the cancellation with `clear_schedule`, or leave the parent to end. |
| The parent has a queued change | `400 SUBSCRIPTION_BILLING_GROUP_MEMBER_HAS_PENDING_CHANGES`, `details.pendingChangeIds` | Wait for the change to take effect, or cancel it from the parent's scheduled changes — each id is a `changeId` that `GET /v1/subscriptions/{id}/scheduled-changes` lists. |
| The add-on's plan, or a subscription being attached, differs from the parent in customer, currency, billing direction, revenue deferral or Leverandørservice collection | `400 SUBSCRIPTION_ADDON_PARENT_MISMATCH`, `details.field`, `details.value`, `details.parentValue` | Pick a plan, or a subscription, that matches the parent. |
| `cadence` is not one the add-on's plan offers | `400 SHARED_VALIDATION_FAILED`, `details.reason: "ADDON_CADENCE_NOT_ON_PLAN"`, `details.allowedCadences` | Send one of `allowedCadences`, or omit `cadence`. |
| The parent is named among the subscriptions to attach | `400 SUBSCRIPTION_ADDON_SELF_ATTACH`, `details.subscriptionId` | Leave the parent out. |
| A subscription to attach is already an add-on of another subscription | `400 SUBSCRIPTION_ADDON_TARGET_ALREADY_AN_ADDON`, `details.addonOfSubscriptionId` | Attach it to that parent, or cancel it. |
| A subscription to attach has add-ons of its own | `400 SUBSCRIPTION_ADDON_JOINER_HAS_ADDONS`, `details.addonCount` | Add-ons are one level deep; attach its add-ons' parent instead of it, or cancel its add-ons. |
| A subscription to attach is billed together with others in a different billing group | `400 SUBSCRIPTION_BILLING_GROUP_ALREADY_IN_A_GROUP` | Take it off that group first. |
| The parent's set, or its group, would exceed 20 slots | `400 SUBSCRIPTION_BILLING_GROUP_MEMBER_LIMIT`, `details.slotsUsed`, `details.slotsRequested`, `details.maxSlots` | Cancel an add-on or remove a member first. |
| Taking an add-on off a billing group | `400 SUBSCRIPTION_ADDON_CANNOT_LEAVE_ALONE`, `details.subscriptionId` | Cancel the add-on, or take its parent off the group. |
| Cancelling an add-on, or a parent with add-ons outside a billing group, with `refundBehavior: "last_invoice"` | `400 SUBSCRIPTION_ADDON_REFUND_LAST_INVOICE_NOT_SUPPORTED` (a group member: `SUBSCRIPTION_BILLING_GROUP_REFUND_LAST_INVOICE_NOT_SUPPORTED`) | Use `prorated` or `none`. |
| Cancelling a parent immediately while an add-on cannot be cancelled | `400 SUBSCRIPTION_ADDON_CASCADE_MEMBER_NOT_CHURNABLE`, `details.addonSubscriptionId` | Resolve the named add-on's state first. |
| Cancelling a parent immediately with a refund that spans invoices | `400 SUBSCRIPTION_ADDON_CASCADE_MIXED_CREDITED_INVOICES`, `details.participants` | Cancel with `refundBehavior: "none"`, or cancel the add-ons first. |
| An add-on was added or removed while a cancellation ran | `400 SUBSCRIPTION_ADDON_CASCADE_SET_CHANGED` | Nothing was cancelled. Read the parent again and retry. |
| `automaticBilling: true`, or another invoice or payment setting, on an add-on | `400 SUBSCRIPTION_ADDON_DETAILS_FOLLOW_PARENT`, `details.field` | Change it on the parent, or set up card billing there. |
| Moving the parent's billing grid while it has add-ons | `400 SUBSCRIPTION_ADDON_PARENT_GRID_LOCKED` | Cancel the add-ons first. |

A refusal answered outside `/v1/billing-groups` never carries
`details.billingGroupId`, whatever the code.
