API Reference

Billing

Account Hobby/Pro/Scale subscriptions remain wallet-credit plans with resource limits (GET /credits/plans). Their credits can fund PAYG or purchase a monthly pool; resource limits alone do not prepay compute. For prepaid monthly resource pools and capped PAYG, see PAYG and monthly compute. It documents /billing/capacity/*, the shared quote/settlement flow, wallet conversion, paid-through coverage, included transfer, spending controls, events and savings. The existing checkout/portal/plan-change APIs below also serve reseller monthly offers with offer.billingMode: "monthly", offer.capacity and offer.credits: 0. This buys namespace capacity without subscribing the owner account to the customer's product. New purchases share the owner tier’s budget across namespaces. allocationBasis: "running" releases confirmed stopped CPU/RAM while retaining owned disks/counts; already-sold allocated contracts preserve their original terms.

Managed internet transfer uses generic provider-controlled defaults. Monthly namespaces receive inclusion automatically; reseller writes to /billing/capacity/network/defaults or /billing/capacity/network/policy are optional spending authorizations, not required provisioning steps. Those writes cannot change included bytes, unlimited eligibility or grace. Read capacity.network.unlimited, status, usage and policy revision. Unlimited paid coverage returns includedBytes: null and active available bytes as null; inactive coverage returns zero available bytes. Read the transfer contract for policy changes, paid-period preservation and signed events.

The reseller Billing API lets you sell subscriptions and credit packs to your own customers and have Oblien run the payment + entitlement engine for you. Each reseller customer 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. Metered credits and refund adjustments can be fractional.

For a complete integration, read Build a SaaS or VM reseller. It covers secure customer identity, resource caps, hosted payment, signed callbacks and VM provisioning.

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; namespace count uses your paid account tier or a higher admin grantAccount subscription; admin grants via PUT /account/tier
Namespace plane (each end-customer)Saved compute contract, subscription, resource pool, applicable credit allowance and suspend stateVerified payment or account credit purchase; policy for the existing allowance product

Every namespace is billed, renewed, and suspended independently. Namespace allowances are isolated; the owner wallet and account resource pool are shared. One Stripe subscription maps to exactly one namespace.

New namespace checkouts require an Enterprise owner account, either through its active account subscription or a platform-admin grant. This applies to custom offers, catalog subscriptions and top-ups, in both test and live mode. A customer's namespace subscription cannot grant this eligibility. An ineligible owner receives 403 reseller_enterprise_required before a payment session is created. Existing subscription reads, portals, cancellation, resume, renewals and refund processing remain available after an owner downgrade. The public catalog advertises reseller.requiredAccountTier: 'enterprise'; the API checks the authenticated owner's eligibility on each new checkout.

max_namespaces per account tier

Account tiermax_namespaces
free3
hobby10
pro50
scale100
enterpriseunlimited

Namespace-count capacity uses the higher of the owner's active account subscription and its admin-granted tier. A legacy account_tier: free default does not downgrade a paid Enterprise owner. Customer namespace subscriptions never grant account capacity.

Both POST /namespaces and POST /namespaces/ensure enforce that count inside the namespace insert transaction while holding the owner's row lock. Concurrent requests cannot reserve the same remaining slot for different namespaces. Ensuring a namespace that already exists still succeeds when the account is full.

Oblien resolves and enforces resource capacity. Declare the customer's namespace limits directly; use null for a VM dimension that should inherit capacity. Namespace detail, create, ensure and update responses include data.resource_limits (declared policy) and data.effective_resource_limits (the caps Oblien enforces). A policy above today's machine ceiling is accepted; actual allocation remains bounded by account, platform and saved customer caps. Enterprise removes account pool/count caps while each VM keeps a platform ceiling. Clients do not need to fetch account quota or calculate an intersection before saving a policy. These ceilings do not reserve host capacity or wallet funds.

Price and allowance units

Direct wallet deposits use 100 credits/USD with a $5 minimum and no automatic bulk bonus. New wallet package prices use that same funding rate. Reseller offer.unitAmount supports $1–$10,000; monthly capacity also accepts zero when fully funded by the owner's credit balance. A retail minimum change does not invalidate a saved customer offer or renewal. The offer's credits controls the namespace allowance independently of owner wallet funding. Treat saved offers and paid receipts as authoritative after catalog changes.

Use /billing/capacity/catalog for current PAYG and monthly prices. A verified monthly purchase prepays its resource pool; a credit grant or hardware-policy edit alone does not. Existing credit contracts retain their legacy UTC discounts and allowance rules. The earlier /pricing/compute-rate-cards proposal remains preview-only and is separate from the active capacity tariff. See Billing & Credits.

Two ways to use it

Mode A and Mode B describe payment collection for the existing allowance product. Compute billing uses the separate payg / monthly contract. For wallet-funded capacity, use capacity preview and confirmation. For hosted monthly capacity, use a reseller offer with billingMode: 'monthly', capacity and credits: 0; successful payment establishes coverage instead of a finite allowance.

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.
  4. Reset usage for your next paid cycle with POST /billing/policy/:namespace/reset.

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 checkout 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 { namespace } — this customer's invoices, payment methods, and subscription management.
  5. Read, cancel, or resume renewal with the subscription endpoints. Paid renewals reset quota automatically.

Checkout pages and promotion codes

Stripe-hosted checkout is the default for namespace POST /billing/checkout, account POST /credits/purchase, and account POST /credits/subscribe. Customers go directly to Stripe, with its promotion-code field enabled. The same two optional fields control all three endpoints:

Request optionsCustomer experienceCode issuer and eligibility
Omit both fields, or checkoutMode: "stripe", allowPromotionCodes: trueDirect Stripe Checkout with code entryNative Stripe codes, including account-scoped codes issued in Oblien Admin
checkoutMode: "stripe", allowPromotionCodes: falseDirect Stripe Checkout without code entryCodes disabled for this checkout
checkoutMode: "oblien", allowPromotionCodes: trueOblien's custom payment page, then StripeOblien-admin codes with owner/namespace scope and capped discounts
checkoutMode: "oblien", allowPromotionCodes: falseOblien's custom payment page without code entry, then StripeCodes disabled for this checkout

checkoutMode accepts only stripe or oblien; allowPromotionCodes is a strict boolean and defaults to true. A reseller controls these options on its server. The request cannot supply a coupon ID, discount amount, or override eligibility. Existing checkout links remain valid. Use a new idempotency key when changing checkout options.

{
  "namespace": "acme",
  "kind": "subscription",
  "planTierId": "pro",
  "checkoutMode": "stripe",
  "allowPromotionCodes": true
}

Stripe promotion codes

At Admin → Promotions, select Stripe hosted, a percentage or fixed USD discount, expiry, total redemptions, and eligible Oblien account IDs. Choose Namespace checkout only for a reseller's customers. For example, account 80 with namespace scope covers its namespaces, including new ones; the owner's own checkout and other accounts cannot use that code. A code can cover several selected owners or all accounts. Resellers choose whether to show code entry; only platform administrators issue campaigns.

Oblien binds a separate native Stripe promotion code to each eligible billing customer before opening a new checkout. Namespace customers and portals stay separate. All copies use the same visible code and share one coupon's redemption limit. Redemptions per customer counts one namespace or the account itself; it is not a combined per-reseller limit. Use a new checkout to redeem a newly issued code. Stripe enforces customer, expiry, total-use, per-customer-use and minimum-purchase restrictions; metadata alone grants no access.

Admin-issued codes are first payment only (duration: once). A monthly subscription receives the discount on its first month and renews at the saved normal price. An annual subscription receives the discount on its first annual payment. Codes also apply to one-time purchases within the selected scope. Set the per-customer limit to 1 for a welcome offer. Refunds do not restore uses. Disabling deletes the shared coupon in Stripe, preventing new applications; already applied discounts and paid invoices retain their terms.

Stripe percentage discounts have no dollar cap. Choose the custom page for a capped percentage, a combined per-owner limit, or a subscription-only/top-up-only rule. These unsupported combinations are rejected for Admin Stripe codes. Platform operators can also create codes directly in Stripe Dashboard; those follow Stripe's configured restrictions and duration. Live and test campaigns are separate; Admin creates native codes only in the currently active billing environment.

Custom page and scoped promotions

Choose checkoutMode: "oblien" to keep Oblien's custom page and its features. Only Oblien administrators issue these codes, at Admin → Promotions. They can choose fixed USD discounts or percentages with a USD cap, expiry, total uses, optional limits per owner/namespace, eligible Oblien owner IDs, and account/namespace scope. Owner 80 in namespace scope means namespaces belonging to owner 80; a matching email or slug under another owner does not qualify. Resellers control code entry, not campaign rules.

Custom-page discounts apply to the first payment only. Preview consumes no use. Confirmation reserves a use atomically for a 45-minute payment session. Expiry or disabling prevents new reservations; already reserved payments remain valid until their session expires. Unstarted custom links expire after 24 hours. Stripe-hosted payment sessions also expire after 45 minutes; Stripe manages native code eligibility and redemption.

Payment, credits and redirects

Both modes preserve full purchased credits and declared namespace rules. For a $10 offer granting 100 namespace credits, a 50% code collects $5, funds the full 1,000 wallet credits at the current standard rate, and grants 100 namespace credits. Oblien funds and records the $5 difference. With a once coupon, renewal charges $10 with the normal credits and quotas. The owner's account plan is unchanged. Refunding the actual $5 reverses the full corresponding wallet/namespace grant; partial refunds reverse the same proportion and cannot reduce a newer subscription cycle's allowance.

Payment receipt, wallet credit, namespace policy and the billing-event outbox commit together, with idempotent recovery around external Stripe calls. Custom reservations and the verified redemption record for Admin-issued native codes participate in that financial transaction. Stripe enforces native usage limits; Admin usage shows verified payments and their funded discounts. A zero total still requires a verified completed provider checkout. A browser redirect is not proof of payment: verify signed events and read checkout status/entitlement.

Redirect to the returned URL unchanged. It is normally https://checkout.stripe.com/...; the explicit custom mode returns https://api.oblien.com/billing/pay#<capability>. Treat checkoutId as opaque: both bco_ and historical cs_ IDs are supported, including bco_ IDs for direct Stripe checkout. Save the ID with the customer/order and use it with the namespace checkout-status endpoint. The success-URL {CHECKOUT_SESSION_ID} placeholder resolves to that same public ID. Keep payment URLs and custom capabilities out of logs and analytics.

Custom-page errors include a request reference and codes such as promotion_unavailable, promotion_expired, promotion_limit_reached, promotions_disabled, checkout_already_started, and checkout_expired. Stripe shows native code errors in its own checkout. Invalid options return invalid_checkout_mode or invalid_promotion_option. See the SaaS integration guide for customer ownership and webhook handling.

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
authenticated billing endpointsadmin or billing scoped key
/billing/payPublic payment page; an opaque checkout capability authorizes only its saved payment
PUT /billing/account/tieradmin plus the platform operator's X-API-Key

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

Keep the admin/billing key in your backend. Authenticate your customer and resolve their namespace from your customer record before calling Oblien. A privileged key can manage all of its owner's namespaces, so never trust a browser-supplied namespace without checking ownership. Namespace- and workspace-scoped tokens receive 403 on these authenticated billing routes. Use stable namespace identities and never reassign a customer's old slug to another customer, since billing history remains attached to it.


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).
planTierIdstringcatalog subscriptionCatalog tier id (e.g. pro).
packIdstringcatalog topupCatalog pack id (e.g. pack_1k).
billingIntervalstringNomonthly (default) or yearly.
successUrl / cancelUrlstringNoReturn URLs. Hosts must be allowlisted; an unapproved host returns billing_redirect_not_allowed.
idempotencyKeystringWith offerStable key to make a retried checkout safe.
customerobjectNo{ email?, name? } for the namespace's first checkout; never defaults to the API-key owner's details.
checkoutModestringNostripe (default) opens Stripe directly; oblien selects the custom page with scoped, capped Oblien-admin promotions.
allowPromotionCodesbooleanNoDefaults to true. Set false to hide and disable code entry in either mode.

Response

{ "success": true, "url": "https://checkout.stripe.com/c/pay/...", "checkoutId": "bco_..." }

Stripe is the default destination; checkoutMode: "oblien" returns the optional custom page. Redirect the browser to the exact returned url. Treat checkoutId as opaque: either mode may return bco_..., and historical or promotion-disabled checkouts may return cs_.... Do not construct payment URLs from the ID. Errors: invalid_namespace, invalid_kind, invalid_plan, invalid_pack (all 400).

Subscription and top-up checkouts reuse a Stripe customer dedicated to this owner and namespace. Reuse an idempotency key only for an identical request. Configure your app's return host in REDIRECT_ALLOWED_HOSTS before launching the integration.

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 with a monthly capacity offer → reserves the shared pool and establishes its paid period after verified payment, without a compute-credit ceiling.
  • subscription with a catalog plan or metered offer → sets the namespace's quota ceiling to its credits-per-cycle, renewed every billing cycle.
  • topup → adds credits to your account ledger and that namespace's purchasedCredits. It increases the ceiling, preserves usage, and carries only unused purchased credits into renewals.

Billing portal

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

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

{ "namespace": "acme", "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 '{"namespace":"acme","returnUrl":"https://app.example.com/billing"}'

Response

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

namespace is required when opening a reseller customer's portal. Omit it, or send null, for the authenticated owner's personal billing customer; the response has namespace: null. A named request never falls back to personal billing. The portal shows that customer's invoices, payment methods, contact details, and subscription cancellation at the end of the paid period. It returns 404 no_customer if this namespace has no billing customer, or 409 billing_customer_conflict for a legacy Stripe customer shared with other namespaces or the owner account. Shared billing must be separated by the operator.

The API-key owner's dashboard continues to use its separate /credits/manage route. Existing reseller subscriptions use the plan-change APIs for prorated upgrades and scheduled downgrades. Catalog subscriptions still use a replacement checkout with a new full-price cycle. Tier changes inside the portal remain disabled.

Reseller subscription plan changes

These endpoints change an existing reseller offer or a direct personal monthly capacity subscription, without creating a second subscription or changing the owner's account-tier plan. For direct personal capacity, omit namespace (or send null) and use backend-priced capacity terms; reseller eligibility is not required for that saved direct purchase. The namespace inputs in the reseller examples below are mandatory for customer isolation. For reseller offers, new quotes and acceptance require an Enterprise owner and a server API key with billing or admin scope. Read and cancel operations remain available if reseller eligibility later ends. End-user namespace/workspace tokens cannot call billing APIs. Your backend must derive the namespace and offer from its authenticated customer's organization and trusted catalog.

Method and route (under /billing)InputResult
POST /subscription/changes/previewnamespace, offer, idempotencyKey; optional metadata, billingInterval{ success, namespace, quote }
POST /subscription/changesnamespace, quoteId, idempotencyKey{ success, namespace, change }
GET /subscription/changes/:changeIdnamespace query parameterThe saved change
POST /subscription/changes/:changeId/cancelnamespace, idempotencyKeyThe canceled change, or its pending cancellation
const { quote } = await client.billing.previewPlanChange(namespace, {
  offer: {
    reference: 'pro-v2', name: 'Pro', unitAmount: 4000, currency: 'usd',
    credits: 3500, description: 'Your application hosting plan',
    policy: { overdraft: 0, suspendThreshold: 0, onOverdraftAction: 'stop_workspaces' },
    resourceLimits: { max_workspaces: 4, max_total_vcpus: 4, max_total_ram_mb: 8192 },
  },
  metadata: { plan: 'pro-v2' },
  idempotencyKey: `preview-${requestId}`,
});
// Show quote.current / quote.next (saved offers), unusedTimeCredit,
// remainingTimeCharge, amountDueNow, effectiveAt and expiresAt to the customer.
const { change } = await client.billing.changePlan(namespace, {
  quoteId: quote.id, idempotencyKey: `confirm-${requestId}`,
});
// Grant access only after change.status === 'applied' or a signed applied event.
// payment_pending: show change.payment.url so the customer can complete payment.
// queued/dispatching/canceling: poll this SAME change, never start a second charge.
await client.billing.planChange(namespace, change.id);
// If the customer asks to cancel a pending, cancelable change:
// await client.billing.cancelPlanChange(namespace, change.id, { idempotencyKey: `cancel-${requestId}` });

Quotes expire after at most ten minutes, and before the current cycle ends. The provider price, billing period, discounts, amount due and saved source contract are rechecked before charging. A changed/expired quote requires a new preview key; an accepted operation keeps its original confirmation key across every retry. IDs and keys are bound to the authenticated owner, namespace and Stripe environment. Only one accepted change can be pending per namespace. Unknown provider outcomes remain pending for reconciliation, preventing another payment from being started.

All monetary values are USD cents; credits are a separate allowance. Preview returns current, next, unusedTimeCredit, remainingTimeCharge, amountDueNow, nextInvoiceAmount, includedCreditIncrease, billingInterval, effectiveAt and expiresAt. nextInvoiceAmount is an estimate for a downgrade; discounts, taxes and customer balance can change before its renewal. Existing subscriptions use their saved price and billing contract, irrespective of later catalog edits.

For monthly capacity subscriptions, the same operations change the paid resource pool. A resource reduction or billing-mode change waits for renewal even if its retail price increases. Pure funded upgrades can apply immediately after payment; your available account credit balance covers any provider-cost increase that exceeds the retail increase. The contribution is checked before payment and again at settlement. A monthly offer uses credits: 0, so it grants capacity rather than an included-credit increment. The credit-specific arithmetic below applies to metered offers. Paid coverage, retained-storage accounting and provider-fee refunds follow the capacity contract.

  • Higher price: Stripe invoices the prorated difference. A declined payment or required authentication keeps the old plan and limits. Once payment and Stripe's price update are confirmed, the new offer/limits and prorated credit increase commit together. The increase is max(0, newCredits - oldCredits) × remainingSeconds / cycleSeconds, rounded down to six decimals. Usage and purchased credits remain unchanged. The partial payment funds wallet credits at the saved standard rate.
  • Lower or equal price: new terms begin at the next paid renewal. Until then, GET /subscription exposes subscription.pendingChange. Cancel it before effectiveAt; releasing the schedule keeps the underlying subscription active. At paid renewal the new full allowance is installed, usage resets, and unspent top-ups carry forward. Already-consumed top-ups do not reappear.
  • Refunds: a verified proration refund reverses its wallet funding and only its included-credit increment in that same cycle. It cannot consume purchased top-ups or reduce a later cycle. A full-cycle refund retains the existing cycle-scoped refund rules.

Changes currently support active, automatically charged, single-item monthly or yearly USD subscriptions. The interval is preserved. Resolve unpaid current invoices, pending subscription cancellations or externally managed schedules first. An existing plan change must be canceled before canceling/resuming the subscription itself. Quotes cannot include unrelated pending invoice items. Downgrades enforce new admission limits; they do not resize or delete existing workspaces. Independent namespace restrictions, account capacity and the platform's per-VM ceiling still apply.

Change statuses are queued, dispatching, payment_pending, scheduled, canceling, reconciliation_required, applied, canceled, expired, and failed. HTTP 202 means the operation is durably accepted but still being confirmed; HTTP 200 with payment_pending or scheduled also does not grant the new plan. The response includes safe error.code/message, optional payment.url/expiresAt, cancelable, and appliedAt. Stable errors include billing_quote_expired, billing_quote_changed, idempotency_conflict, billing_plan_change_pending, billing_change_busy, billing_change_not_cancelable, and plan_change_not_found.

Subscribe to subscription.change.scheduled, .applied, .canceled, .payment_required, .failed, and .expired. They use the existing HMAC-signed, namespace-bound webhook delivery and carry { orgRef, namespace, change } in data. Deduplicate by event id and re-read the subscription after delayed events. Financial writes and these event-outbox records share one database transaction; Stripe calls and webhook delivery are retried separately. Existing subscription.tier_changed, subscription.renewed and entitlement.changed events continue to be emitted when current entitlements actually change.

Manage a subscription

These routes also manage direct personal compute subscriptions when namespace is omitted or null. They do not select the legacy account-tier subscription; that remains under /credits. For a reseller customer, always send the exact namespace resolved by your backend.

Read the subscription and any pending cancellation:

GET /billing/subscription?namespace=acme
{
  "success": true,
  "namespace": "acme",
  "subscription": {
    "tierId": "pro",
    "status": "active",
    "billingInterval": "monthly",
    "periodStart": "2026-09-01T00:00:00Z",
    "periodEnd": "2026-10-01T00:00:00Z",
    "cancelAtPeriodEnd": false,
    "canceledAt": null
  }
}

subscription is null if this namespace has never subscribed. Subscription status can be active, trialing, past_due, unpaid, paused, or canceled.

ActionEndpointBody
Stop renewal at period endPOST /billing/subscription/cancel{ "namespace": "acme" }
Undo a scheduled cancellationPOST /billing/subscription/resume{ "namespace": "acme" }

Both return the subscription response above and are safe to repeat. Cancellation keeps paid access until periodEnd; resume clears cancelAtPeriodEnd before the subscription ends. An ended subscription needs a new checkout (409 subscription_ended). A missing subscription returns 404 no_subscription; mismatched stored/Stripe identity returns 409 billing_identity_conflict. Callers cannot select a Stripe customer or subscription by supplying its ID.

TypeScript SDK

The [email protected] release adds the billing module. Use it on your server:

const namespace = authenticatedCustomer.namespace;
const { url } = await client.billing.portal({ namespace });
const { subscription } = await client.billing.subscription(namespace);
await client.billing.cancelSubscription(namespace);
await client.billing.resumeSubscription(namespace);

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, "overdraft": 0, "suspendThreshold": 0 }
}

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

The example above is a metered allowance. A capacity contract returns tierId: 'capacity', billingMode, computeCovered and the current capacity view. Its compatibility quota has limit: null, balance: null, used: 0 and alert: null; those values do not represent unlimited physical resources. Read the returned capacity pool and paid period. Manual suspension still denies access.

For a subscription converted to monthly capacity during its paid cycle, periodStart and periodEnd match the preserved subscription dates. coverageStartsAt separately records when monthly coverage began. Conversion does not reset renewal or cancellation dates. Normal purchases and renewals have the same billing and coverage start. Use computeCovered and status for current access; the earlier billing start does not grant retroactive coverage.


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 }

Use the returned blocking field as the billing gate; do not recompute it from a numeric balance. Under a legacy allowance it checks suspension and quota exhaustion. Under a capacity contract it checks the active period, manual suspension and funding required by that mode. Confirmed paid compute is not blocked by an exhausted legacy quota or owner wallet.

A paid monthly result can be { "success": true, "namespace": "acme", "billingMode": "monthly", "blocking": false, "balance": null, "computeCovered": true, "paidThrough": "2026-11-04T00:00:00Z", "alert": null }. Transfer and retained-storage balances are separate in GET /billing/capacity. Resource admission still checks the requested allocation. For a subscription-only product, also read its subscription and entitlement state.


Namespace policy

Read or set a single namespace's allowance and suspend policy. This sets the current policy. For a recurring reseller offer, put lasting grace changes in the offer: each paid renewal reapplies its saved policy.

GET /billing/policy/:namespace
{
  "success": true,
  "namespace": "acme",
  "service": "workspace_vm",
  "quotaLimit": 3000,
  "purchasedCredits": 500,
  "effectiveCeiling": 3500,
  "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.

purchasedCredits and effectiveCeiling are read-only. PUT /policy preserves usage and purchased credits; a successful write returns the normalized policy. overdraft must not exceed a non-null suspendThreshold.

Reset a quota cycle

For Mode A, call this after your own next billing cycle clears:

POST /billing/policy/acme/reset
Content-Type: application/json

{ "periodEnd": "2026-10-01T00:00:00Z" }
{ "success": true, "namespace": "acme", "applied": true }

This resets usage and threshold markers while keeping the policy and purchased credits. Repeat the same ISO timestamp on retry: the same or an older cycle returns applied: false and preserves subsequent usage. Errors are 400 invalid_period, 404 no_policy, or 409 subscription_managed for an active hosted subscription. Mode-B subscriptions reset automatically on paid renewal invoices.

await client.billing.resetQuota(namespace, { periodEnd: '2026-10-01T00:00:00Z' });

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 platform privilege grant: it requires owner authentication with admin scope and the platform operator's X-API-Key (ADMIN_API_KEY). An owner's admin API key alone cannot grant capacity; it receives 403 platform_admin_required.

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 "X-API-Key: $PLATFORM_ADMIN_API_KEY" \
  -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 preserves these four status names across both contracts. Use the returned state with balance.blocking and current coverage; clients should not reproduce the backend's status mapping.

StatusMeaning
canceledThe subscription or capacity period has ended.
past_duePayment failed — subscription is past_due / unpaid.
credit_exhaustedNamespace suspension or missing funding required by the applicable contract. A paid monthly period has no finite compute-credit allowance.
activeThe applicable funding or paid coverage currently permits compute. Resource admission and manual restrictions still apply.

Quota & suspend model

For an existing metered allowance on a namespace's workspace_vm quota:

used             usage debited this cycle
quotaLimit       included allowance (tier credits-per-cycle or your PUT in Mode A)
purchasedCredits accumulated paid top-ups; preserved at cycle reset
ceiling          quotaLimit + purchasedCredits
overdraft        credits allowed past the limit before hard-block
suspendThreshold credits past the limit before the namespace is SUSPENDED
balance          ceiling + overdraft − used   (null = uncapped)
  • balance ≤ 0 → status credit_exhausted, balance.blocking = true.
  • Usage past ceiling + suspendThreshold with onOverdraftAction: 'stop_workspaces' → the namespace flips to suspended, running workspaces stop, and namespace.suspended fires.
  • A paid renewal (Mode B) or POST /policy/:namespace/reset (Mode A) resets used once per cycle and preserves purchased credits. Setting policy alone does not reset usage.
  • A top-up, renewal, reset, or policy increase restores a billing-suspended namespace once its balance is positive or uncapped. Manual suspensions stay in place. Workspaces can then be started explicitly.
  • Suspensions created before billing reasons were recorded need an explicit operator restore.

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 its saved credit or capacity contract was fulfilled.
subscription.renewedA billing cycle renewed under its saved credit or capacity terms.
subscription.tier_changedPlan upgraded or downgraded.
subscription.past_dueA renewal payment failed.
subscription.canceledSubscription cancelled.
subscription.updatedSubscription state changed, including scheduled cancellation/resume.
entitlement.changedA namespace's ceiling or entitlement changed.
namespace.suspendedA namespace crossed its suspend threshold.
namespace.restoredA suspended namespace was reactivated.

Capacity contracts also emit capacity.changed, capacity.renewed, capacity.expired, capacity.payment_required and capacity.revoked. Transfer purchases and warnings use network.topup_applied, network.allowance.low, network.allowance.depleted, network.allowance.grace, network.allowance.blocked and network.allowance.restored. network.policy.updated reports owner spending settings. Depletion begins a recovery warning; it does not prove internet has stopped. Re-read capacity.network for included bytes, grace and actual access state. Retained-storage balances use storage.retention.payment_required and storage.retention.paid. These use the same signed delivery and deduplication contract. See capacity events.

Reseller payment, subscription and namespace credit-alert notifications use durable delivery records and retry after failures. Deduplicate X-Webhook-Id before acknowledging, and re-read GET /billing/entitlement to handle reordering. General VM and namespace operational events retain best-effort delivery.

Namespace credit alerts and customer recovery

This section applies to metered credit allowances. For monthly capacity, display coverage/renewal and the separate transfer or retained-storage warning; never derive a compute-credit exhaustion banner from an old quota.

Oblien computes the warning state from the same quota row that enforces spending. Read quota.alert from GET /billing/entitlement?namespace=..., or alert from GET /billing/balance?namespace=.... Namespace quota rows also include alert. Its state is ok, low, grace, depleted, unlimited, or disabled. percent uses included allowance + purchased top-ups, excluding grace. remaining is paid allowance left; balance also includes configured grace. All amounts are JSON numbers in Oblien credits. Never add grace to balance again.

Subscribe to namespace.quota.threshold, credits.low, and credits.depleted:

  • Warning percentages default to 80% and 95% and can be configured per service. notificationThresholds: [] disables percentage warnings, not enforcement or exhaustion alerts. A large usage window reports the most urgent crossed state.
  • credits.low reports entry into configured grace. Zero grace remains the default. credits.depleted reports exhaustion at balance <= 0.
  • The usage debit, threshold marker and outgoing alert commit in one payments transaction. Delivery retries after failures and API restarts, retaining its signed body and event ID. Metering does not wait for your webhook or email server.
  • Each event identifies data.namespace, data.service, data.transaction_id and a numeric data.alert snapshot. Resolve the customer from your saved namespace mapping, verify the signature, and durably save the event and notification job before acknowledging. Deduplicate event IDs, then re-read current entitlement; an old alert may arrive after a top-up or renewal. Do not use event amounts to grant credits or override the current quota.
  • Show a warning banner and a billing action. Use your server-defined top-up offer to create a namespace-bound checkout after the customer chooses to buy. Payment fulfillment increases purchased allowance once; your next entitlement read clears the warning when funded. Workspaces stopped for billing can then be restarted. A top-up does not change the plan's resource caps or grace policy.
  • Paid renewals re-arm warnings and preserve custom notification percentages. A top-up or usage refund that lowers usage below a warning band re-arms that band for the larger remaining budget. Mode A cycles must still be reset by your backend; Mode B cycles renew after verified paid invoices.

External email delivery is asynchronous and can repeat after a lost acknowledgment. Use a durable notification queue, honor verified recipient preferences, recheck organization membership before delivery, and bind billing links to the intended organization. Keep a read-only dashboard warning while email is unavailable.

Reseller offers: price, allowance and product text

POST /billing/checkout also accepts a server-defined offer instead of a catalog planTierId or packId. Keep this request behind your authenticated backend and resolve the customer's namespace from your own database.

Choose the offer type explicitly. The example below is a metered credit offer. A monthly capacity offer instead sets billingMode: 'monthly', capacity: { vcpus, memoryMb, diskGb, workspaces } and credits: 0, and omits policy. It requires kind: 'subscription' and billingInterval: 'monthly'. Your available account credit balance funds any gap between your retail price and the backend's saved provider price; physical admission is reserved before checkout. See the monthly reseller example.

You can sponsor the whole monthly period with unitAmount: 0, or charge any supported retail price below cost and cover the difference. For a $5 offer with an $11.25 provider fee, the owner's contribution is $6.25 (625 credits) per month. A zero-price sponsored subscription needs no payment method and has no promotion-code entry.

Checkout and immediate upgrades return 402 insufficient_redeemable_balance if the account credit balance cannot cover the contribution. The response includes requiredCredits, eligibleCredits and walletCredits. This check does not hold wallet funds. Settlement checks again under the wallet lock, and an unfunded payment stays pending fulfillment until funding and retry succeed. All committed account credits, including admin grants, can fund monthly capacity. Namespace allowances and uncredited payment receipts cannot; provider payments still require verification in the correct test/live environment. Keep funds available for renewals; check fulfilled and computeCovered before granting access. walletCreditDelta can be negative when your wallet contributes; walletCreditsGranted stays at zero in that case.

{
  "namespace": "tenant-a",
  "kind": "subscription",
  "billingInterval": "monthly",
  "offer": {
    "reference": "compute-starter-v1",
    "name": "Compute Starter",
    "description": "Your hosted application compute plan",
    "unitAmount": 1000,
    "currency": "usd",
    "credits": 100,
    "policy": { "overdraft": 0, "suspendThreshold": 0, "onOverdraftAction": "stop_workspaces" },
    "resourceLimits": { "max_workspaces": 2, "max_vcpus": 2, "max_ram_mb": 4096, "max_disk_gb": 32 }
  },
  "metadata": { "appOrderId": "order-123", "planVersion": "1" },
  "idempotencyKey": "order-123",
  "customer": { "email": "[email protected]" },
  "successUrl": "https://app.example.com/billing/return",
  "cancelUrl": "https://app.example.com/billing"
}

This charges $10, funds the owner's Oblien wallet at the standard rate (currently 1,000 platform credits), and grants 100 namespace credits. It does not pay out to the reseller. Checkout and invoices use the chosen product name/description. Those display fields do not grant CPU minutes or increase account resource capacity. kind: 'topup' adds the allowance as purchased credits; subscription sets the included allowance for each paid monthly/yearly cycle. Unused purchased credits carry across renewals; spent top-ups do not replenish.

FieldContract
offer.referenceOptional application catalog reference, at most 100 characters
offer.nameRequired customer-facing name, at most 120 characters
offer.descriptionOptional customer-facing text, at most 500 characters
offer.unitAmountRequired integer USD cents, 100–1,000,000 ($1–$10,000); monthly capacity also accepts 0 when fully funded by the owner
offer.currencyusd only; defaults to usd
offer.billingModemonthly purchases monthly capacity. Omit for the existing metered allowance product; its historical payg value also selects that allowance product. New capped PAYG uses /billing/capacity/preview.
offer.capacityRequired for monthly: shared vcpus, memoryMb, diskGb, workspaces. Rejected for metered offers.
offer.tariffIdOptional published capacity tariff ID for monthly; renewals and changes preserve saved terms by default.
offer.creditsMetered offer: integer 1–1,000,000,000. Monthly capacity: 0 (also the API default when omitted); no compute allowance.
offer.policyMetered subscription only: optional overdraft, suspendThreshold, onOverdraftAction; zero grace by default. Rejected for monthly capacity.
offer.resourceLimitsSubscription only: nonempty subset of max_workspaces, max_vcpus, max_ram_mb, max_disk_gb, max_total_vcpus, max_total_ram_mb, max_total_disk_gb; integer values 0–1,000,000,000 or null
metadataOptional, only with offer: at most 35 string entries; key [A-Za-z0-9_], length 1–40; value at most 500 characters without control characters
idempotencyKeyRequired with offer, nonempty string up to 200 characters; reuse only for an identical request

Metadata may not override service, user_id, client_id, namespace, billing_order_id, kind, credits, amount, credits_per_cycle, plan_family, billing_interval, prototype keys, or any oblien_ or promotion_ prefix. Unknown offer fields are rejected. Customer accepts only email and/or name; provider customer IDs are never accepted. Custom offers support native Stripe codes by default or scoped Oblien-admin codes with checkoutMode: "oblien", and exclude trials and portal price changes. Existing reseller subscriptions support proration through the plan-change APIs above.

The contract is saved before contacting the provider. It binds owner, namespace, amount, allowance, metadata, provider customer and checkout mode. Reusing a key with a changed request returns 409 billing_idempotency_conflict. An interrupted checkout older than the provider's safe idempotency window returns 409 billing_checkout_reconciliation_required; do not create another payment blindly.

Saved policy and resource caps

Check billing.catalog().reseller before enabling offers with policy or limits: contractVersion >= 2, offerPolicy: true, resourceLimits: true, and effectiveResourceLimits: true identify per-VM support; aggregateResourceLimits: true additionally confirms total namespace enforcement in the deployed API. A dashboard-only deployment does not add this API capability. SDK 2.4.0 already transports reseller offers; 2.5.0 adds these TypeScript fields and public reseller type exports.

For metered offers, grace is zero by default. To permit 60 extra namespace credits, save policy: { overdraft: 60, suspendThreshold: 60, onOverdraftAction: 'stop_workspaces' } on the subscription offer. suspendThreshold must be at least overdraft. quota.balance already includes grace (limit + overdraft - used); never add it again on the client. A top-up adds purchased allowance and cannot change policy or resource caps.

The saved price, allowance, metadata, grace and resource caps are reused for renewals. Editing your catalog does not alter existing subscriptions. The cycle checkpoint, allowance reset, grace and receipt allocation commit in one database transaction. VM admission reads caps from that committed cycle, so a missed reseller webhook cannot leave a larger namespace mirror as a bypass. A full refund of the current cycle removes its remaining allowance and grace; an old invoice refund cannot erase a newer cycle.

max_workspaces counts allocated workspaces in this namespace. CPU, RAM and disk are per-workspace caps. The optional max_total_vcpus, max_total_ram_mb and max_total_disk_gb additionally cap all allocated resources together, including stopped VMs, managed disks and pending changes. Top-ups never change these caps. SDK 2.6.0 adds the corresponding types. Oblien resolves the strictest cap from the saved offer, configured namespace policy, owner account and platform. null inherits capacity and cannot remove a restriction from another layer. Concurrent creates reserve slots under a namespace lock. Create, resize and subsequent VM start/resume/wake/snapshot-resume check the caps. Lowering limits does not delete existing workspaces or resize running machines; arrange any necessary cleanup or stop/resize before moving customers to a smaller plan. Service, project, seat and other application limits belong to your application and can be saved as versioned metadata; Oblien does not interpret those metadata fields.

Only the namespace receives this subscription. Owner plan reads, owner portal customer lookup and owner plan replacement exclude namespace subscriptions. The owner's wallet receives payment funding, and that wallet entry does not change the owner's platform plan.

Stripe, the payment database, namespace storage and VM hosts are separate systems. The whole network operation is not one distributed transaction: receipts and cycle checkpoints make retries safe, and fresh entitlement/status reads prevent a checkout redirect from granting access before fulfillment.

Inspect a reseller checkout

GET /billing/checkout/:checkoutId?namespace=tenant-a

Requires an admin or billing credential. Looks up both owner and namespace before contacting Stripe. Returns 404 no_checkout for an unknown or differently scoped session. This lookup supports saved reseller offers; catalog payments use entitlement/subscription and payment events.

{
  "success": true,
  "namespace": "tenant-a",
  "checkout": {
    "id": "cs_...",
    "kind": "subscription",
    "status": "complete",
    "paymentStatus": "paid",
    "fulfilled": true,
    "fulfillmentStatus": "completed",
    "walletCreditsGranted": 1000,
    "namespaceCreditsGranted": 100,
    "billingInterval": "monthly",
    "subscriptionId": "sub_...",
    "offer": {
      "reference": "compute-starter-v1",
      "name": "Compute Starter",
      "unitAmount": 1000,
      "currency": "usd",
      "credits": 100
    },
    "metadata": { "appOrderId": "order-123", "planVersion": "1" }
  }
}

paymentStatus: paid can coexist with fulfillmentStatus: pending until local fulfillment commits. superseded means this checkout must not replace a newer plan. fulfilled means initial processing finished; always inspect the net credit fields and fulfillmentStatus for partially_refunded, refunded or disputed. Use GET /billing/entitlement and /billing/balance for current access. The lookup describes the checkout's initial payment, not its later renewal invoices. Reseller subscriptions also expose offer and metadata through GET /billing/subscription; their tierId is reseller and does not upgrade owner capacity.

Custom payment.succeeded, subscription.renewed and entitlement.changed events include orgRef, namespace, kind, checkoutId, paymentId, saved offer, application metadata, amount: { unitAmount, currency }, net walletCredits and namespaceCredits. Fulfillment uses stable receipt-based event IDs. Verify the raw-body HMAC, deduplicate by event ID, then re-read entitlement to handle reordering. For reseller offers, financial changes and outgoing event records commit together. Each authorized receiver retries independently after non-2xx responses or network failure, with the same event ID and body. Save and deduplicate the event before returning 2xx, and reconcile current entitlement to tolerate reordering. Disabled, deleted or retargeted receivers stop receiving pending deliveries.

Monthly capacity checkout results distinguish gross walletCreditsFunded, capacityCreditsCharged and signed walletCreditDelta; namespaceCreditsGranted is zero. Gross payment funding is not the remaining spendable wallet balance. Read committed capacity coverage instead of granting another allowance from the payment event.

Useful checkout and portal errors

Billing responses include a safe diagnostic code and message. Internal failures include a details.reference correlated with backend logs; SQL and raw provider requests are not returned.

{
  "success": false,
  "error": "billing_database_collation_error",
  "code": "billing_database_collation_error",
  "message": "Oblien billing storage is misconfigured. Contact Oblien support with the reference below.",
  "details": {
    "cause": "ER_CANT_AGGREGATE_NCOLLATIONS",
    "retryable": false,
    "reference": "support-reference"
  }
}
HTTPCodeAction
400invalid_offer, invalid_billing_metadata, ambiguous_checkout_offerCorrect the backend request
400billing_redirect_not_allowedAsk the operator to allowlist the browser return host in REDIRECT_ALLOWED_HOSTS
401 / 403authentication or scope errorUse deployed routes and server-side admin/billing credentials
404no_customer, no_subscription, no_checkoutNo matching record in this owner/namespace; never fall back to another customer
409billing_customer_conflict, billing_identity_conflictReconcile an isolated customer binding; do not bypass the check
409billing_idempotency_conflict, billing_checkout_reconciliation_requiredReconcile the existing application order
502 / 503billing_provider_configuration_error, billing_provider_unavailable, billing_provider_rejected, billing_storage_unavailable, billing_database_collation_errorDisplay the safe message/reference and fix the provider or backend configuration

Supplied checkout and portal return URLs must be HTTPS and allowlisted. An invalid host returns an error; it is not silently replaced. Omitted URLs use the platform default. Retrying payment creation must reuse the original idempotency key.

SDK 2.4.0 adds typed offer/metadata and client.billing.checkoutStatus(namespace, checkoutId). For a complete customer onboarding, resource-limit, VM creation and signed webhook walkthrough, see Build a SaaS or VM reseller.