Billing
Payment method, VAT number, invoices, billing address, subscription tier, Stripe Connect payouts, and the console's Usage page (the organization's live charger fleet against its tier's licensed limit). Stripe integration lives entirely behind a separate billing service; this process never holds Stripe key material and only ever relays what that service already returned. User reads, Admin manages the payment method, VAT number, address, plan and payout onboarding.
14 endpoints
Returns the billing address filed for Stripe Tax, if any.
get/v1/organizations/{organizationId}/billing/addressRequires a bearer token
Any member may call this. 204 with no body means no address has been
filed yet.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 200 | BillingAddressBody | The filed billing address |
| 204 | — | No billing address filed |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Replaces the whole billing address.
put/v1/organizations/{organizationId}/billing/addressRequires a bearer token
Requires Owner or Admin. Stripe Tax needs this on file before a Pro
subscription can be created (see PUT /organizations/{organizationId}/billing/subscription)
— 422 if country is not a valid ISO 3166-1 alpha-2 code or a
required line is empty.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Request bodyBillingAddressBody
| Field | Type | Description |
|---|
| city* | string | — |
| country* | string | ISO 3166-1 alpha-2, upper case — billing refuses anything else
(422). |
| line1* | string | — |
| line2 | string | null | — |
| name* | string | The legal/billing name printed on invoices. |
| postalCode* | string | — |
Responses
| Status | Body | Description |
|---|
| 200 | BillingAddressBody | The address as now filed |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 403 | ErrorBody | Not an Owner/Admin |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 422 | ErrorBody | A non-ISO-3166 country or an empty required line |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Lists the organization's invoices, newest first.
get/v1/organizations/{organizationId}/billing/invoicesRequires a bearer token
Any member of the organization may call this. Draft invoices are
excluded. Cursor-paginated: pass the previous page's nextCursor back
as cursor to fetch the next page; omit cursor for the first page.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
| limit | query | integer<int32> | Invoices per page, capped at 100. Defaults to 24. A larger value is
clamped to 100 rather than rejected. |
| cursor | query | string | An opaque cursor from a previous page's nextCursor response
field; omit for the first page. This is Stripe's own cursor value,
round-tripped unchanged — never parse or construct it yourself. |
Responses
| Status | Body | Description |
|---|
| 200 | InvoicesResponse | The organization's invoices, newest first |
| 400 | ErrorBody | An unrecognised or malformed query parameter |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Returns the organization's default card, if any.
get/v1/organizations/{organizationId}/billing/payment-methodRequires a bearer token
Any member of the organization may call this — reading which card bills
the organization is not privileged. 204 with no body means no card is
on file yet; a 200 response never carries "card": null.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 200 | PaymentMethodResponse | The organization's default card |
| 204 | — | No card on file |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Promotes a confirmed SetupIntent to the organization's default card.
put/v1/organizations/{organizationId}/billing/payment-methodRequires a bearer token
Requires Owner or Admin. The SetupIntent must have already succeeded
client-side — 422 if it has not, and 403 if it belongs to a
different organization's Stripe customer.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Request bodyConfirmPaymentMethodRequest
| Field | Type | Description |
|---|
| setupIntentId* | string | The Stripe SetupIntent id — Stripe's own value, round-tripped
unchanged. |
Responses
| Status | Body | Description |
|---|
| 200 | PaymentMethodResponse | The now-default card |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 403 | ErrorBody | Not an Owner/Admin, or the SetupIntent belongs to a different organization |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 422 | ErrorBody | The SetupIntent has not succeeded yet |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Returns the organization's Stripe Connect status and payout fee rate.
get/v1/organizations/{organizationId}/billing/payoutsRequires a bearer token
Any member of the organization may call this. status is not_started
before onboarding has begun.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 200 | PayoutsResponse | The organization's connected-account status and fee rate |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Requests (or resumes) Stripe Connect onboarding for driver payouts.
post/v1/organizations/{organizationId}/billing/payouts/onboarding-linkRequires a bearer token
Requires Owner or Admin. Creates the Standard account on the first
call; every call — including a later one against an already-onboarded
account — returns a fresh, single-use, short-lived Stripe-hosted link.
returnUrl/refreshUrl must be absolute https:// URLs, since Stripe
redirects the calling Admin's own browser there.
Not idempotent. Returns 200, not 201, and names the operation
request_onboarding_link rather than a create_* id deliberately:
nothing addressable is created at a URI this API owns — the connected
account itself is reachable only through
GET /organizations/{organizationId}/billing/payouts, and the link
this returns is single-use and expires within minutes, so there is
nothing to point a Location header at.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Request bodyCreateOnboardingLinkRequest
| Field | Type | Description |
|---|
| refreshUrl* | string | Where Stripe sends the admin if the link expired. Absolute
https:// only. |
| returnUrl* | string | Where Stripe sends the admin when done. Absolute https:// only. |
Responses
| Status | Body | Description |
|---|
| 200 | OnboardingLinkResponse | A single-use Stripe-hosted onboarding link |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 403 | ErrorBody | Not an Owner/Admin |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 422 | ErrorBody | invalidUrl: returnUrl/refreshUrl is not an absolute https:// URL |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Begins adding or replacing the organization's payment method.
post/v1/organizations/{organizationId}/billing/setup-intentRequires a bearer token
Requires Owner or Admin. Lazily creates the organization's Stripe
Customer on first call, then returns a SetupIntent's clientSecret for
the caller to confirm client-side with Stripe.js — this route never
sees card data itself. Call PUT .../payment-method with the confirmed
SetupIntent's id to promote it to the default card.
Not idempotent: each call mints a new SetupIntent, and this route
accepts no Idempotency-Key. Returns 200, not 201, and names the
operation begin_payment_method_setup rather than a create_* id
deliberately: the SetupIntent this returns is not a resource this API
exposes at any URI — there is no GET-by-id route for it, and Stripe
discards the object once it is confirmed — so there is nothing
addressable to point a Location header at.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 200 | SetupIntentResponse | A SetupIntent to confirm client-side |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 403 | ErrorBody | The caller is a member but not an Owner or Admin |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Returns the organization's current subscription, tier, and entitlements.
get/v1/organizations/{organizationId}/billing/subscriptionRequires a bearer token
Any member of the organization may call this. An organization with no
subscription row is always answered as Starter, with the Starter
Product's entitlements — this never answers 404 for "no subscription
yet".
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 200 | SubscriptionResponse | The organization's current subscription |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Moves the organization between the Starter and Pro tiers.
put/v1/organizations/{organizationId}/billing/subscriptionRequires a bearer token
Requires Owner or Admin. Enterprise is never self-serve — any tier
outside "pro"/"starter" is 422 invalidTier before billing is
ever asked. Upgrading to Pro requires a card and a billing address on
file (422 notReady with message paymentMethodRequired or
billingAddressRequired otherwise); downgrading sets
cancelAtPeriodEnd rather than cancelling immediately.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Request bodyChangePlanRequest
| Field | Type | Description |
|---|
| tier* | string | pro | starter. |
Responses
| Status | Body | Description |
|---|
| 200 | SubscriptionResponse | The subscription after the change |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 403 | ErrorBody | Not an Owner/Admin |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 422 | ErrorBody | invalidTier, or notReady with message paymentMethodRequired/billingAddressRequired |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Returns the organization's filed VAT number, if any.
get/v1/organizations/{organizationId}/billing/tax-idRequires a bearer token
Any member of the organization may call this. 204 with no body means
none is filed.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 200 | TaxIdResponse | The filed VAT number |
| 204 | — | No VAT number filed |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Files (replacing any previous) a VAT number.
put/v1/organizations/{organizationId}/billing/tax-idRequires a bearer token
Requires Owner or Admin. The tax-id type (eu_vat, gb_vat, no_vat,
ch_vat, and so on) is derived server-side from the value, never
supplied by the caller — 422 if the registry or Stripe refuses the
number.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Request bodySetTaxIdRequest
| Field | Type | Description |
|---|
| value* | string | — |
Responses
| Status | Body | Description |
|---|
| 200 | TaxIdResponse | The filed VAT number |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 403 | ErrorBody | Not an Owner/Admin |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 422 | ErrorBody | The registry or Stripe refused the number |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Detaches the filed VAT number.
delete/v1/organizations/{organizationId}/billing/tax-idRequires a bearer token
Requires Owner or Admin. A no-op (still 204) if none was filed.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 204 | — | Detached |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 403 | ErrorBody | Not an Owner/Admin |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |
Returns the organization's live charger fleet against its tier's licensed limit.
get/v1/organizations/{organizationId}/billing/usageRequires a bearer token
Any member of the organization may read this — it is a read of the
organization's own fleet against its own tier, not a management action.
acChargers/dcChargers/environments are counted live, so an
operator who was just refused at charger creation for being over the
limit sees that same number here rather than a stale one. chargerLimit
belongs to the organization as a whole; an absent value means uncapped,
never 0.
Parameters
| Name | In | Type | Description |
|---|
| organizationId* | path | string<uuid> | Organization id |
Responses
| Status | Body | Description |
|---|
| 200 | UsageResponse | The organization's live charger fleet against its tier's licensed limit |
| 401 | ErrorBody | Missing, invalid, or expired bearer token |
| 404 | ErrorBody | billingNotConfigured: no billing backend is configured for this deployment, or it has no Stripe secret key configured. Any other billing failure is a 500, not this. |
| 429 | ErrorBody | The caller has exceeded its request budget. error is rateLimited. Wait Retry-After seconds before retrying — a prior successful response's own RateLimit-Remaining/RateLimit-Reset headers show this coming before it happens. |
| 500 | ErrorBody | An unexpected internal error. error is always internalError; message is deliberately the fixed string "an unexpected error occurred", never the real cause, which is logged server-side instead of being returned to the caller. |