API Reference

Billing

The Billing API lets you sell subscriptions and credit packs to your own customers and have Oblien run the payment + entitlement engine for you. Everything is keyed on namespace, so you never touch Stripe directly — you point customers at a hosted checkout link, and Oblien provisions the namespace and sets its quota when payment clears.

TL;DR — Each of your namespaces is its own billing unit: its own subscription, tier, quota ceiling, and suspend state. Send a customer a POST /billing/checkout link; on payment Oblien auto-creates the namespace and entitles its credits. You (the reseller) set default and per-namespace policy; Oblien handles renewals, suspension, and webhooks. Whole credits everywhere.

The model

There is no separate "organization" object. Your authenticated principal is the owner, and an owner holds many namespaces. Billing runs on two independent planes:

PlaneGovernsSet by
Account plane (you, the reseller)Platform capacity — chiefly max_namespacesOblien / admin, via PUT /account/tier
Namespace plane (each end-customer)One subscription, tier, quota ceiling, and suspend state per namespacePayment (Mode B) or your policy (Mode A)

Every namespace is billed, renewed, and suspended independently — exhausting one never affects the others. One Stripe subscription maps to exactly one namespace.

max_namespaces per account tier

Account tiermax_namespaces
free3
hobby10
pro50
scale100
enterpriseunlimited

Two ways to use it

You already charge your customers yourself. Oblien is just the quota/enforcement engine.

  1. PUT /billing/policy/:namespace — set the namespace's quotaLimit (plus optional overdraft, onOverdraftAction, suspendThreshold).
  2. (optional) PUT /billing/defaults — a template auto-applied to every new namespace.
  3. Read live state with GET /billing/entitlement and GET /billing/balance.

Oblien collects payment per namespace and entitles credits automatically.

  1. GET /billing/catalog — show plans and packs.
  2. POST /billing/checkout { namespace, kind:'subscription', planTierId } — redirect the customer to the returned Stripe URL.
  3. On payment, Oblien auto-creates the namespace and sets its quota ceiling to the tier's credits-per-cycle.
  4. POST /billing/portal — the hosted "manage subscription" page.

Authentication & scopes

Base URL: https://api.oblien.com. All responses carry a success boolean; errors follow the standard { success:false, error, code, message } shape (see the Overview).

EndpointScope required
GET /billing/catalogPublic — no auth
everything elseadmin or billing scoped key
PUT /billing/account/tieradmin only

Authenticate with an API key (X-Client-ID + X-Client-Secret) or a scoped token (Authorization: Bearer <jwt>). See Scoped Tokens.


Catalog

Public list of sellable plans and credit packs, so your UI never hardcodes prices.

GET /billing/catalog
curl "https://api.oblien.com/billing/catalog"

Response

{
  "success": true,
  "plans": [
    {
      "tierId": "pro",
      "name": "Pro",
      "description": null,
      "priceMonthly": 49,
      "priceYearly": 490,
      "currency": "USD",
      "creditsPerCycle": 3000,
      "yearlyCreditsPerCycle": 36000,
      "overdraftCredits": null,
      "features": ["..."],
      "popular": true
    }
  ],
  "creditPacks": [
    { "packId": "pack_1k", "name": "1,000 credits", "credits": 1000, "price": 20, "currency": "USD", "popular": false }
  ]
}

Create a checkout

Start a hosted Stripe checkout for a namespace — either a recurring subscription or a one-time credit top-up.

POST /billing/checkout
Content-Type: application/json

{
  "namespace": "acme",
  "kind": "subscription",
  "planTierId": "pro",
  "billingInterval": "monthly",
  "successUrl": "https://app.example.com/ok",
  "cancelUrl": "https://app.example.com/cancel",
  "idempotencyKey": "optional-stable-key"
}
curl -X POST "https://api.oblien.com/billing/checkout" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"namespace":"acme","kind":"subscription","planTierId":"pro","successUrl":"https://app.example.com/ok","cancelUrl":"https://app.example.com/cancel"}'

Parameters

ParameterTypeRequiredDescription
namespacestringYesNamespace to bind this purchase to. Created automatically on payment if it doesn't exist.
kindstringYessubscription (recurring plan) or topup (one-time credit pack).
planTierIdstringsubscriptionCatalog tier id (e.g. pro).
packIdstringtopupCatalog pack id (e.g. pack_1k).
billingIntervalstringNomonthly (default) or yearly.
successUrl / cancelUrlstringYesWhere Stripe returns the customer.
idempotencyKeystringNoStable key to make a retried checkout safe.

Response

{ "success": true, "url": "https://checkout.stripe.com/...", "checkoutId": "cs_..." }

Redirect the browser to url. Errors: invalid_namespace, invalid_kind, invalid_plan, invalid_pack (all 400).

Provisioning happens on the webhook, not the redirect. Do not grant access when the customer lands on successUrl — wait for the payment.succeeded webhook or poll GET /billing/entitlement.

What each kind does to the namespace:

  • subscription → sets the namespace's quota ceiling to the tier's credits-per-cycle, renewed every billing cycle.
  • topup → adds credits to your account pool and extends that namespace's headroom for the current cycle (its used-counter is reduced by the pack size), so a top-up gives that one namespace more room immediately.

Billing portal

Open the hosted Stripe portal so a customer can manage their subscription and payment method.

POST /billing/portal
Content-Type: application/json

{ "returnUrl": "https://app.example.com/billing" }
curl -X POST "https://api.oblien.com/billing/portal" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"returnUrl":"https://app.example.com/billing"}'

Response

{ "success": true, "url": "https://billing.stripe.com/..." }

Returns 404 no_customer if the owner has never completed a checkout.


Get entitlement

Read a namespace's current plan, status, billing period, and quota.

GET /billing/entitlement?namespace=acme
curl "https://api.oblien.com/billing/entitlement?namespace=acme" \
  -H "Authorization: Bearer $TOKEN"

Response

{
  "success": true,
  "namespace": "acme",
  "tierId": "pro",
  "status": "active",
  "periodStart": "2026-07-01T00:00:00Z",
  "periodEnd": "2026-08-01T00:00:00Z",
  "quota": { "limit": 3000, "used": 420, "balance": 2580 }
}

status is one of the frozen entitlement statuses. balance = limit + overdraft − used (null = uncapped).


Get balance

A lightweight "can this namespace keep spending?" check.

GET /billing/balance?namespace=acme
curl "https://api.oblien.com/billing/balance?namespace=acme" \
  -H "Authorization: Bearer $TOKEN"

Response

{ "success": true, "namespace": "acme", "blocking": false, "balance": 2580 }

blocking is true when the namespace is suspended or its balance is ≤ 0 — use it as a fast gate before doing metered work.


Namespace policy

Read or set a single namespace's allowance and suspend policy. This is the Mode-A lever (set the ceiling yourself) and also lets you tune a Mode-B namespace's overdraft/suspend behavior.

GET /billing/policy/:namespace
{
  "success": true,
  "namespace": "acme",
  "service": "workspace_vm",
  "quotaLimit": 3000,
  "overdraft": 0,
  "onOverdraftAction": "stop_workspaces",
  "suspendThreshold": null
}
PUT /billing/policy/:namespace
Content-Type: application/json

{
  "quotaLimit": 5000,
  "overdraft": 500,
  "onOverdraftAction": "stop_workspaces",
  "suspendThreshold": 1000
}

Parameters

ParameterTypeDescription
quotaLimitnumber | nullCredit ceiling for the cycle. null = uncapped.
overdraftnumberCredits allowed past the limit before hard-block.
onOverdraftActionstringstop_workspaces (stop running workspaces) or block (reject new requests).
suspendThresholdnumber | nullCredits past the limit before the whole namespace is suspended. null = use your account default.

Account defaults

A template policy auto-applied to every new namespace, so each customer starts with the same limits.

GET /billing/defaults
{
  "success": true,
  "service": "workspace_vm",
  "quotaLimit": 1000,
  "autoApply": true,
  "overdraft": 0,
  "onOverdraftAction": "stop_workspaces",
  "suspendThreshold": null
}
PUT /billing/defaults
Content-Type: application/json

{
  "quotaLimit": 1000,
  "autoApply": true,
  "overdraft": 0,
  "onOverdraftAction": "stop_workspaces",
  "suspendThreshold": 500
}

Same fields as namespace policy, plus autoApply (default true) — when true, the template is applied to each new namespace at creation.


Account tier

Set an owner's account tier, which governs max_namespaces. This is a privilege grant, so it requires an admin scoped key — not a reseller's own billing key.

PUT /billing/account/tier
Content-Type: application/json

{ "userId": "1234", "tier": "pro" }
curl -X PUT "https://api.oblien.com/billing/account/tier" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"userId":"1234","tier":"pro"}'
ParameterTypeRequiredDescription
tierstringYesOne of free, hobby, pro, scale, enterprise.
userIdstringNoTarget owner. Defaults to the caller.

Response

{ "success": true, "userId": "1234", "tier": "pro" }

Returns 400 invalid_tier for an unknown tier.


Entitlement status

GET /billing/entitlement returns exactly one of these, in precedence order (first match wins):

StatusMeaning
canceledSubscription cancelled, or the Stripe period has ended.
past_duePayment failed — subscription is past_due / unpaid.
credit_exhaustedNamespace suspended for overdraft, or limit + overdraft − used ≤ 0.
activeEverything is current and within quota.

Quota & suspend model

For a namespace's workspace_vm quota:

used             usage debited this cycle
quotaLimit       ceiling (tier credits-per-cycle in Mode B, or your PUT in Mode A)
overdraft        credits allowed past the limit before hard-block
suspendThreshold credits past the limit before the namespace is SUSPENDED
balance          quotaLimit + overdraft − used   (null = uncapped)
  • balance ≤ 0 → status credit_exhausted, balance.blocking = true.
  • Usage past quotaLimit + suspendThreshold with onOverdraftAction: 'stop_workspaces' → the namespace flips to suspended, running workspaces stop, and namespace.suspended fires.
  • A renewal (Mode B) or your next PUT /policy resets used and re-arms the namespace.

See Namespaces for how quotas relate to resource caps and lifecycle.


Webhooks

Billing lifecycle events are delivered to your configured webhook endpoints. Every payload includes the namespace it concerns, a stable X-Webhook-Id header, and an HMAC-SHA256 X-Webhook-Signature over the raw body (verify it before trusting the event).

EventFired when
payment.succeededA checkout payment cleared and credits were entitled.
subscription.renewedA billing cycle renewed; quota reset.
subscription.tier_changedPlan upgraded or downgraded.
subscription.past_dueA renewal payment failed.
subscription.canceledSubscription cancelled.
entitlement.changedA namespace's ceiling or entitlement changed.
namespace.suspendedA namespace crossed its suspend threshold.
namespace.restoredA suspended namespace was reactivated.

Webhook delivery is best-effort with idempotent provisioning — if you miss an event, reconcile by calling GET /billing/entitlement. Redelivery of the same event (same X-Webhook-Id) never double-grants credits.