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.
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);}