Billing
The plan catalog this application sells, and metering usage back against a subscriber's add-on balance.
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.
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:
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 · 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:
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 · 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:
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 · 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.
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 · 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 · 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 · 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.
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 · 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 |