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:
| Plane | Governs | Set by |
|---|---|---|
| Account plane (you, the reseller) | Platform capacity — chiefly max_namespaces | Oblien / admin, via PUT /account/tier |
| Namespace plane (each end-customer) | One subscription, tier, quota ceiling, and suspend state per namespace | Payment (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 tier | max_namespaces |
|---|---|
free | 3 |
hobby | 10 |
pro | 50 |
scale | 100 |
enterprise | unlimited |
Two ways to use it
You already charge your customers yourself. Oblien is just the quota/enforcement engine.
PUT /billing/policy/:namespace— set the namespace'squotaLimit(plus optionaloverdraft,onOverdraftAction,suspendThreshold).- (optional)
PUT /billing/defaults— a template auto-applied to every new namespace. - Read live state with
GET /billing/entitlementandGET /billing/balance.
Oblien collects payment per namespace and entitles credits automatically.
GET /billing/catalog— show plans and packs.POST /billing/checkout{ namespace, kind:'subscription', planTierId }— redirect the customer to the returned Stripe URL.- On payment, Oblien auto-creates the namespace and sets its quota ceiling to the tier's credits-per-cycle.
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).
| Endpoint | Scope required |
|---|---|
GET /billing/catalog | Public — no auth |
| everything else | admin or billing scoped key |
PUT /billing/account/tier | admin 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/catalogcurl "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
| Parameter | Type | Required | Description |
|---|---|---|---|
namespace | string | Yes | Namespace to bind this purchase to. Created automatically on payment if it doesn't exist. |
kind | string | Yes | subscription (recurring plan) or topup (one-time credit pack). |
planTierId | string | subscription | Catalog tier id (e.g. pro). |
packId | string | topup | Catalog pack id (e.g. pack_1k). |
billingInterval | string | No | monthly (default) or yearly. |
successUrl / cancelUrl | string | Yes | Where Stripe returns the customer. |
idempotencyKey | string | No | Stable 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=acmecurl "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=acmecurl "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
| Parameter | Type | Description |
|---|---|---|
quotaLimit | number | null | Credit ceiling for the cycle. null = uncapped. |
overdraft | number | Credits allowed past the limit before hard-block. |
onOverdraftAction | string | stop_workspaces (stop running workspaces) or block (reject new requests). |
suspendThreshold | number | null | Credits 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"}'| Parameter | Type | Required | Description |
|---|---|---|---|
tier | string | Yes | One of free, hobby, pro, scale, enterprise. |
userId | string | No | Target 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):
| Status | Meaning |
|---|---|
canceled | Subscription cancelled, or the Stripe period has ended. |
past_due | Payment failed — subscription is past_due / unpaid. |
credit_exhausted | Namespace suspended for overdraft, or limit + overdraft − used ≤ 0. |
active | Everything 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→ statuscredit_exhausted,balance.blocking = true.- Usage past
quotaLimit + suspendThresholdwithonOverdraftAction: 'stop_workspaces'→ the namespace flips to suspended, running workspaces stop, andnamespace.suspendedfires. - A renewal (Mode B) or your next
PUT /policyresetsusedand 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).
| Event | Fired when |
|---|---|
payment.succeeded | A checkout payment cleared and credits were entitled. |
subscription.renewed | A billing cycle renewed; quota reset. |
subscription.tier_changed | Plan upgraded or downgraded. |
subscription.past_due | A renewal payment failed. |
subscription.canceled | Subscription cancelled. |
entitlement.changed | A namespace's ceiling or entitlement changed. |
namespace.suspended | A namespace crossed its suspend threshold. |
namespace.restored | A 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.