Overview

Build a SaaS or VM reseller

Choose the product before publishing your prices. A metered offer grants a finite namespace allowance. A monthly capacity offer prepays the namespace’s shared CPU, RAM and disk for the paid month, without a second compute-credit ceiling. Both use the same billing identity and verified payment flow. Builds and hosted projects are ordinary workspaces: they share whichever namespace pool you assign. Separate build pools and build TTLs belong to your application's placement policy.

For a monthly offer, set billingMode: 'monthly', capacity: { vcpus, memoryMb, diskGb, workspaces }, and credits: 0 on your server-owned offer. Keep name, description and unitAmount in your retail catalog. Your retail price is independent of the provider cost: your available account credit balance covers any shortfall, including the entire cost of a zero-price sponsored offer. A verified payment funds the owner wallet and debits the provider capacity fee in the same transaction. Checkout/payment events expose gross walletCreditsFunded, capacityCreditsCharged, and the signed walletCreditDelta; a negative delta is your contribution. Never grant these credits twice.

Renewals and prorated plan changes use the existing subscription APIs and saved terms. Purchased top-ups and transfer packs remain separate. Show paid-through date, renewal and capacity utilization for a monthly customer; do not show an obsolete compute-credit exhaustion warning. Show managed-transfer warnings separately and offer a transfer purchase when needed. Owner account capacity and reseller eligibility are never inherited from a customer subscription.

Give each customer a stable namespace. Your backend chooses what they can run, how much they pay, the allowance they receive, and the product description shown at checkout. Oblien hosts payment, funds your Oblien wallet, meters VM usage, and enforces namespace quotas.

Use https://api.oblien.com. The complete route contract is in Billing. SDK 2.8.0 includes typed capacity, saved offers, resource limits, coverage and transfer operations. Read the deployed capacity catalog before enabling the product. REST calls do not depend on an SDK release. Existing paid credit contracts remain on their saved terms until an explicit plan change.

Use an Enterprise owner account for Oblien-hosted customer checkout. The API enforces this for custom offers, catalog plans and credit top-ups in test and live mode. A customer's namespace plan cannot qualify the owner. A refusal uses 403 reseller_enterprise_required and creates no payment session. Existing customer billing management and payment processing continue after an owner downgrade.

Keep price, credits, product text and capacity separate

ControlExampleEffect
Checkout priceoffer.unitAmount: 1000, USD centsCharges $10 and funds your wallet at Oblien's standard rate: currently 1,000 platform credits
Namespace allowanceoffer.credits: 100Grants 100 metered credits to this namespace
Monthly resource pooloffer.billingMode: 'monthly', offer.capacity, offer.credits: 0Prepays the shared pool; provider capacity cost is deducted from payment funding
Product textoffer.name and offer.descriptionShows your chosen plan and benefits to the paying customer
Resource limitsTwo workspaces, two CPUs per VM, three CPUs totalCaps both individual VM size and combined allocation

The money funds your Oblien wallet; it is not paid out to you. A custom subscription does not upgrade your account's capacity. Namespace contracts are separate, while your wallet and account resource pool are shared. The saved provider capacity fee is deducted from customer payment funding plus your paid balance when needed. An empty wallet does not revoke already-paid monthly compute. PAYG, future renewals requiring your contribution, retained storage and proxy-transfer purchases still require their applicable funding.

A description such as “1,000,000 CPU minutes” is display text. It does not change VM metering, reserve CPU time, or guarantee that 100 credits buy that amount of compute. Set allowances and resource limits using the actual service rates before promising a specific quantity. If your app uses its own token or credit unit, convert it explicitly on your backend.

1. Establish customer identity and safe defaults

Keep the owner admin credential on your backend. Authenticate the customer in your app, then resolve an immutable namespace from your database, such as tenant-<random-id>. Never accept a browser-provided owner, namespace, Stripe customer ID, offer price, or credit allowance as trusted input. Do not reuse a former customer's namespace slug for someone else.

import Oblien from 'oblien';

const client = new Oblien({
  clientId: process.env.OBLIEN_CLIENT_ID!,
  clientSecret: process.env.OBLIEN_CLIENT_SECRET!,
});

// Configure before onboarding customers: no paid VM usage before funding.
await client.billing.setDefaults({
  quotaLimit: 0, overdraft: 0, suspendThreshold: 0,
  onOverdraftAction: 'stop_workspaces', autoApply: true,
});

// namespace comes from your authenticated customer's database record.
await client.namespaces.create({
  name: 'Customer workspace', slug: namespace,
  resource_limits: {
    max_workspaces: 2,
    max_vcpus: 2,
    max_ram_mb: 2048,
    max_disk_gb: 32,
    max_total_vcpus: 3,
    max_total_ram_mb: 8192,
    max_total_disk_gb: 64,
  },
});
await client.billing.setPolicy(namespace, {
  quotaLimit: 0, overdraft: 0, suspendThreshold: 0,
  onOverdraftAction: 'stop_workspaces',
});

Defaults apply to new namespaces, not retroactively to every existing customer. Set an explicit policy during onboarding before allowing VM creation. quotaLimit: null is uncapped; zero is a useful unpaid starting state. Namespace creation and resource-limit changes require an admin key. A billing key can use the Billing API but cannot provision resources.

2. Set VM limits separately from spending

Namespace fieldScope
max_workspacesMaximum number of workspaces in this namespace
max_vcpusMaximum CPUs of each workspace
max_ram_mbMaximum RAM of each workspace, in MB
max_disk_gbMaximum disk of each workspace, in GB
max_total_vcpusCombined allocated CPU quota across the namespace
max_total_ram_mbCombined allocated RAM across the namespace, in MB
max_total_disk_gbCombined allocated VM and managed-disk storage, in GB

Use both per-workspace and total caps for retail plans. With two workspaces, max_vcpus: 2 and max_total_vcpus: 3, each VM may request two CPUs, but both together must fit three. Total allocation includes stopped VMs, build workspaces, managed disks and pending resizes. Credit top-ups do not increase any hardware limit. Your account's limits still apply. Choose a VM shape and an image that fit your account and namespace limits; a checkout cannot raise those limits. Read Namespaces and Workspace plans.

Oblien calculates effective capacity. Send the customer's limits as policy; use explicit finite per-VM and total limits for retail customers. null inherits another policy layer and must not be mistaken for a retail allowance. There is no client-side account-capacity calculation. Namespace detail and mutation responses include effective_resource_limits alongside the original resource_limits. allocated_resource_usage reports the current combined workspaces, vcpus, ram_mb, disk_gb and pending_updates when total caps are configured. Oblien intersects the saved paid offer, configured policy, owner capacity and platform ceilings. A 50-service customer can spread workloads over several VMs; one VM must still fit its effective ceiling. Prices, credit allowances and service counts are independent of those VM sizes.

Capacity admission reserves the allocation under owner/namespace database locks before calling the VM provider. An uncertain resize keeps its larger reservation until provider configuration and live state agree; pending_capacity_verification means capacity has not yet been released. The recovery worker observes provider state and never restarts customer VMs on its own. Reduce or delete resources before requesting more capacity. Lowering a policy does not shrink disks or stop existing VMs automatically.

Every workspace has a platform ceiling of 12 vCPUs, including Enterprise and reseller customers. Sell larger aggregate CPU allowances through max_total_vcpus and distribute them across workspaces. Neither a larger paid offer nor max_vcpus: null removes this ceiling.

Check billing.catalog().reseller.aggregateResourceLimits === true before selling a plan that relies on total caps. Deploy the API capability before the reseller application. For example, a rejected total-RAM request returns 409 NAMESPACE_LIMIT_REACHED with details.enforcementScope: "namespace_allocated_pool", the resource, requested amount, current usage and effective limit, plus the normal request ID.

3. Start a checkout from a saved application order

Choose the offer on the server from your catalog and save the order before calling Oblien. Supply a unique stable idempotencyKey for that order, and persist the returned checkoutId. Retries must send the identical namespace, offer, metadata, customer contact, interval and return URLs. Changed parameters return 409 billing_idempotency_conflict; an old interrupted checkout may require reconciliation rather than a fresh charge attempt.

Metered allowance offer

This example sells a finite credit allowance. Its monthly interval describes allowance renewal, rather than a prepaid resource pool.

const checkout = await client.billing.checkout({
  namespace,
  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,
      max_total_vcpus: 3, max_total_ram_mb: 8192, max_total_disk_gb: 64 },
  },
  metadata: { appOrderId: order.id, planVersion: '1' },
  customer: { email: customer.email, name: customer.name },
  idempotencyKey: order.id,
  successUrl: 'https://app.example.com/billing/return?session_id={CHECKOUT_SESSION_ID}',
  cancelUrl: 'https://app.example.com/billing',
});
// Save checkout.checkoutId against order.id, then redirect to checkout.url.

For a one-time metered refill, send kind: 'topup' with an offer and a new order ID. It adds purchased namespace allowance without resetting existing usage. Only unused purchased credits carry into a later metered subscription cycle. A metered monthly/yearly subscription sets the saved allowance and policy for each paid cycle; yearly means one allowance grant per annual payment, not twelve monthly grants.

Direct Oblien wallet deposits start at $5 and use 100 credits/USD. That retail minimum does not change saved reseller contracts. Metered offers support USD, $1–$10,000, monthly/yearly subscriptions or one-time top-ups, and a positive integer namespace allowance. Monthly capacity instead uses zero credits and a monthly interval; its retail price may also be zero when you fund the whole period. Paid offers support promotion codes through the selected checkout mode. Free trials and arbitrary portal price changes are unsupported. Saved offers are immutable; changing your catalog affects future purchases. Use the plan-change APIs for an existing reseller subscription.

Monthly capacity offer

The owner’s credit plan remains the funding and capacity tier. In Mode A, pay for a pool by spending owner account credits with previewCapacity and confirmCapacity; there is no need for another Oblien subscription. In Mode B, the hosted customer payment can fund its saved monthly capacity offer below. Neither path changes the owner’s account tier. New plan-funded pools share the owner’s overall owned/running limits across namespaces, in addition to each namespace policy.

Read allocationBasis and the effective limits. New contracts free running CPU/RAM after a confirmed stop; retained disks and owned workspace slots still count. Old allocated contracts keep their saved rights. Builds and hosting are ordinary workspaces to Oblien: select their resources, namespace and TTL in your application, and display the provider’s actual limit violation. Do not infer resource capacity from dollars, credits or VM vCPU topology.

Resolve plan from your backend's retail catalog. Its price, description and shared resource pool belong to your application. The published Oblien tariff determines the provider cost. If your price is lower, your available account credit balance covers the difference; checkout checks that contribution and physical availability before starting payment.

For example, 1 vCPU / 4 GiB RAM / 25 GiB disk costs $11.25/month under workspace-capacity-2026-10-v1. You can sell it for $5 and fund $6.25 (625 credits) from your wallet each month. Set unitAmount: 0 to give the customer a sponsored subscription and fund all $11.25 (1,125 credits) yourself. A zero-price checkout needs no payment method; renewal remains enabled until canceled.

Insufficient credits return 402 insufficient_redeemable_balance. Add account credits and retry with the same idempotency key. All committed account credits can fund monthly capacity. Namespace allowances and uncredited payment receipts cannot; provider payments still require verification in the correct test/live environment. The pre-payment check does not reserve wallet money. If another operation spends it before settlement, the payment remains pending fulfillment and automatically retries after funding. Only mark the customer's capacity ready after confirmed fulfillment and computeCovered: true; keep funds available for every future cycle and any subsidized upgrade.

const providerCatalog = await client.billing.capacityCatalog();
const checkout = await client.billing.checkout({
  namespace,
  kind: 'subscription', billingInterval: 'monthly',
  offer: {
    reference: plan.version, name: plan.name, description: plan.description,
    unitAmount: plan.monthlyPriceCents, currency: 'usd',
    billingMode: 'monthly', credits: 0,
    capacity: plan.capacity, // { vcpus, memoryMb, diskGb, workspaces }
    tariffId: providerCatalog.tariffId,
  },
  metadata: { appOrderId: order.id },
  idempotencyKey: order.id,
  successUrl: 'https://app.example.com/billing/return?session_id={CHECKOUT_SESSION_ID}',
  cancelUrl: 'https://app.example.com/billing',
});
// Save checkout.checkoutId, then redirect to checkout.url.
// After verified payment, read billing.capacity(namespace) and entitlement.

Omit offer.policy: a monthly pool has no credit overdraft policy, and that field is rejected. offer.resourceLimits may impose additional hardware restrictions. Existing configured namespace limits still apply. The paid period covers compute and included storage; managed proxy transfer is a separate purchase. Expiry stops compute and retains disks under the saved, billable retention policy.

For capped PAYG or a wallet-funded monthly pool, use previewCapacity / confirmCapacity. Setting offer.billingMode: 'payg' on the older checkout selects the metered allowance product; it does not activate a new PAYG capacity contract.

Changing an existing subscription

Use billing.previewPlanChange(namespace, { offer, idempotencyKey }) to show the customer their saved current price, new price, unused-time credit, amount due now, effective date and quote expiry. Accept it with billing.changePlan(namespace, { quoteId, idempotencyKey }). Choose the namespace and offer on your authenticated backend, from your own customer and catalog records.

A higher price charges the prorated difference. Stripe keeps the old price if payment fails; Oblien applies the new offer and limits only after payment and the provider update are confirmed. The namespace receives only the prorated increase in included credits, rounded down to six decimals, while usage and purchased top-ups are preserved. The next renewal uses the full saved new allowance. Spent top-ups cannot reappear at renewal.

That credit increase applies to metered offers. Monthly capacity upgrades extend the purchased pool after confirmed funding and grant no compute credits. A resource reduction or change of billing model waits for renewal even when its retail price increases. Always use the quote's direction and effective date. Existing hosted credit subscriptions adopt monthly capacity through this plan-change flow, rather than a competing checkout.

A lower or equal price is scheduled for the next paid billing cycle. billing.subscription(namespace) includes pendingChange; billing.planChange(namespace, changeId) returns the operation. Let the customer cancel a pending downgrade before its effective date using billing.cancelPlanChange(namespace, changeId, { idempotencyKey }). This keeps the underlying subscription active. The monthly/yearly interval is preserved.

Treat payment_pending, queued, dispatching, and scheduled as pending, even when the HTTP request succeeds. Use the returned Stripe invoice URL if payment needs attention. Keep retrying the same operation/key after a timeout. Signed subscription.change.applied and entitlement.changed events confirm current access; re-read entitlement after delayed events. Independent namespace restrictions and the account resource pool still apply to upgraded limits.

See the complete plan-change contract for endpoints, SDK methods, statuses, cancellation rules, refund behavior, and signed event names. The reseller owner's account subscription is unaffected.

Checkout pages and promotion codes

Stripe-hosted checkout is the default, with Stripe's promotion-code field enabled. Omit both options, or send checkoutMode: "stripe", allowPromotionCodes: true. Set allowPromotionCodes: false to hide and disable code entry. Customers go directly to the returned Stripe URL; your integration does not collect codes or expose a Stripe key.

Choose checkoutMode: "oblien" for Oblien's custom payment page and percentage discount caps or combined per-owner use limits. Both checkout modes support account/namespace audience restrictions through Admin-issued codes. The same allowPromotionCodes flag controls code entry. Existing links continue to work.

const checkout = await client.billing.checkout({
  ...savedCheckoutRequest, // Your authenticated customer's namespace and server-priced offer.
  checkoutMode: 'stripe',  // Default; use 'oblien' for the custom page.
  allowPromotionCodes: true, // Default; false disables code entry.
});
// Save checkout.checkoutId and redirect to checkout.url unchanged.

For direct Stripe checkout, platform operators issue codes at Admin → Promotions, selecting Stripe hosted. Choose selected Oblien account IDs, namespace-only scope, an expiry and a shared redemption limit. Selecting owner 80 covers that owner's namespace customers. Each namespace keeps its own Stripe customer and billing portal; a customer restriction enforces eligibility on Stripe's page. Matching emails, slugs or reseller-supplied metadata do not grant eligibility. Newly issued codes become available on new checkouts, including for future namespaces.

Admin native codes discount the first payment only: the first month on monthly plans or the first annual invoice on annual plans. Subsequent renewals use the normal saved price and refill the full allowance. They also cover one-time credit purchases in the selected account/namespace scope. Set Redemptions per customer to 1 for a welcome offer; the shared total still covers every eligible namespace. Stripe enforces these limits and minimum purchases. Native percentages cannot have a dollar cap or a combined per-owner use limit. Codes created directly in Stripe Dashboard follow the duration and restrictions configured there.

For the custom page, only Oblien administrators issue promotions at Admin → Promotions. A code can be restricted to selected Oblien owner IDs and namespace checkout. Selecting owner 80 with namespace-only scope makes it available to that owner's namespace customers. Email and reseller metadata cannot override eligibility. Custom codes have an expiry, shared redemption limit, optional per-owner/namespace limits, and a required dollar cap for percentages; they discount the first payment only.

Both modes preserve the full purchased offer. For a $10 offer granting 100 internal namespace credits, a 50% code charges $5, funds the full standard 1,000-credit owner wallet purchase, and grants the same 100 namespace credits. Oblien records the $5 subsidy separately. Resource limits, grace, plan description and the base recurring price stay as saved. With a once coupon, the next payment charges $10 and refills the configured allowance. A namespace checkout never subscribes the reseller owner to its customer's plan.

Treat checkoutId as opaque (bco_... and cs_... are supported, including bco_ for direct Stripe). Return URLs and signed events retain that public ID. Use verified webhooks and entitlement to grant access, including for fully discounted checkout. Refund only the cash collected; the corresponding credits are reversed proportionally, without changing a later subscription cycle's allowance. See the billing API contract for both page modes and transaction semantics.

Metadata supports up to 35 string entries, key length 40 and value length 500. Identity, routing and credit fields (including namespace, user_id, credits, billing_order_id and the oblien_ prefix) are reserved. Store application references, not secrets or card data. The saved offer and metadata are returned through checkout status, subscription reads and payment events.

You can instead sell Oblien catalog products: fetch client.billing.catalog(), then pass planTierId for subscriptions or packId for top-ups. Do not combine these with offer. Custom metadata is supported on reseller offers.

Pricing a finite allowance

Keep the ordinary namespace allowance funded by its payment; any subsidy needs an explicit promotional budget. Credits measure usage across resources, not minutes. Show the plan price, included allowance, finite VM and total namespace limits, top-up terms and grace before checkout. A top-up buys consumption; increased capacity requires a new saved plan or capacity contract.

Use /billing/capacity/catalog for current capacity prices and /pricing/calculator only for the existing credit meter. The historical workspace-2026-09-29 proposal is separate from the active capacity tariff. See Billing & Credits. Group accounting counts customer cash once; funding a reseller-owned Oblien wallet is not a second company revenue receipt.

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 support in the deployed API. A dashboard-only deployment does not add this API capability. SDK 2.4.0 already transports reseller offers; the 2.5.0 source 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. 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: receipt, wallet funding, subscription state, namespace allowance, grace, paid resource caps, cycle checkpoint and outgoing billing event records commit together in the payment database. Interrupted external work recovers through durable jobs. Fresh entitlement/status reads prevent a checkout redirect from granting access before fulfillment.

Configure the checkout return host

All supplied return URLs must be HTTPS and use a host allowlisted by Oblien's operator in REDIRECT_ALLOWED_HOSTS (or APP_URL). An unapproved host returns 400 billing_redirect_not_allowed. Omitted URLs use the platform default. The allowlist concerns the browser's return host, not the webhook receiver.

4. Confirm payment on your backend

The browser returning to your app is not proof of payment. For a reseller checkout, use:

const { checkout } = await client.billing.checkoutStatus(namespace, order.checkoutId);
const [entitlement, balance, subscription] = await Promise.all([
  client.billing.entitlement(namespace),
  client.billing.balance(namespace),
  client.billing.subscription(namespace),
]);

GET /billing/checkout/:checkoutId?namespace=... verifies both owner and namespace. Unknown or differently scoped IDs return 404 no_checkout. It reports the original payment's paymentStatus, fulfilled, fulfillmentStatus, walletCreditsGranted, and namespaceCreditsGranted. Refunded/disputed payments may have fulfilled: true but zero net grants. Use entitlement and balance for current access; checkout status describes the initial payment, while later renewals have separate payment events.

Top-up-only customers may have subscription: null while still holding usable purchased allowance. Use your chosen subscription policy together with billing.balance().blocking, not just whether a subscription exists. For catalog payments, reconcile entitlement/subscription and payment events.

Register a signed billing receiver

Use an admin credential to register billing webhooks. Namespace tokens cannot create, read, update or delete billing webhook configurations. A single account-wide receiver is usually suitable: verify the signature, then map data.orgRef and data.namespace to your customer. There are at most ten hooks per owner, so a hook per customer is not a scalable default.

await client.webhooks.create({
  url: process.env.OBLIEN_WEBHOOK_URL!,
  secret: process.env.OBLIEN_WEBHOOK_SECRET!,
  events: [
    'payment.succeeded', 'subscription.renewed', 'subscription.updated',
    'subscription.past_due', 'subscription.canceled', 'entitlement.changed',
    'namespace.suspended', 'namespace.restored',
    'namespace.quota.threshold', 'credits.low', 'credits.depleted',
    'capacity.changed', 'capacity.renewed', 'capacity.expired',
    'capacity.payment_required', 'capacity.revoked',
    'network.topup_applied', 'network.allowance.low', 'network.allowance.depleted',
    'storage.retention.payment_required', 'storage.retention.paid',
  ],
});

OBLIEN_WEBHOOK_URL is a setting in your SaaS backend, not a universal Oblien route. Use the exact public HTTPS route your application implements. For the Openship integration this is https://api.openship.io/api/billing/oblien-webhook. A valid receiver must handle POST there; a public 404 means the handler or route is not deployed at that address.

Every signed delivery includes X-Webhook-Signature (HMAC-SHA256 of raw body bytes) and X-Webhook-Id, matching the JSON body's id. Verify before parsing or acting. Register your raw-body route before a global JSON middleware:

import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

app.post('/api/billing/oblien-webhook', express.raw({ type: 'application/json', limit: '256kb' }), async (req, res) => {
  const signature = req.get('X-Webhook-Signature') || '';
  if (!/^[a-f0-9]{64}$/i.test(signature)) return res.sendStatus(401);
  const expected = createHmac('sha256', process.env.OBLIEN_WEBHOOK_SECRET!)
    .update(req.body).digest();
  if (!timingSafeEqual(Buffer.from(signature, 'hex'), expected)) return res.sendStatus(401);
  let event;
  try { event = JSON.parse(req.body.toString('utf8')); }
  catch { return res.sendStatus(400); }
  if (!event.id || event.id !== req.get('X-Webhook-Id')) return res.sendStatus(400);
  // Implement this as one DB transaction with UNIQUE(event.id) and a durable job.
  // Resolve orgRef + namespace to your customer; reject unknown identities.
  await acceptBillingEventOnce(event);
  return res.sendStatus(200);
});

acceptBillingEventOnce is application code: deduplicate the stable event ID and durably enqueue work before acknowledging it. Its worker re-reads Oblien entitlement/subscription to tolerate reordered events. Do not apply arbitrary credit amounts from an unsigned body or repeat grants on duplicate delivery.

Reseller payment, subscription and namespace credit-alert notifications are durable. Each receiver retries independently after a non-2xx response or network failure, with the same event ID and body, and retries survive API restarts. Disabled, deleted or retargeted receivers no longer receive pending deliveries. A lost acknowledgment can cause a repeat, so save the event before returning 2xx and reconcile current entitlement to handle reordering. General namespace/VM operational events remain best effort. Verified incoming Stripe events are also saved for recovery; the browser redirect never grants credits. A paid checkout stays pending until local fulfillment commits.

Namespace credit alerts and customer recovery

Use the alerts below for metered allowances. For a monthly customer, read billing.capacity(namespace) for coverage, renewal, retained-storage amounts due and transfer. Reconcile capacity/storage/network events with that state. A transfer warning alone does not end paid compute, and an old credit quota must not produce a monthly compute-exhaustion banner.

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.

5. Provision and operate customer VMs

After verified funding and your access checks, use the same server-resolved namespace for every VM request:

const vm = await client.workspaces.create({
  namespace,
  name: 'Customer application', image: 'oblien/node:24',
  cpus: 2, memory_mb: 2048, disk_size_mb: 32768,
  idempotency_key: `vm-${provisioningOrder.id}`, wait_ready: false,
});
// Save vm.id immediately; a readiness timeout does not cancel creation.
const ready = await client.workspaces.waitUntilReady(vm.id);

Fetch images from client.workspaces.images.list() and use the catalog's image reference. Reuse a VM creation key when retrying an uncertain response. Scope stored workspace IDs to the same customer before forwarding stop, start, resize, shell, snapshot, network or delete requests. See Workspace API.

For direct customer or agent access, issue a short-lived token bound to their namespace or workspace using Scoped Tokens. Keep privileged billing and resource-policy changes behind your backend. Tenant tokens cannot call billing routes or change sibling webhook configurations. VM resource caps, quotas and token scopes serve different purposes; configure all three.

6. Handle exhaustion, renewal and customer self-service

Use the API's returned balance.blocking and current entitlement. Oblien enforces the applicable billing contract on create/start/resume/wake; stop/read/delete remain available for cleanup. A monthly namespace exposes tierId: 'capacity', billingMode and computeCovered; its compatibility quota is not a finite allowance. Paid monthly coverage survives an empty owner wallet, while manual suspension and resource limits still apply.

For metered allowances, top-ups can recover billing suspension. Use overdraft <= suspendThreshold for a grace zone, or zero for both to avoid one. Metering is periodic, so zero grace is not an instantaneous dollar-accurate cutoff. For expired capacity, settle retained storage and renew coverage before restarting. Buying transfer bytes does not renew compute.

Show subscription details with client.billing.subscription(namespace). Open the customer's invoices and payment methods with client.billing.portal({ namespace, returnUrl }). The portal uses an isolated provider customer for that owner and namespace; a shared historical customer returns 409 billing_customer_conflict instead of exposing mixed billing history.

cancelSubscription(namespace) schedules cancellation at the paid period end; resumeSubscription(namespace) reverses that schedule before expiry. They do not accept a caller-chosen provider customer or subscription ID. An ended subscription needs a new checkout. Portal price switching is disabled to keep the saved allowance contract synchronized with payments.

Paid metered renewals reset used allowance once, set the new included ceiling and carry only unused purchased credits. Monthly capacity renewals extend paid coverage at the saved price without granting another compute allowance. Purchased transfer remains separate. Refunds and disputes follow the saved payment's model and period; an older invoice cannot revoke a newer paid period. Keep access synchronized with current entitlement.

For externally collected payments funding a metered allowance, use setPolicy(namespace, ...), then resetQuota(namespace, { periodEnd }) once per external cycle. Reuse the same ISO cycle end on retry. This does not fund your wallet; fund it separately. An active hosted subscription manages its own reset and rejects manual resets with 409 subscription_managed. To purchase monthly capacity from available account credits, use the capacity quote/confirmation flow instead.

7. Show actionable failures and verify launch readiness

Preserve API message, code and details.reference in your backend diagnostics and customer error UI. Do not replace every response with “Checkout failed.” Map invalid input and unsupported offers to a correctable request, unallowlisted return hosts to operator setup, and database/provider failures to an unavailable billing service. Never display raw SQL, provider keys, payment details or credentials. The SDK exposes these safe fields through OblienError.

Before launch, verify payment and its signed public webhook, the namespace's committed contract, VM admission, renewal, plan changes, portal isolation, cancellation/resume, refunds and replayed events without duplicate grants. For metered allowances, verify exhaustion and top-up recovery. For monthly capacity, verify continued paid compute with zero legacy allowance, separate transfer exhaustion, and expiry/storage settlement/restart. Check deployed capabilities and the published SDK version. A generated checkout URL or local payment test alone does not establish that the public payment-to-SaaS cycle is ready.