Plans, limits and billing
What an organisation's plan allows, how the API says "not on this plan", and the routes behind the dashboard's Billing page.
When a request is over the plan
Anything that would create something the plan does not cover returns 402 with the problem
type plan_limit_exceeded, and a limit member you can act on:
{
"type": "https://docs.akteora.com/errors/plan_limit_exceeded",
"title": "Plan limit reached",
"status": 402,
"detail": "The Free plan includes 1 brand, and this organization has 1. Upgrade to Starter for 3 brands.",
"request_id": "01J…",
"limit": {
"name": "brands",
"plan": "free",
"allowed": 1,
"requested": 2,
"upgrade_to": "starter",
"upgrade_url": "https://app.akteora.com/billing?upgrade=starter"
}
}
detailis written for a person: show it as it is.limit.nameis for a program. Branch on it, never on the prose.upgrade_urlopens the dashboard's billing page with that plan chosen. Link to it from your own interface; it is behind the owner's sign-in. It isnullwhen no plan raises the limit.payment_past_duemeans a payment is overdue past the 14-day grace period and the account is read-only. The title is "Payment overdue", andupgrade_urlis the billing page, where the payment method is updated.
limit.name |
Refused when | Counted |
|---|---|---|
brands |
creating a brand | brands that exist now |
submissions_per_month |
a respondent opens a link, or starts a testimonial, on Free only; paid plans are unlimited (allowed: null) |
testimonials sent this calendar month (UTC), on every plan |
imported_reviews |
importing reviews | imported reviews kept |
storage_bytes |
the same | every file kept: recordings, renditions, posters, reels |
render_seconds_per_month |
rendering a reel | CPU core-seconds rendered this month; a render minute is 60 |
exports_per_month |
starting an export | exports started this month |
submissions_per_export, media_files_per_export |
starting an export that big | the export's own size |
media_exports |
an MP4, MP3 or ZIP export on Free | not counted |
widget_attribution_removal |
hiding the widget's attribution link on Free | not counted |
payment_past_due |
anything gated, while read-only | not counted |
A 402 never removes anything. A lower plan, or a lapsed one, limits what can be added; every testimonial, brand, widget and public page already there stays, and keeps working.
What a respondent sees
When an organisation cannot take a testimonial (Free's testimonials for the month or its storage are used up, or the account is read-only), its collect links close with the reason paused:
{
"is_open": false,
"reason": "paused",
"message": "This link isn’t taking testimonials at the moment. Please try again later.",
"branding": { "brand_name": "Northwind Coffee", "…": "…" }
}
GET /public/links/{slug} returns it with 410, before anyone records, and creating a
submission is refused the same way. The message says nothing about plans or payments; the
organisation's owners are emailed the reason, once a month.
The plans
GET /v1/billing lists every plan as sold, so you never hard-code a limit:
curl -H "$AUTH" https://api.akteora.com/v1/billing
{
"plan": "starter",
"features": ["widget_attribution_removal", "media_exports"],
"access": "full",
"past_due_since": null,
"read_only_from": null,
"subscription": {
"status": "active",
"plan": "starter",
"current_period_end": "2026-10-01T00:00:00.000Z",
"cancels_at": null
},
"available": true,
"plans": [
{
"plan": "core",
"name": "Agency",
"price_cents": 1900,
"currency": "USD",
"quotas": {
"brands": 15,
"submissions_per_month": null,
"render_seconds_per_month": 3600,
"storage_bytes": 100000000000,
"exports_per_month": 200,
"imported_reviews": 5000
},
"features": ["widget_attribution_removal", "media_exports", "api_access"]
}
]
}
accessisfull,grace(a payment is overdue; everything works untilread_only_from) orread_only.- In
quotas,nullis unlimited and0is not included. Storage is in decimal bytes: 1 GB is 1,000,000,000. - Prices are US dollars a month, in cents, before any sales tax or VAT added at checkout.
Usage
GET /v1/billing/usage gives each quota against the plan: this calendar month for the monthly
ones, and the total now for brands and storage.
{
"plan": "starter",
"period_start": "2026-09-01",
"period_ends_at": "2026-10-01T00:00:00.000Z",
"quotas": [
{
"quota": "submissions_per_month",
"used": 85,
"allowed": null,
"level": "ok",
"resets_at": "2026-10-01T00:00:00.000Z"
}
],
"history": [{ "period_start": "2026-09-01", "submissions": 85, "render_seconds": 126, "exports": 3 }]
}
level is approaching from 80% and reached at 100%. history covers up to twelve months,
newest first.
Paying and changing plan
These three are the organisation owner's alone, and are used by the dashboard with the owner's session. Stripe takes the payment on its own pages; no card or address ever passes through this API.
| Route | Does |
|---|---|
POST /v1/billing/checkout { "plan": "core" } |
Returns { "url" }, a Stripe-hosted checkout. Send the browser there; Stripe returns it to the billing page, and the plan changes when Stripe confirms the payment, usually within seconds. 409 if the organisation already has a subscription |
POST /v1/billing/portal |
Returns { "url" }, Stripe's customer portal for this organisation: the payment method, invoices, cancelling. Short-lived; ask each time |
POST /v1/billing/change-plan { "plan": "plus" } |
Moves the subscription to another paid plan at once, prorated. "free" cancels at the end of the period already paid for. Returns the billing state; read GET /v1/billing again to see the new plan once Stripe confirms |
A server that takes no payments answers checkout and the portal with 409
billing_unavailable, and available is false. If Stripe fails to answer, the response is
502 billing_provider_error, and nothing has been charged.