Subscriptions API
The Subscriptions API manages a Tenant's annual credit subscription. Customers
purchase credits that are valid for a full year and draw down from a shared
credit pool as the platform is used. Subscriptions, credit
packs, and billing settings live in the License Manager; Informer proxies those
operations and keeps a local entitlement cache in sync. All routes are prefixed
with /api.
This API covers buying and managing credits. For how credits are governed (usage profiles, model tiers, per-actor caps) and reported, see Usage & Credits.
Every route here is a pure License Manager proxy. The handler calls the License
Manager and returns its response.data; the money-touching routes refresh the
local entitlement cache afterward and append a cacheUpdated boolean.
Authentication
All endpoints require permission.billing.manage (the route delegates to the
billingManage global permission). The billing gate is not AI-gated: the AI
module does not need to be enabled on the Tenant to manage a subscription.
The money-touching endpoints (_cancel, _reactivate, _purchase,
_convert-dollars) refresh the local AiEntitlementCache after the License
Manager call and return a cacheUpdated boolean. cacheUpdated: false means
the License Manager operation succeeded but the local cache is momentarily
stale, so the client should trigger its own refresh rather than show an
unconditional success.
GET /api/usage/subscription
Get the active subscription for the current license. Proxies the License Manager
GET platform/subscription.
Authentication: permission.billing.manage
Response:
The License Manager body is nested under subscription (the payload is
{ subscription: ... }, or { subscription: null } when there is no active
subscription), not a flat subscription object.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/subscription"
}
},
"subscription": {
"id": "sub-1",
"status": "active",
"billingCycle": "monthly",
"startedAt": "2026-01-15T16:20:00.000Z",
"renewsAt": "2026-01-15T16:20:00.000Z",
"graceEndsAt": null,
"creditPackId": "00000000-0000-4000-8000-000000000001",
"creditPackQuantity": 1,
"pendingCancel": false,
"pack": {
"name": "Starter",
"slug": "starter-annual",
"credits": 2400,
"price": 300,
"type": "subscription"
},
"pendingPack": null
}
}
Subscription properties:
| Field | Type | Description |
|---|---|---|
id | string | Subscription id |
status | string | Subscription status (e.g. active) |
billingCycle | string | Billing cycle (e.g. monthly) |
startedAt | date | When the subscription began |
renewsAt | date | Next renewal date |
graceEndsAt | date | null | End of the grace period when past due |
creditPackId | string (UUID) | Id of the subscribed credit pack |
creditPackQuantity | integer | Number of packs on the subscription |
pendingCancel | boolean | true when cancellation is scheduled at term end |
pack | object | Embedded credit pack (name, slug, credits, price, type) |
pendingPack | object | null | Pending pack change, when one is queued |
There is no term, creditsIncluded, cardLast4, or top-level
cancelAtPeriodEnd field. The scheduled-cancellation signal is
subscription.pendingCancel.
POST /api/usage/subscription/_cancel
Schedule cancellation of the subscription at the next renewal date. Credits
remain spendable until the end of the current annual term. Proxies the License
Manager POST platform/subscription/_cancel, then refreshes the local
AiEntitlementCache.
Authentication: permission.billing.manage
Payload: empty
Response:
Responds 200 with { cancelled, subscription, balance, cacheUpdated }. The
pending-cancel flag is subscription.pendingCancel: true (not a top-level
cancelAtPeriodEnd). The balance still reflects the live pool because credits
remain spendable until the term ends.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/subscription/_cancel"
}
},
"cancelled": true,
"subscription": {
"id": "sub-1",
"status": "active",
"renewsAt": "2026-01-15T16:20:00.000Z",
"pendingCancel": true
},
"balance": {
"included": 5000,
"used": 1250,
"remaining": 3750,
"slush": 250,
"dollarBalance": 50
},
"cacheUpdated": true
}
POST /api/usage/subscription/_reactivate
Undo a pending cancellation. Proxies the License Manager POST platform/subscription/_reactivate, then refreshes the cache.
Authentication: permission.billing.manage
Payload: empty
Response:
Responds 200 with { reactivated, subscription, cacheUpdated }. The response
sets subscription.pendingCancel: false.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/subscription/_reactivate"
}
},
"reactivated": true,
"subscription": {
"id": "sub-1",
"status": "active",
"renewsAt": "2026-01-15T16:20:00.000Z",
"pendingCancel": false
},
"cacheUpdated": true
}
GET /api/usage/subscription/proration-preview
Preview the prorated charge for changing to a different credit pack mid-term.
Read-only proxy to the License Manager GET platform/subscription/proration-preview. There is no cache refresh and no
cacheUpdated. Both this preview and the eventual purchase route through the
same License Manager calculation, so the figures shown match what the customer
is billed.
Authentication: permission.billing.manage
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
packSlug | string | Yes | Slug of the target credit pack |
packSlug is a required query param. Omitting it answers 400.
Response:
The License Manager body carries dollar math, credit math, and direction/state flags, well beyond a single proration figure.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/subscription/proration-preview"
}
},
"daysRemaining": 364,
"dailyRate": 49.32,
"prorationAmount": 17952.48,
"annualDifference": 18000,
"creditDifference": 200000,
"proratedCredits": 199452,
"isUpgrade": true,
"isDowngrade": false,
"pastDue": false,
"withinGrace": false,
"hasActiveSubscription": true,
"currentPackSlug": "starter-annual",
"targetPackSlug": "pro-annual",
"renewsAt": "2026-01-15"
}
| Field | Type | Description |
|---|---|---|
daysRemaining | integer | Days left in the current term |
dailyRate | number | Daily rate used for the proration |
prorationAmount | number | Dollar amount charged for the change |
annualDifference | number | Annual price difference between packs |
creditDifference | integer | Annual credit difference between packs |
proratedCredits | integer | Credits granted for the remainder of the term |
isUpgrade / isDowngrade | boolean | Direction of the change |
pastDue | boolean | Whether the subscription is past due |
withinGrace | boolean | Whether the subscription is within its grace period |
hasActiveSubscription | boolean | Whether an active subscription exists |
currentPackSlug / targetPackSlug | string | Current and target pack slugs |
renewsAt | date | Next renewal date |
GET /api/usage/credit-packs
List the credit packs available for purchase. Proxies the License Manager GET platform/credit-packs.
Authentication: permission.billing.manage
Response:
The response is a top-level array of pack objects, not a
{ creditPacks: [...] } wrapper, so it carries no _links envelope.
[
{
"id": "00000000-0000-4000-8000-000000000001",
"slug": "starter-annual",
"name": "Starter",
"credits": 2400,
"type": "subscription",
"productId": "qb-item-starter",
"purchasable": true,
"pricePerCredit": 0.125,
"price": 300
},
{
"id": "00000000-0000-4000-8000-000000000002",
"slug": "pro-annual",
"name": "Pro",
"credits": 12000,
"type": "subscription",
"productId": "qb-item-pro",
"purchasable": true,
"pricePerCredit": 0.1,
"price": 1200
},
{
"id": "00000000-0000-4000-8000-000000000003",
"slug": "business-annual",
"name": "Business",
"credits": 60000,
"type": "subscription",
"productId": "qb-item-business",
"purchasable": true,
"pricePerCredit": 0.1,
"price": 6000
},
"… 1 more items (4 total) — omitted from docs"
]
Credit pack properties:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Pack id |
slug | string | URL-safe pack slug |
name | string | Display name |
credits | integer | Annual credits per unit |
type | string | subscription or one-off |
productId | string | External billing product id |
purchasable | boolean | Whether the pack can be bought |
pricePerCredit | number | Price per credit |
price | number | Annual price per unit |
credits and price are annual per unit. Per-month consumers divide by 12.
POST /api/usage/_purchase
Purchase a credit pack. Proxies the License Manager POST platform/_purchase,
then writes the authoritative balance straight into the entitlement cache so
the credits are immediately visible.
Authentication: permission.billing.manage
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
packSlug | string | Yes | Credit pack slug |
cardId | string | Yes | Payment card token |
quantity | integer | No | Number of packs (min 1, default 1) |
idempotencyKey | string | No | Forwarded to the License Manager so a double-submit cannot charge the card twice |
Example Request:
{ "packSlug": "starter-annual", "cardId": "qb-card-1", "quantity": 1 }
Response:
Responds 200 with the License Manager purchase body (credit, subscription,
prorationCharge, balance) plus cacheUpdated. The balance object is
{ included, used, remaining, slush, dollarBalance }.
{
"packSlug": "starter-annual",
"cardId": "qb-card-1",
"quantity": 1
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/_purchase"
}
},
"credit": {
"id": "credit-1",
"credits": 5000,
"paymentType": "subscription"
},
"subscription": {
"id": "sub-1",
"billingCycle": "monthly",
"renewsAt": "2026-01-15T16:20:00.000Z"
},
"prorationCharge": null,
"balance": {
"included": 5000,
"used": 0,
"remaining": 5000,
"slush": 250,
"dollarBalance": 50
},
"cacheUpdated": true
}
POST /api/usage/_convert-dollars
Promote legacy dollar-unit credits (from pre-3.0.0 balances) into the spendable
credit pool. One-way migration. Proxies the License Manager POST platform/_convert-dollars, refreshes the cache, and patches legacyDollars: 0
so the "Convert" prompt disappears immediately.
Authentication: permission.billing.manage
Payload: empty. The License Manager identifies the installation by its JWT.
Response:
Responds 200 with
{ promoted, dollarsPromoted, creditsPromoted, balance, cacheUpdated }.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/_convert-dollars"
}
},
"promoted": 3,
"dollarsPromoted": 50,
"creditsPromoted": 1000,
"balance": {
"included": 1000,
"used": 0,
"remaining": 1000,
"slush": 0,
"dollarBalance": 0
},
"cacheUpdated": true
}
GET /api/usage/billing-config
Get the auto-replenish and alert settings. Proxies the License Manager GET platform/billing-config.
Authentication: permission.billing.manage
Response:
The auto-replenish amount is paygDollars (dollars per refill), with packSlug
kept nullable for back-compat. The GET additionally returns monthlySpent and
monthlyRemaining (the running auto-refill count this month). The
alerts.thresholds keys are 80, 90, 95, and exhausted.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/billing-config"
}
},
"replenish": {
"enabled": true,
"paygDollars": 30,
"packSlug": null,
"cardId": "qb-card-1",
"monthlyCap": 3,
"monthlySpent": 1,
"monthlyRemaining": 2
},
"alerts": {
"recipients": [
"ops@example.com",
"billing@example.com"
],
"thresholds": {
"80": true,
"90": true,
"95": false,
"exhausted": true
}
}
}
PUT /api/usage/billing-config
Update auto-replenish and alert settings. Proxies the License Manager PUT platform/billing-config; the License Manager echoes the saved config.
Authentication: permission.billing.manage
Request Body:
| Field | Type | Description |
|---|---|---|
replenish.enabled | boolean | Enable pay-as-you-go auto-replenish |
replenish.paygDollars | integer | null | Dollars to spend each time auto-refill fires (20-10000) |
replenish.packSlug | string | null | Pack slug, kept nullable for back-compat |
replenish.cardId | string | null | Payment card token |
replenish.monthlyCap | integer | Max auto-refills per month (1-10) |
alerts.recipients | string[] | Email recipients for usage alerts |
alerts.thresholds | object | Toggle alerts at 80, 90, 95 percent, and exhausted |
Example Request:
{
"replenish": { "enabled": true, "paygDollars": 100, "cardId": "qb-card-1", "monthlyCap": 5 },
"alerts": { "recipients": ["admin@example.com"], "thresholds": { "80": true, "90": true, "95": true, "exhausted": true } }
}
Response:
Responds 200 with the saved billing configuration (same shape as the GET).
{
"replenish": {
"enabled": true,
"paygDollars": 100,
"cardId": "qb-card-1",
"monthlyCap": 5
},
"alerts": {
"recipients": [
"admin@example.com"
],
"thresholds": {
"80": true,
"90": true,
"95": true,
"exhausted": true
}
}
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage/billing-config"
}
},
"replenish": {
"enabled": true,
"paygDollars": 100,
"packSlug": null,
"cardId": "qb-card-1",
"monthlyCap": 5,
"monthlySpent": 0,
"monthlyRemaining": 5
},
"alerts": {
"recipients": [
"admin@example.com"
],
"thresholds": {
"80": true,
"90": true,
"95": true,
"exhausted": true
}
}
}
Invalid email recipients or an out-of-range monthlyCap answer 400.
Subscription lifecycle
- Purchase buy a credit pack; credits are valid for the annual term
- Active credits draw down from the pool as the platform is used (see Usage & Credits)
- Auto-replenish (optional) pay-as-you-go top-ups fire when the pool runs low, subject to the monthly cap
- Cancellation scheduled at period end via
pendingCancel; credits remain spendable until the term expires - Reactivation undo a pending cancellation before the term ends
Related APIs
- Usage & Credits usage profiles, entity budgets, and consumption reporting
- Teams assign usage profiles to teams