Skip to main content

Self-serve billing integration

This guide connects the main self-serve Billing APIs into one implementation path:

  1. Your backend issues a short-lived bearer token for a stable principal_id.
  2. Your frontend uses that bearer token to read catalog products and prices.
  3. Your frontend creates a hosted checkout session when the user buys.
  4. Your frontend creates a hosted portal session when the user manages billing.

Use this guide when you want a practical storefront path. If you already have a sales-led or contract-driven flow, prefer direct plan assignment instead of provider checkout.

Prereqs

Provider-hosted checkout, portal, and webhook finalization are handled by Vluna. This guide focuses only on what your backend and frontend need to integrate.

Architecture split

Keep the responsibility boundary explicit:

  • Backend only:
    • POST /mgt/v1/token/issue
  • Frontend or mobile app with bearer token:
    • GET /api/v1/catalog/products
    • GET /api/v1/catalog/prices
    • POST /api/v1/checkout/sessions
    • POST /api/v1/portal/sessions

Do not call token/issue directly from an untrusted client. It is a service-to-service step that binds principal_id to billing_account_id, binds user_id to billing_user_id under that account, and returns the short-lived bearer token your client will use afterward.

Step 1: issue a bearer token from your backend

Your backend should issue the token as early as possible in a customer-scoped session:

  1. Resolve the correct billing owner and runtime user in your system.
  2. Use that stable identifier as principal_id.
  3. Call POST /mgt/v1/token/issue with scopes such as checkout and portal.
  4. Store the returned billing_account_id and billing_user_id if your backend needs support or analytics joins.
  5. Return only the short-lived bearer token to the frontend.

Recommended request shape:

  • principal_id: stable billing owner in your system
  • user_id: end-user identifier for the current session
  • scopes: ['checkout', 'portal']
  • session_ttl_sec: short lifetime, for example 900

Why this should happen up front:

  • It validates connectivity before the user reaches your pricing page.
  • It creates or confirms the immutable principal_id -> billing_account_id mapping outside the purchase path.
  • It creates or confirms the runtime user mapping for that session.
  • It gives support and analytics stable Vluna-side account and user identifiers.

Step 2: render the storefront from the public catalog

Use the bearer token from step 1 to call:

  • GET /api/v1/catalog/products
  • GET /api/v1/catalog/prices

Recommended storefront pattern:

  1. List products first.
  2. For each displayed product, load prices with the relevant product_id.
  3. Render recurring and one-time prices differently.
  4. Respect returned ordering instead of re-sorting prices in the client.
  5. Use response hints such as subscription-state projections to decide whether the next action should be buy or manage.

Important catalog semantics:

  • Public catalog endpoints return only active products and active prices.
  • GET /api/v1/catalog/prices requires product_id.
  • Price metadata, not client-only logic, drives downstream billing-plan, grants, and gate-bundle behavior.

Step 3: create checkout when the user buys

When the user selects a purchasable price, call POST /api/v1/checkouts from the frontend with the bearer token.

Recommended inputs:

  • items[].catalog_price_id as the primary identifier
  • return_url for the allowlisted application return page

Operational expectations:

  • The response returns launch_url; perform a full-page redirect to it.
  • Vluna sends provider success through a bounded return bridge before redirecting to your application.
  • Read billing_checkout_result from the returned URL. success means the local Checkout result is readable, processing means provider success has not finished projecting locally, and cancel means the user canceled Checkout.
  • billing_checkout_id identifies the Checkout for an authenticated GET /api/v1/checkouts/{checkout_id} recovery check when the result is processing.
  • Redirect completion is not entitlement authority. Provider webhooks and reconciliation remain the source of truth.

Conflict behavior matters:

  • If the requested purchase conflicts with an existing non-stackable active subscription, Vluna may return a portal destination instead of a new checkout flow.
  • Your frontend should treat the returned URL as the next billing action, not assume it always represents a new purchase session.

Step 4: create portal when the user manages billing

Use POST /api/v1/portal/sessions when the user wants to:

  • manage an existing subscription
  • update payment method
  • inspect invoices or billing history in the provider-hosted surface

The response returns portal_url. Redirect the user there, then return them to your app with return_url.

Portal is also your fallback path when:

  • checkout detects a subscription conflict
  • the user already has an active recurring relationship
  • the safest next step is customer-managed provider UX instead of creating a second checkout

Step 5: treat webhooks as the finalizer

The self-serve path is finalized by provider observations rather than the browser checkout event.

Your product should assume this lifecycle:

  1. Checkout session is created.
  2. User completes payment in the provider.
  3. Provider sends webhook events to Vluna.
  4. Vluna updates subscriptions, assignments, wallet balances, and related state.
  5. Vluna's return bridge waits for a bounded period, then returns success or processing to your UI.
  6. Your UI refreshes from Vluna APIs and shows the projected state.

This means:

  • do not grant final access based only on redirect success
  • treat processing as an explicit eventual-consistency state and retry the authenticated Checkout read later
  • do poll or refresh the relevant Billing APIs after return

Verify

  • POST /mgt/v1/token/issue returns access_token, billing_account_id, and billing_user_id.
  • GET /api/v1/catalog/products returns the active products visible to the realm.
  • GET /api/v1/catalog/prices returns the expected active prices for a chosen product.
  • POST /api/v1/checkout/sessions returns a hosted URL and the browser can redirect there.
  • POST /api/v1/portal/sessions returns a hosted management URL.
  • After webhook finalization, subscription or wallet state reflects the completed purchase.

Troubleshoot

  • 401 on token/issue: wrong service key or missing service authentication.
  • 401/403 on catalog, checkout, or portal: frontend is using an expired bearer token or insufficient token scopes.
  • Empty catalog: products or prices are not active, or the selected catalog entries are not available to the current integration.
  • Checkout appears successful but product access is unchanged: provider redirect completed, but webhook finalization has not yet updated Vluna state.
  • Checkout sends the user to management instead of purchase: the selected price conflicts with an existing recurring subscription, so portal is the correct next action.

Next