Skip to content

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.

Terminal window
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:

  • tierstarter, team, business, or enterprise.
  • subscriptionStatus — Stripe’s status verbatim. The values you’ll see in practice are trialing, 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 while subscriptionStatus = trialing.
  • billedFeaturesActivetrue while 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.

Terminal window
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.

Terminal window
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:

// pseudocode
const 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);
}