The plan catalog you sell, and metering what a subscriber consumes.

**Rendering pricing.** `GET /plans` returns your active plans so a partner-hosted pricing page stays correct through a price change instead of hardcoding one. A monthly and an annual plan sharing a `planGroup` are *one tier at two cadences* - group on that field, never on `name`.

```ts
const { plans } = await connect.billing.listPlans();
const tiers = groupBy(plans, (plan) => plan.planGroup);
```

**Metering an add-on.** A consumable add-on carries a per-cycle balance. Read it, then debit it as your users consume:

```ts
const { addons } = await connect.billing.getAddonUsage(workspaceId);
const credits = addons.find((addon) => addon.addonId === "extra_messages");

await connect.billing.consumeAddon({
  workspaceId,
  addonId: "extra_messages",
  quantity: 1,
  // Same key on a retry debits once. Send one per unit of work, not per attempt.
  idempotencyKey: messageId,
});
```

**`overagePolicy` is yours to honour.** `reject` means refuse the work once the allowance is gone - if you keep serving, the customer is getting something nobody is billing for and the setting is decorative. `bill` charges past the allowance up to `maxOverageUnits`.

Debiting is idempotent on `idempotencyKey` and nothing else. A retry without one double-charges a real customer.

## Endpoints

### GET `/api/v1/application/plans`

List this application's active plans

**Scope** `plans:read` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

Lists every currently active plan on this application - the same catalog the "Subscribe" flow reads from - so you can render your own pricing instead of hardcoding it. Application-wide, not per-workspace; use `subscription:read` above to check which one plan a specific workspace is actually on.

#### Implementation

**1. Prerequisite** - an API key scoped to `plans:read`.

**2. Send the request:**
```bash
curl "https://connect.cosmoops.com/api/v1/application/plans" \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"
```

**3. Read `plans`** - archived plans are never included, so nothing here needs its own status check.

`features` is a list of `{ key, name, description }`, joined against this application's own feature catalog - **render `name`, gate on `key`.** Note the asymmetry with `subscription:read`'s `plan.features`, which stays a bare `string[]` of keys: that one is a gating contract your backend compares against, and a display label is neither stable nor comparable. This one is a catalogue, and a catalogue that renders `campaign-module` where it meant "Campaign module" is just wrong. `name` falls back to the key for a plan predating this application's feature catalog.

#### Errors
`forbidden` (403 - key missing `plans:read`), `rate_limited` (429 - same read-heavy generous limit as `rbac:read`/`subscription:read`).

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Active plan catalog |
| `403` | forbidden - key missing plans:read |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `plans` | `array` | yes |  |

### GET `/api/v1/application/addons/workspaces/{workspaceId}`

Read remaining balance for every consumable add-on

**Scope** `addons:read` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

The read half of `addons:consume` below: how much of each consumable add-on a workspace has left this cycle. Without it an application could debit an allowance on every request and had no way to show its own users what remained - `subscription:read` returns `subscription.addons` as a list of ids and stops there.

#### Implementation

**1. Prerequisite** - an API key scoped to `addons:read`. Separate from `addons:consume` on purpose, the same way `drive:read` is separate from `drive:write`: a key that only renders a usage meter should not also be able to spend the balance down.

**2. Send the request:**
```bash
curl "https://connect.cosmoops.com/api/v1/application/addons/workspaces/WORKSPACE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"
```

**3. Render `balance` against `includedPerCycle`.** There is deliberately no "used" field: under `carryForward` an unspent balance rolls into the next cycle, so `balance` can exceed `includedPerCycle` and `included - balance` would go negative. `balance` itself goes negative only when `overagePolicy` is `"bill"` - that is accrued overage, capped at `maxOverageUnits`.

**4. `resetsAt`** is the end of the current billing cycle, when balances are re-granted.

A workspace with no subscription, no consumable add-ons, or none selected returns an empty `addons` array - not an error, since your reaction to all three is the same. Unlike consuming, this is **not** gated on the subscription being active: a paused subscription still holds its balance, and hiding it while someone works out what they have left would be perverse.

#### Errors
`forbidden` (403 - key missing `addons:read`), `rate_limited` (429 - same read-heavy limit as `subscription:read`).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | path | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Per-add-on balances |
| `403` | forbidden - key missing addons:read |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `string` | yes |  |
| `applicationId` | `string` | yes |  |
| `addons` | `array` | yes |  |
| `resetsAt` | `any` | yes |  |

### POST `/api/v1/application/addons/consume`

Debit a consumable add-on's balance

**Scope** `addons:consume` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

Debits units from a consumable add-on the workspace is subscribed to on this application (see `PlanAddonConsumable`) - the metered counterpart to the read-only endpoints above. Call it at whatever cadence your own usage happens (per request, per generation, per unit), not once per cycle.

#### Implementation

**1. Prerequisite** - an API key scoped to `addons:consume`, and the workspace must be actively subscribed to a plan whose selected add-ons include `addonId`.

**2. Send the request** - and send an `Idempotency-Key` with it. This matters more here than on any other endpoint in this reference: a retried consume without one debits the balance twice, and units cannot be un-spent.
```bash
curl -X POST https://connect.cosmoops.com/api/v1/application/addons/consume \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"workspaceId":"WORKSPACE_ID","addonId":"ADDON_ID","quantity":1,"description":"pdf-export"}'
```

**3. Read `balance`** - what remains after this call. `overageUnits` is how much of `quantity` went past the balance and will be billed on the next cycle; it is always `0` when the add-on's `overagePolicy` is `"reject"`, because that policy fails the call outright rather than partially succeeding.

#### Errors
`validation_error` (400 - malformed body, or an add-on that isn't consumable/isn't selected on the workspace's plan, or an inactive subscription), `forbidden` (403 - key missing `addons:consume`), `not_found` (404 - unknown workspace or add-on), `insufficient_balance` (409 - not enough balance and `overagePolicy` is `"reject"`), `rate_limited` (429).

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `uuid` | yes |  |
| `addonId` | `string` | yes |  |
| `quantity` | `integer` | yes |  |
| `description` | `string` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Units debited |
| `400` | validation_error - malformed body, non-consumable/unselected add-on, or inactive subscription |
| `403` | forbidden - key missing addons:consume |
| `404` | not_found - unknown workspace or add-on |
| `409` | insufficient_balance - balance too low and overagePolicy is "reject" |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `addonId` | `string` | yes |  |
| `consumed` | `number` | yes |  |
| `balance` | `number` | yes |  |
| `overageUnits` | `number` | yes |  |

### GET `/api/v1/application/credit/balance`

Read a workspace's prepaid credit

**Scope** `credit:read` &nbsp;·&nbsp; **Rate limit** 120 requests / min per key

A workspace's prepaid credit with your application - a rupee balance the customer buys from Connect whenever they like (GST charged on the top-up, invoiced by Connect) and your application spends per use. Unlike a consumable add-on it does not reset each cycle, and it can be topped up mid-cycle and credited back.

Amounts are **paise, pre-tax**. `spendable` is what a consume may take right now: the balance plus any overdraft Connect granted your application (usually none). `topUpPath` is the Connect page that sells more - hand it to `useConnect().openConnect()` and the customer returns to you when they have paid. `topUpAvailable` is `false` when your program is switched off.

#### Errors
`validation_error` (400), `forbidden` (403 - key missing `credit:read`), `not_found` (404 - the workspace holds no credit with you and has not installed your application), `rate_limited` (429).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | query | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | The balance |
| `400` | validation_error - workspaceId missing or malformed |
| `403` | forbidden - key missing credit:read |
| `404` | not_found - no credit and no subscription in that workspace |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `string` | yes |  |
| `applicationId` | `string` | yes |  |
| `label` | `string` | yes |  |
| `currency` | `string` | yes |  |
| `balance` | `number` | yes |  |
| `spendable` | `number` | yes |  |
| `overdraftLimit` | `number` | yes |  |
| `toppedUp` | `number` | yes |  |
| `consumed` | `number` | yes |  |
| `refunded` | `number` | yes |  |
| `topUpAvailable` | `boolean` | yes |  |
| `topUpPath` | `string` | yes |  |
| `updatedAt` | `any` | yes |  |

### GET `/api/v1/application/credit/ledger`

List a workspace's credit movements

**Scope** `credit:read` &nbsp;·&nbsp; **Rate limit** 120 requests / min per key

Every movement of a workspace's credit with your application, newest first: `topup` (the customer bought credit), `consume` and `refund` (yours), `adjustment` (Connect support) and `migration` (a balance carried over from an older Connect product). `amount` is signed; `balanceAfter` is the balance once it applied. Page with `cursor` = the last page's `nextCursor`.

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | query | yes |  |
| `cursor` | query | no |  |
| `limit` | query | no | 1-100, default 25 |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | One page of the ledger |
| `403` | forbidden - key missing credit:read |
| `404` | not_found - no credit and no subscription in that workspace |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `items` | `array` | yes |  |
| `nextCursor` | `any` | yes |  |

### POST `/api/v1/application/credit/consume`

Spend prepaid credit

**Scope** `credit:write` &nbsp;·&nbsp; **Rate limit** 300 requests / min per key

Debits a workspace's prepaid credit for one unit of work - a call when it ends, a message when it is sent.

#### Implementation

**1. Prerequisite** - an API key scoped to `credit:write`, and a credit program for your application in Connect (set up by the Connect team: the top-up packs, your SAC code and GST rate).

**2. Send `idempotencyKey` - it is required.** Use your own id for the unit of work (the call id). It becomes the ledger entry's id, written in the same transaction as the balance, so a retry answers `duplicate: true` with the original entry and moves nothing - however long after. Reusing a key for a different amount is refused with `idempotency_key_reused`.
```bash
curl -X POST https://connect.cosmoops.com/api/v1/application/credit/consume \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
  -H "Content-Type: application/json" \
  -d '{"workspaceId":"WORKSPACE_ID","amount":360,"idempotencyKey":"call_8f2a","description":"Call to +91 98xxx (4 min)","reference":"call_8f2a"}'
```

**3. Credit never goes negative unless Connect granted you an overdraft.** A consume larger than `spendable` is refused with `insufficient_credit` (409) and nothing moves; send the customer to `topUpPath`. Check `spendable` before work you cannot stop half-way.

#### Errors
`validation_error` (400), `forbidden` (403 - key missing `credit:write`), `not_found` (404), `insufficient_credit` (409), `idempotency_key_reused` (409), `rate_limited` (429).

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `uuid` | yes |  |
| `amount` | `integer` | yes |  |
| `idempotencyKey` | `string` | yes |  |
| `description` | `string` | yes |  |
| `reference` | `any` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Debited, or the replay of an earlier debit (duplicate: true) |
| `400` | validation_error - malformed body or idempotency key |
| `403` | forbidden - key missing credit:write |
| `404` | not_found - no credit and no subscription in that workspace |
| `409` | insufficient_credit, or idempotency_key_reused |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `duplicate` | `boolean` | yes |  |
| `entry` | `object` | yes |  |
| `balance` | `number` | yes |  |
| `spendable` | `number` | yes |  |

### POST `/api/v1/application/credit/refund`

Credit back an earlier consume

**Scope** `credit:write` &nbsp;·&nbsp; **Rate limit** 300 requests / min per key

Returns part or all of one earlier consume to the balance - a call that dropped, a message the carrier never delivered. Name the consume by the `idempotencyKey` it was sent with (`consumeIdempotencyKey`); several credit-backs may follow one consume, but never more in total than it took. Idempotent on its own `idempotencyKey`, exactly like `consume`. A refund here moves credit, not money: the customer's card is never touched.

#### Errors
`validation_error` (400 - more than what remains of that consume), `forbidden` (403), `not_found` (404 - no consume under that key), `idempotency_key_reused` (409), `rate_limited` (429).

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `uuid` | yes |  |
| `consumeIdempotencyKey` | `string` | yes |  |
| `amount` | `integer` | yes |  |
| `idempotencyKey` | `string` | yes |  |
| `description` | `string` | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Credited back, or the replay of an earlier credit-back |
| `400` | validation_error - malformed, or exceeds what remains of the consume |
| `403` | forbidden - key missing credit:write |
| `404` | not_found - no consume under that key |
| `409` | idempotency_key_reused |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `duplicate` | `boolean` | yes |  |
| `entry` | `object` | yes |  |
| `balance` | `number` | yes |  |
| `spendable` | `number` | yes |  |