Billing
Esta página aún no está disponible en tu idioma.
The billing resource exposes your tenant’s subscription state, plan catalog, usage counters, and write endpoints for initiating Stripe Checkout sessions or opening the Stripe Customer Portal. Writes use the owner-gated billing routes and require an API key with billing.write. API access requires the Business plan (scale) and an environment where the public API is enabled.
Stripe IDs not exposed, internal Stripe price IDs, customer IDs, and session IDs are not surfaced. Use the
urlfield from checkout/portal responses to redirect the user.
Plan names and API codes
Section titled “Plan names and API codes”The API retains these plan codes. Send the code in requests and use the name for display.
starter: Standard. $29 a month. 1 person, 1 store, 75 new tickets a month (imported tickets don’t count). No API access.pro: Pro. $79 a month. Up to 8 people, 1 store, unlimited tickets. No API access.scale: Business. $149 a month for the first store and $79 for each extra store. Unlimited people and unlimited tickets, and every store gets everything in Business. API access.
These USD list prices are from the US public pricing feed. Prices are monthly. Pricing can vary by country and account offer. Founders pricing is for the first 50 shops that subscribe after the beta in eligible regions while the founders window is open: $59/month for Pro, or $99/month for the first Business store plus $59 for each extra store; Standard has no founders price. Read the feed’s foundersWindow for current availability.
Self-serve trials default to 14 days on Business (scale), with no credit card. Trial length and plan can be configured; use the returned trial state for the account’s actual dates. An invited beta grant provides 365 days on Business free from account creation, then half of list price on the chosen plan for the life of the account.
More than one store needs Business. All billable locations use the workspace’s plan; warehouses are not billed as stores. There are no customer-count caps in the plan limits below. null means no fixed numeric cap, not -1.
Retrieve billing status
Section titled “Retrieve billing status”GET /api/v1/billingReturns the tenant’s subscription state: current plan, effective plan, entitlements, trial status, and the active subscription summary. If billing is not configured, the response can have configured: false and null plan/subscription fields. A failure to read billing authority can return 503; handle it as unavailable rather than treating the account as unconfigured.
Scope required: billing.read
curl "https://app.benchkey.com/api/v1/billing" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/billing", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },});const billing = await res.json();Response
Section titled “Response”Example for an active Business subscription. Dates and usage counts on this page are illustrative.
{ "object": "billing", "configured": true, "plan": "scale", "status": "active", "effective_plan": "scale", "grandfathered": false, "subscription": { "plan": "scale", "status": "active", "current_period_end": "2026-10-24T00:00:00.000Z", "cancel_at_period_end": false }, "trial": null, "entitlements": { "plan": "scale", "status": "active", "grandfathered": false, "features": { "tickets": true, "customers": true, "invoices": true, "inventory_basic": true, "pos_quicksale": true, "appointments": true, "kiosk": true, "reports_basic": true, "notifications": true, "settings": true, "payment_integrations": true, "configure_chat": true, "leads": true, "buy_sell": true, "time_clock": true, "integrations": true, "automation": true, "reports_advanced": true, "ai_features": true, "multi_location": true, "customer_plans": true, "campaigns": true, "customer_portal": true, "compensation": true, "b2b_asset_tracking": true, "mailin_shipping": true, "api_access": true, "shift_scheduling": true } }}| Field | Type | Description |
|---|---|---|
object | string | Always "billing" |
configured | boolean | Whether billing is set up for this tenant |
plan | string|null | The raw subscription plan key |
status | string|null | Stored subscription status, for example active, trialing, past_due, or canceled |
effective_plan | string|null | The plan whose limits are actually enforced (may differ from plan if grandfathered) |
grandfathered | boolean | Whether the tenant has a grandfathered entitlement override; this is separate from founders pricing |
subscription | object|null | Active subscription summary; null if no subscription |
trial | object|null | Synthetic trial metadata (onTrial, expired, endsAt, daysLeft, plan); null for other subscription rows, including Stripe-managed trials |
entitlements | object | Resolved entitlements including feature flags |
Retrieve usage
Section titled “Retrieve usage”GET /api/v1/billing/usageReturns usage counters and effective limits. tickets excludes hidden, soft-deleted, and billing-only tickets. locations counts active locations, including warehouses, so it is not a billable-store total. Pro’s users limit is 8 and its locations limit is 1; Business has no numeric users cap.
Scope required: billing.read
curl "https://app.benchkey.com/api/v1/billing/usage" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/billing/usage", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },});const usage = await res.json();Response
Section titled “Response”{ "object": "billing_usage", "effective_plan": "scale", "limits": { "users": null, "locations": null }, "usage": { "users": 5, "locations": 2, "tickets": 1842, "invoices": 1200, "leads": 38 }, "usage_status": { "users": { "used": 5, "limit": null, "remaining": null, "within_limit": true }, "locations": { "used": 2, "limit": null, "remaining": null, "within_limit": true }, "tickets": { "used": 1842, "limit": null, "remaining": null, "within_limit": true }, "invoices": { "used": 1200, "limit": null, "remaining": null, "within_limit": true }, "leads": { "used": 38, "limit": null, "remaining": null, "within_limit": true } }}| Field | Type | Description |
|---|---|---|
object | string | Always "billing_usage" |
effective_plan | string|null | The plan whose limits are being measured |
limits | object | Enforced numeric caps for users and locations; null means no fixed cap |
usage | object | Counts for users, locations, tickets, invoices, and leads; a count can be null when unavailable |
usage_status | object | Per-dimension used, limit, remaining, and within_limit; uncapped dimensions have limit: null, remaining: null, and within_limit: true |
List plans
Section titled “List plans”GET /api/v1/billing/plansReturns the server-side plan catalog, pricing tiers, limits, and feature flags. Use this to build an upgrade UI without hard-coding plan details in your client.
Scope required: billing.read
curl "https://app.benchkey.com/api/v1/billing/plans" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/billing/plans", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },});const { plans } = await res.json();Response
Section titled “Response”{ "object": "billing_plan_catalog", "configured": true, "plans": [ { "key": "starter", "name": "Standard", "price": { "monthlyCents": 2900, "currency": "usd", "interval": "month", "display": "$29/mo" }, "limits": { "users": 1, "locations": 1 }, "features": { "tickets": true, "customers": true, "invoices": true, "inventory_basic": true, "pos_quicksale": true, "appointments": true, "kiosk": true, "reports_basic": true, "notifications": true, "settings": true, "payment_integrations": true, "configure_chat": true, "leads": false, "buy_sell": false, "time_clock": false, "integrations": false, "automation": false, "reports_advanced": false, "ai_features": false, "multi_location": false, "customer_plans": false, "campaigns": false, "customer_portal": false, "compensation": false, "b2b_asset_tracking": false, "mailin_shipping": false, "api_access": false, "shift_scheduling": false } }, { "key": "pro", "name": "Pro", "price": { "monthlyCents": 7900, "currency": "usd", "interval": "month", "display": "$79/mo" }, "founders": { "monthlyCents": 5900, "currency": "usd", "interval": "month", "display": "$59/mo" }, "limits": { "users": 8, "locations": 1 }, "features": { "tickets": true, "customers": true, "invoices": true, "inventory_basic": true, "pos_quicksale": true, "appointments": true, "kiosk": true, "reports_basic": true, "notifications": true, "settings": true, "payment_integrations": true, "configure_chat": true, "leads": true, "buy_sell": true, "time_clock": true, "integrations": true, "automation": true, "reports_advanced": true, "ai_features": true, "multi_location": false, "customer_plans": true, "campaigns": true, "customer_portal": true, "compensation": true, "b2b_asset_tracking": false, "mailin_shipping": false, "api_access": false, "shift_scheduling": true } }, { "key": "scale", "name": "Business", "price": { "monthlyCents": 14900, "currency": "usd", "interval": "month", "display": "$149/mo" }, "founders": { "monthlyCents": 9900, "currency": "usd", "interval": "month", "display": "$99/mo" }, "limits": { "users": null, "locations": null }, "features": { "tickets": true, "customers": true, "invoices": true, "inventory_basic": true, "pos_quicksale": true, "appointments": true, "kiosk": true, "reports_basic": true, "notifications": true, "settings": true, "payment_integrations": true, "configure_chat": true, "leads": true, "buy_sell": true, "time_clock": true, "integrations": true, "automation": true, "reports_advanced": true, "ai_features": true, "multi_location": true, "customer_plans": true, "campaigns": true, "customer_portal": true, "compensation": true, "b2b_asset_tracking": true, "mailin_shipping": true, "api_access": true, "shift_scheduling": true } } ]}This example shows the US catalog while founders pricing is available to the account. price.monthlyCents and founders.monthlyCents are integer cents, not whole dollars. founders is optional. Feature flags come from the server; api_access and multi_location are true only for Business; shift_scheduling is true for Pro and Business. There is no separate webhooks flag in this catalog.
Internal Stripe price IDs are stripped. This v1 catalog exposes key, name, price, optional founders, limits, and features. It does not expose the public pricing feed’s foundersWindow field. The v1 checkout endpoint does not accept an interval field.
Retrieve effective limits
Section titled “Retrieve effective limits”GET /api/v1/billing/limitsReturns the enforced limits for the tenant’s effective plan, plus the tenant-aware plan catalog. Catalog limits are per-plan base limits: Pro allows 8 people and one store. Grandfathered entitlement overrides can also affect the effective plan.
Scope required: billing.read
curl "https://app.benchkey.com/api/v1/billing/limits" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Response
Section titled “Response”{ "object": "billing_limits", "effective_plan": "scale", "limits": { "users": null, "locations": null }, "plans": [ { "key": "starter", "name": "Standard", "price": { "monthlyCents": 2900, "currency": "usd", "interval": "month", "display": "$29/mo" }, "limits": { "users": 1, "locations": 1 }, "features": { "tickets": true, "customers": true, "invoices": true, "inventory_basic": true, "pos_quicksale": true, "appointments": true, "kiosk": true, "reports_basic": true, "notifications": true, "settings": true, "payment_integrations": true, "configure_chat": true, "leads": false, "buy_sell": false, "time_clock": false, "integrations": false, "automation": false, "reports_advanced": false, "ai_features": false, "multi_location": false, "customer_plans": false, "campaigns": false, "customer_portal": false, "compensation": false, "b2b_asset_tracking": false, "mailin_shipping": false, "api_access": false, "shift_scheduling": false } }, { "key": "pro", "name": "Pro", "price": { "monthlyCents": 7900, "currency": "usd", "interval": "month", "display": "$79/mo" }, "founders": { "monthlyCents": 5900, "currency": "usd", "interval": "month", "display": "$59/mo" }, "limits": { "users": 8, "locations": 1 }, "features": { "tickets": true, "customers": true, "invoices": true, "inventory_basic": true, "pos_quicksale": true, "appointments": true, "kiosk": true, "reports_basic": true, "notifications": true, "settings": true, "payment_integrations": true, "configure_chat": true, "leads": true, "buy_sell": true, "time_clock": true, "integrations": true, "automation": true, "reports_advanced": true, "ai_features": true, "multi_location": false, "customer_plans": true, "campaigns": true, "customer_portal": true, "compensation": true, "b2b_asset_tracking": false, "mailin_shipping": false, "api_access": false, "shift_scheduling": true } }, { "key": "scale", "name": "Business", "price": { "monthlyCents": 14900, "currency": "usd", "interval": "month", "display": "$149/mo" }, "founders": { "monthlyCents": 9900, "currency": "usd", "interval": "month", "display": "$99/mo" }, "limits": { "users": null, "locations": null }, "features": { "tickets": true, "customers": true, "invoices": true, "inventory_basic": true, "pos_quicksale": true, "appointments": true, "kiosk": true, "reports_basic": true, "notifications": true, "settings": true, "payment_integrations": true, "configure_chat": true, "leads": true, "buy_sell": true, "time_clock": true, "integrations": true, "automation": true, "reports_advanced": true, "ai_features": true, "multi_location": true, "customer_plans": true, "campaigns": true, "customer_portal": true, "compensation": true, "b2b_asset_tracking": true, "mailin_shipping": true, "api_access": true, "shift_scheduling": true } } ]}List per-location seat plans
Section titled “List per-location seat plans”GET /api/v1/billing/location-plansReturns stored seat assignments for active locations. Deactivated locations are filtered out and do not affect counts. This is a read-only summary, not support for choosing different plans per location: every billable location uses the workspace plan. Accounts without seat rows return an empty seats array, zero counts, and highest_plan: null.
Scope required: billing.read
curl "https://app.benchkey.com/api/v1/billing/location-plans" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Response
Section titled “Response”{ "object": "billing_location_plans", "highest_plan": "scale", "counts": { "pro": 0, "scale": 2 }, "seats": [ { "location_id": 1, "plan": "scale" }, { "location_id": 2, "plan": "scale" } ]}| Field | Type | Description |
|---|---|---|
highest_plan | string|null | The highest plan among all active seat assignments (Business "scale" > Pro "pro") |
counts | object | Count of active seats per plan key |
seats | array | Per-location seat assignments (active locations only) |
Start a Checkout session
Section titled “Start a Checkout session”POST /api/v1/billing/checkoutFor a tenant starting a subscription, creates a Stripe Checkout session and returns its redirect URL. An existing subscription is never redirected to a Portal plan switcher. A refused plan change returns 409 (including plan_change_unavailable when self-service changes are unavailable), with no redirect.
The billing backend can also apply an eligible existing-subscriber upgrade or schedule a downgrade. The current v1 response reduces a successful backend result without a redirect to { "object": "billing_checkout_session", "url": null, "portal": false }; it does not expose the backend’s applied/scheduled fields. Re-read billing status and use Settings → Billing & Payments → Plan & Billing to review the change. Do not assume every 200 contains a Checkout URL.
Send an Idempotency-Key header and reuse the same key and body when retrying the same request. Create a new key only for a new intentional operation.
Scope required: billing.write (owner-only)
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
plan | yes | Target plan code: "starter" (Standard), "pro" (Pro), or "scale" (Business) |
success_url | no | Accepted and validated as an absolute http(s):// URL; the backend uses its own return URL instead |
cancel_url | no | Accepted and validated as an absolute http(s):// URL; the backend uses its own return URL instead |
The backend returns users to the workspace’s Settings → Billing & Payments → Plan & Billing page. successUrl and cancelUrl are accepted aliases; if both forms are supplied they must match. Unknown fields, including interval, are rejected.
# Generate once and retain this key for retries of this request.checkout_key="checkout-$(uuidgen)"curl -X POST "https://app.benchkey.com/api/v1/billing/checkout" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $checkout_key" \ -d '{ "plan": "scale" }'Node.js
Section titled “Node.js”// Retain this key if the same request needs to be retried.const checkoutKey = crypto.randomUUID();const res = await fetch("https://app.benchkey.com/api/v1/billing/checkout", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", "Idempotency-Key": checkoutKey, }, body: JSON.stringify({ plan: "scale" }),});const result = await res.json();if (!res.ok) throw new Error(result.error?.message || "Checkout failed");if (result.url) { // Redirect the user to result.url.} else { // Re-read billing status and review the change in Plan & Billing.}Response
Section titled “Response”{ "object": "billing_checkout_session", "url": "https://checkout.stripe.com/c/pay/cs_live_example", "portal": false}| Field | Type | Description |
|---|---|---|
url | string|null | The Checkout URL for a new session; null when the backend returned no redirect |
portal | boolean | Compatibility boolean copied from the backend; currently false for these Checkout responses |
Open the Customer Portal
Section titled “Open the Customer Portal”POST /api/v1/billing/portalReturns a Stripe Customer Portal URL for payment methods, invoice history, and cancellation. Plan switching is disabled in the Portal. Omit the request body or send an empty JSON object.
Scope required: billing.write (owner-only)
curl -X POST "https://app.benchkey.com/api/v1/billing/portal" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/billing/portal", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },});const { url } = await res.json();// Redirect the user to `url`Response
Section titled “Response”{ "object": "billing_portal_session", "url": "https://billing.stripe.com/p/session/live_example", "portal": false}The current response has portal: false because the backend returns only url. Use the endpoint and url to identify this as a Portal session; do not use that compatibility flag to infer the destination.
Assign a location seat plan (retired)
Section titled “Assign a location seat plan (retired)”POST /api/v1/billing/location-planThis write endpoint is retired. Every billable location uses the workspace’s main plan. Manage location counts in Settings → Business → Locations; use Settings → Billing & Payments → Plan & Billing for the workspace plan.
Scope required: billing.write (owner-gated billing operation)
The v1 route still validates a JSON object with location_id (a positive integer no greater than 2,147,483,647; locationId is an alias) and plan ("pro" or "scale"). A valid request reaches the retired internal /api/billing/location-plan route, which returns 410 location_plan_retired without changing anything. The current v1 billing error mapper translates that internal response to 422 billing_unprocessable. Do not retry it as a plan assignment.
Response
Section titled “Response”Public v1 response (422); request_id varies:
{ "error": { "type": "unprocessable_error", "code": "billing_unprocessable", "message": "Per-location plan levels have been retired \u2014 every location rides your main plan. Change your plan from Settings → Plan & Billing; locations are billed automatically as you add them.", "request_id": "req_example" }}Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | missing_field | Checkout plan is absent or empty |
400 | invalid_plan | plan is not one of the accepted values |
400 | invalid_url | success_url or cancel_url is not a valid http(s):// URL |
400 | invalid_location_id | location_id is missing or is not a positive integer at most 2,147,483,647 |
400 | invalid_body / unknown_field / alias_conflict | Invalid JSON body shape, unsupported field, or conflicting aliases |
400 | billing_invalid_request | The billing backend rejected the request |
402 | upgrade_required | API access requires Business (scale) |
402 | billing_subscription_required | An active subscription is required for this operation |
403 | billing_forbidden | The underlying billing operation is not permitted for this tenant |
403 | insufficient_scope | API key lacks billing.read or billing.write |
404 | not_found | No billing customer found for this tenant |
409 | plan_change_unavailable | Self-service plan changes are unavailable; no Portal redirect is created |
409 | starter_multi_location | Deactivate extra billable locations before choosing Standard |
409 | plan_stores_over_limit | Pro covers one store: deactivate the extra stores or choose Business |
409 | billing_conflict | Another backend billing conflict, including a same-plan request or billing identity/country issue |
422 | billing_unprocessable | The retired location-plan write (internal 410) or another unprocessable billing request |
503 | billing_projection_authority_unavailable | Billing status or a public-safe usage/location projection could not be read |
503 | service_unavailable | Billing is not configured or temporarily unavailable |
See Errors for the full error envelope format.