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 | testimonials sent this calendar month (UTC) |
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 | — |
widget_attribution_removal |
hiding the widget's attribution link on Free | — |
payment_past_due |
anything gated, while read-only | — |
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 — the month's testimonials or its storage used
up, or the account 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": "Core",
"price_cents": 7900,
"currency": "USD",
"quotas": {
"brands": 15,
"submissions_per_month": 500,
"render_seconds_per_month": 3600,
"storage_bytes": 100000000000,
"exports_per_month": 200
},
"features": ["widget_attribution_removal", "media_exports", "api_access", "custom_domain"]
}
]
}
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. Paddle adds sales tax or VAT where it applies.
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": 100,
"level": "approaching",
"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. Paddle, the merchant of record, takes the payment; no card or address ever passes through this API.
| Route | Does |
|---|---|
POST /v1/billing/checkout { "plan": "core" } |
Returns { "url" }, a Paddle-hosted checkout. Send the browser there; Paddle returns it to the billing page, and the plan changes when Paddle confirms the payment, usually within seconds. 409 if the organisation already has a subscription |
POST /v1/billing/portal |
Returns { "url" }, Paddle'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 Paddle confirms |
A server that takes no payments answers checkout and the portal with 409
billing_unavailable, and available is false. If Paddle fails to answer, the response is
502 billing_provider_error, and nothing has been charged.