Driftstack DRIFTSTACK docs
Docs

Billing

All Driftstack billing is a thin layer over Stripe. The Driftstack API mints checkout sessions + portal URLs; the customer interacts with the Stripe-hosted UI directly. Driftstack receives webhook events from Stripe (invoice.paid, customer.subscription.updated, etc.) and reflects them into the account’s subscription row + audit-log + email notifications.

Read billing state

GET /v1/billing

const state = await client.billing.getState();

Returns:

{
  "subscription": {
    "tier": "api_builder",
    "status": "active",
    "stripe_subscription_id": "sub_<stripe-id>",
    "current_period_end": "2026-06-01T00:00:00Z",
    "cancel_at_period_end": false,
    "canceled_at": null,
    "created_at": "2026-05-01T00:00:00Z",
    "updated_at": "2026-05-01T00:00:00Z"
  }
}

subscription is null when the account has never subscribed. The id is stripe_subscription_id (the live Stripe-side id, prefix sub_); Driftstack does not mint its own subscription id alongside.

status follows Stripe’s subscription-status vocabulary (active, trialing, past_due, canceled, incomplete, incomplete_expired, unpaid, paused). canceled_at is the Stripe cancellation timestamp — non-null only when the subscription has been cancelled; cancel_at_period_end=true is distinct (cancellation is scheduled but not yet effective).

Start a subscription

POST /v1/billing/checkout-session

{
  "tier": "api_builder",
  "billing_period": "monthly",
  "success_url": "https://your.app/billing/success?session_id={CHECKOUT_SESSION_ID}",
  "cancel_url": "https://your.app/billing/cancel"
}

Returns { checkout_url, checkout_session_id }. Redirect the customer to checkout_url; Stripe handles card collection + 3DS + tax compliance and posts the result back to your success_url.

success_url and cancel_url are validated against an allowlist . Customers self-hosting Driftstack configure the allowlist in their deployment env.

Open the Stripe Customer Portal

POST /v1/billing/portal-session

const { portal_url } = await client.billing.createPortalSession();

Returns a short-lived one-time URL into Stripe’s hosted Customer Portal. The customer manages their payment method, downloads invoices, cancels, or upgrades / downgrades from the portal. Driftstack receives the resulting Stripe events via webhook + the account’s subscription row updates.

The portal URL is single-use and short-lived. Mint a fresh one each time the customer clicks “Manage subscription” — don’t cache.

Webhook events from Stripe → Driftstack → Customer

When Stripe fires customer.subscription.updated (or any of the ~10 lifecycle events Driftstack subscribes to), Driftstack records the change in the account’s audit log (subscription.tier_changed with payload.from + payload.to) and optionally fires a customer-facing webhook event.

See Webhook events catalog for the planned subscription.changed / subscription.cancelled events; today the subscription.tier_changed audit row is the source of truth for programmatic consumers.

Auth + scoping

All /v1/billing/* endpoints are bearer-authenticated and scoped to the calling account, with one read exception: GET /v1/billing honors the team-RBAC X-Driftstack-Account header, so a team member acting as the owner reads the OWNER’s subscription state (tier, status, period end) — the same act-as behavior as GET /v1/usage. The mutation endpoints (checkout-session, portal-session, billing-portal) do NOT honor the header — only the owner manages the owner’s billing.

Reading billing state (GET /v1/billing) requires the read:billing scope — a broad read or account_owner key (the dashboard’s web-session scope set) also satisfies it, but a write-only key is refused with 403; mutation endpoints (checkout, manage-portal) require the admin:billing scope (a broad admin or account_owner key also satisfies it).