Billing API
Three endpoints let agents and integrations work with the billing state your dashboard manages.
All endpoints require a Bearer API key (see Authentication).
GET /me/billing/state
Returns a snapshot of your tenant’s subscription.
curl https://api.saiku.bi/me/billing/state \ -H "Authorization: Bearer $SAIKU_API_KEY"Response:
{ "tenantId": "81e301f2-…", "tier": "team", "stripeCustomerId": "cus_…", "subscriptionStatus": "active", "currentPeriodEnd": "2026-06-23T00:00:00Z", "trialEndsAt": null, "billedFeaturesActive": true}Fields:
tier—starter,team,business, orenterprise.subscriptionStatus— Stripe’s status verbatim. The values you’ll see in practice aretrialing,active,past_due,unpaid,canceled, or empty (free / pre-billing).currentPeriodEnd— when your current billing period ends. Renewal happens at this point if active.trialEndsAt— set only whilesubscriptionStatus = trialing.billedFeaturesActive—truewhile you have access to billed features. False otherwise. Use this to gate your own UI.
Two shapes, not one
A tenant that has never started a subscription has no billing record behind it, and the response is shorter — it carries only:
{ "tenantId": "81e301f2-…", "tier": "starter", "subscriptionStatus": "", "billedFeaturesActive": false}stripeCustomerId, currentPeriodEnd and trialEndsAt are
absent, not null. If your client reads state.currentPeriodEnd
expecting a null, it gets undefined instead — so branch on
billedFeaturesActive (or on the key’s presence) rather than
assuming every field is always there.
When billing isn’t configured
If the gateway has no Stripe key wired, every endpoint on this page
returns 503:
{ "error": "billing_disabled", "message": "Stripe billing is not configured on this gateway."}You’ll see this on self-hosted builds and local stacks, where it’s
expected. On api.saiku.bi it isn’t — if you hit it there, tell us.
POST /me/billing/checkout-session
Mints a one-time URL to Stripe’s hosted Checkout for starting a trial or signing up for a plan.
curl -X POST https://api.saiku.bi/me/billing/checkout-session \ -H "Authorization: Bearer $SAIKU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tier": "team", "email": "alice@acme.com", "name": "Acme Corp", "successUrl": "https://app.acme.com/billing/success", "cancelUrl": "https://app.acme.com/billing/cancel" }'Response:
{ "url": "https://checkout.stripe.com/c/pay/cs_test_a1…", "sessionId": "cs_test_a1…"}Send the user’s browser to url via a 303 redirect. The URL is
single-use and expires after a few minutes — don’t cache it.
successUrl and cancelUrl are absolute URLs Stripe sends the user
back to after completing or abandoning Checkout. Both must be
HTTPS.
POST /me/billing/portal-session
Mints a one-time URL to Stripe’s Customer Portal. The Portal is Stripe’s hosted page for managing the subscription: update card, change plan, download invoices, cancel.
curl -X POST https://api.saiku.bi/me/billing/portal-session \ -H "Authorization: Bearer $SAIKU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "returnUrl": "https://app.acme.com/billing" }'Response:
{ "url": "https://billing.stripe.com/p/session/test_…" }Returns 409 no_customer if the tenant has never completed
Checkout — there’s no Stripe customer to manage. Run
POST /me/billing/checkout-session first.
A worked example — embedded subscription state
If you’re building an admin UI that wraps Saiku Cloud and want to show your users their current plan inline:
// pseudocodeconst state = await fetch('https://api.saiku.bi/me/billing/state', { headers: { Authorization: `Bearer ${saikuApiKey}` }}).then(r => r.json());
if (!state.billedFeaturesActive) { // user has no active subscription or trial — show "upgrade" CTA const session = await fetch( 'https://api.saiku.bi/me/billing/checkout-session', { method: 'POST', headers: { Authorization: `Bearer ${saikuApiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ tier: 'team', email: user.email, name: org.name, successUrl: `${window.location.origin}/billing/success`, cancelUrl: `${window.location.origin}/billing/cancel` }) } ).then(r => r.json()); window.location.href = session.url;} else { // user has access — show current plan + manage button renderPlan(state.tier, state.currentPeriodEnd); const portal = await fetch( 'https://api.saiku.bi/me/billing/portal-session', { method: 'POST', headers: { Authorization: `Bearer ${saikuApiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ returnUrl: `${window.location.origin}/billing` }) } ).then(r => r.json()); renderManageButton(portal.url);}