Manage subscriptions, invoices, payment methods, checkout sessions, credit packs, pricing plans, API keys, Stripe Connect profiles, and academic verification flows for Backbuild and Backbuild Science.
GET /v1/academic-verifications
List academic verifications (reviewer queue)
Returns the org-scoped reviewer queue of pending and submitted academic verifications. Filtered by optional `status` and paginated by `page`/`page_size`.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
page | query | integer | no | 1-based page number. |
page_size | query | integer | no | Records per page (1 to 100). |
status | query | string (enum) | no | Filter by verification status. |
Responses
| Status | Description |
200 | Paginated reviewer queue. |
400 | 400 Bad Request: a request parameter, body field, or the JSON body itself failed validation. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
GET /v1/academic-verifications/{id}
Get an academic verification
Returns a single academic verification by id. Accessible by the subject user or a reviewer.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
id | path | string<uuid> | yes | Academic verification identifier (UUIDv7). |
Responses
| Status | Description |
200 | Academic verification record. |
400 | `INVALID_ID_FORMAT`: malformed `id`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
user_id | string<uuid> | |
org_id | string | null | |
saas_package_id | string | null | |
provider | string (enum) manual_reviewsheeridverifyle | |
status | string (enum) pendingsubmittedapprovedrejectedexpired | |
institution_name | string | null | |
academic_role | string | null | |
rejection_reason | string | null | |
reviewed_by | string | null | |
reviewed_at | string | null | |
expires_at | string | null | |
created_at | string<date-time> | |
updated_at | string<date-time> | |
POST /v1/academic-verifications/{id}/review
Review an academic verification
Approves or rejects an academic verification. Callers must hold the reviewer role. `rejection_reason` is required when `decision=rejected`.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
id | path | string<uuid> | yes | Academic verification identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
decision | string (enum) approvedrejected | yes | |
rejection_reason | string | no | Required when `decision=rejected`. |
Responses
| Status | Description |
200 | Verification reviewed. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR` (including missing `rejection_reason` when decision=rejected). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
user_id | string<uuid> | |
org_id | string | null | |
saas_package_id | string | null | |
provider | string (enum) manual_reviewsheeridverifyle | |
status | string (enum) pendingsubmittedapprovedrejectedexpired | |
institution_name | string | null | |
academic_role | string | null | |
rejection_reason | string | null | |
reviewed_by | string | null | |
reviewed_at | string | null | |
expires_at | string | null | |
created_at | string<date-time> | |
updated_at | string<date-time> | |
POST /v1/academic-verifications/{id}/submit
Submit evidence for academic verification
Attaches institution name, role, and optional evidence URL to an in-progress academic verification.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
id | path | string<uuid> | yes | Academic verification identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
institution_name | string | yes | |
academic_role | string | yes | |
evidence_url | string<uri> | no | |
Responses
| Status | Description |
200 | Evidence submitted. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: resubmission quota exceeded. |
429 | 429 Too Many Requests: a rate limit or usage quota was exceeded. Retry after the indicated window. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
user_id | string<uuid> | |
org_id | string | null | |
saas_package_id | string | null | |
provider | string (enum) manual_reviewsheeridverifyle | |
status | string (enum) pendingsubmittedapprovedrejectedexpired | |
institution_name | string | null | |
academic_role | string | null | |
rejection_reason | string | null | |
reviewed_by | string | null | |
reviewed_at | string | null | |
expires_at | string | null | |
created_at | string<date-time> | |
updated_at | string<date-time> | |
POST /v1/academic-verifications/start
Start an academic verification
Initiates an academic verification for the authenticated user. Optionally scoped to a specific SaaS package and verification provider.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | no | |
provider | string (enum) manual_reviewsheeridverifyle | no | |
Responses
| Status | Description |
201 | Academic verification started. |
400 | `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | `NOT_FOUND`: the specified package or provider not found. |
409 | `CONFLICT`: concurrent verification submission quota exceeded. |
429 | `RATE_LIMIT_EXCEEDED`. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
201 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
user_id | string<uuid> | |
org_id | string | null | |
saas_package_id | string | null | |
provider | string (enum) manual_reviewsheeridverifyle | |
status | string (enum) pendingsubmittedapprovedrejectedexpired | |
institution_name | string | null | |
academic_role | string | null | |
rejection_reason | string | null | |
reviewed_by | string | null | |
reviewed_at | string | null | |
expires_at | string | null | |
created_at | string<date-time> | |
updated_at | string<date-time> | |
GET /v1/agent-setup
Read the agent setup guide
Returns, as Markdown, the guide an AI agent follows to redeem a setup link: where the setup code is (the fragment after `#` in the setup URL), the redemption request, and the answers it can receive. Public: no credential is needed or read. The setup code itself is never sent to this route, because a URL fragment never leaves the client.
Public Public: no authentication
Responses
| Status | Description |
200 | The redemption guide. Cached publicly for up to five minutes. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/agent-setup/links
Create a one-time agent setup link
Creates a single-use setup link that an AI agent can redeem for its own API key, so a person never has to paste a key into an agent. The answer carries the setup URL and ready-to-paste instructions for the agent. The link expires ten minutes after it is created and can be redeemed once. Only a signed-in person can create one: an API key, or a session limited to a single capability, is refused. The caller must have signed in within the last five minutes and must hold the permission to create API keys in the active organization. At most 10 links per user per rolling hour. The answer is sent with `Cache-Control: no-store`.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
relay_key_id | string | null | no | Optional. The key id of an AI-control session the Backbuild browser extension has armed in your browser. When it names one of your own active sessions in this organization, the agent's new key may drive that browser (`browser_bound: true`); otherwise the link is created unbound, which is not an error. |
Responses
| Status | Description |
201 | The link was created. Hand `instructions` (or `url`) to the agent; the code inside it is shown only in this answer. |
400 | `VALIDATION_ERROR`: the body is not a JSON object, carries a field other than `relay_key_id`, or `relay_key_id` is not a UUID or null. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | `FORBIDDEN`: the caller is an API key or a capability-limited session, no organization is selected, or the caller cannot create API keys in this organization. `REAUTH_REQUIRED`: the session was not signed in within the last five minutes; sign in again and retry. |
413 | `PAYLOAD_TOO_LARGE`: the body is larger than 1024 bytes. |
415 | `VALIDATION_ERROR`: a non-empty body was sent with a media type other than `application/json`. |
429 | 429 Too Many Requests: a rate limit or usage quota was exceeded. Retry after the indicated window. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
503 | `RATE_LIMIT_UNAVAILABLE`: the request could not be checked against its rate limit, so it was refused. Retry in a minute. |
201 response body: data fields
| Field | Type | Description |
link_id | string<uuid> | The link's id. |
url | string<uri> | The setup URL: the guide's address with the setup code after `#`. |
expires_at | string<date-time> | When the link stops working, ten minutes after creation. |
browser_bound | boolean | Whether the key the agent redeems may drive the browser session named by `relay_key_id`. |
instructions | string | Ready-to-paste instructions for the agent, including the setup code, the redemption request, and when the link expires. |
POST /v1/agent-setup/redeem
Redeem an agent setup link for an API key
Called by the AI agent, not the person. Exchanges the setup code from a setup link for a new API key belonging to the person who created the link, in that link's organization. The key is named `<system> / <agent>` from the request, carries the `mcp:*` scope (the Backbuild MCP server at `mcp_url`), expires after 90 days, and is limited to 60 requests a minute. A link redeems once: an unknown, already used, or expired code all get the same `410 LINK_INVALID`, and the agent must ask its person for a new link. No credential is sent: the code is the credential. At most 10 attempts a minute and 60 an hour from one client address. Send `Accept: text/plain` to receive only the raw key as plain text, with its id in the `X-Backbuild-Key-Id` header. Every answer is sent with `Cache-Control: no-store`.
Public Public: no authentication
Request Body
| Field | Type | Required | Description |
code | string | yes | The setup code: the organization id, a dot, and the 43-character secret, exactly as it appears after `#` in the setup URL. |
system | string | yes | The tool or system the agent runs in, for example the name of the coding tool or chat app. NFC-normalised and trimmed; 1 to 48 letters, digits, spaces, or `. _ - ( ) + @ : #`. |
agent | string | yes | A name for this agent, so the key can be told apart from others. Same rules as `system`. |
Responses
| Status | Description |
201 | The key was created. It is shown only in this answer; store it now. |
400 | `VALIDATION_ERROR`: the body is not exactly `{ code, system, agent }`, the code is not a setup code, a name breaks its rules, or the key could not be created from this request. |
403 | `FORBIDDEN`: the person who created the link can no longer create API keys in that organization. |
410 | `LINK_INVALID`: the code is unknown, already used, or expired. Ask for a new setup link. |
413 | `PAYLOAD_TOO_LARGE`: the body is larger than 2048 bytes. |
415 | `VALIDATION_ERROR`: the media type is not `application/json`. |
429 | 429 Too Many Requests: a rate limit or usage quota was exceeded. Retry after the indicated window. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
503 | `RATE_LIMIT_UNAVAILABLE`: the request could not be checked against its rate limit, so it was refused. Retry in a minute. |
201 response body: data fields
| Field | Type | Description |
key | string | The raw API key (`bb_live_...`). Shown once; it cannot be retrieved later. |
key_id | string<uuid> | The key's id, as listed under API keys. |
name | string | The key's name, `<system> / <agent>`. |
expires_at | string<date-time> | When the key expires, 90 days after redemption. |
mcp_url | string<uri> | The Backbuild MCP server address to use the key with. |
browser_bound | boolean | Whether this key may drive the browser session the link was bound to. |
GET /v1/api-keys
List organization API keys
Returns the API keys for the authenticated organization. Raw key values are never returned: only the key prefix, name, scopes, and metadata. The organization and RLS scope are derived from the session.
Auth Session JWT (Bearer)
Responses
| Status | Description |
200 | List of API keys. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
keys | array of object | |
POST /v1/api-keys
Create an API key
Creates a new organization API key with the specified name, scopes, expiry, and optional rate limit. The raw key (`bb_live_<prefix>_<secret>`) is returned ONCE in the response and never stored: if lost it cannot be retrieved. Requires recent authentication (token issued within 5 minutes). Organization API key cap (100) is enforced before creation.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
name | string | yes | |
scopes | array of string | yes | |
expires_in_days | integer | no | Expiry in days from now (omit for non-expiring). |
rate_limit_rpm | integer | no | |
pinned_env_id | string<uuid> | no | |
Responses
| Status | Description |
201 | API key created. The `key` field contains the raw key; store it now as it will not be shown again. |
400 | `VALIDATION_ERROR`: invalid JSON, invalid `name`, disallowed `scopes`, out-of-range `expires_in_days` or `rate_limit_rpm`, or malformed `pinned_env_id`. |
401 | `AUTH_REQUIRED`, `REAUTH_REQUIRED`: a recent (within 5 min) session token is required. |
403 | `FORBIDDEN`: caller lacks permission to create keys or the `pinned_env_id` would escalate privilege. |
429 | `QUOTA_EXCEEDED`: organization has reached the 100-key limit. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
201 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
key | string | Full raw API key in format `bb_live_{8hexchars}_{64hexchars}`. Store it now: it will not be shown again. |
prefix | string | |
name | string | |
scopes | array of string | |
expires_at | string | null | |
created_at | string<date-time> | |
DELETE /v1/api-keys/{id}
Revoke an API key
Immediately revokes an API key, preventing any future use. Requires recent authentication (token issued within 5 minutes).
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
id | path | string<uuid> | yes | API key identifier (UUIDv7). |
Responses
| Status | Description |
200 | API key revoked. |
400 | `VALIDATION_ERROR`: malformed key `id`. |
401 | `AUTH_REQUIRED`, `REAUTH_REQUIRED`: a recent session token is required. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
revoked | boolean (enum) true | |
POST /v1/api-keys/{id}/rotate
Rotate an API key
Atomically revokes the existing key and creates a replacement with the same name and scopes. The new raw key is returned once. An optional `grace_period_minutes` (0 to 60, default 5) keeps the old key valid for a short overlap window so in-flight requests are not disrupted. Requires recent authentication (token issued within 5 minutes).
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
id | path | string<uuid> | yes | API key identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
grace_period_minutes | integer | no | How long the old key remains valid after rotation (0 to 60 min). |
Responses
| Status | Description |
200 | API key rotated. The `key` field contains the new raw key; store it now as it will not be shown again. |
400 | `VALIDATION_ERROR`: malformed key `id`. |
401 | `AUTH_REQUIRED`, `REAUTH_REQUIRED`: a recent session token is required. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | New key identifier. |
key | string | New raw API key. Store it now. |
prefix | string | |
name | string | |
scopes | array of string | |
old_key_id | string<uuid> | |
old_key_expires_at | string | null | Grace period expiry for the old key (null = revoked immediately). |
created_at | string<date-time> | |
GET /v1/billing
Get billing overview
Returns the billing overview for the authenticated organization, including subscription state, payment methods, and credit balance. Requires owner or admin role within the org; members without that role receive `403 FORBIDDEN`.
Auth Session JWT (Bearer)
Responses
| Status | Description |
200 | Billing overview for the organization. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | `FORBIDDEN`: caller lacks owner or admin role within the organization. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
subscription | object | An organization subscription record. |
payment_methods | array of object | Saved payment methods. |
credits | object | AI credit balance for an organization. |
GET /v1/billing/client-config
Get Stripe client configuration
Returns the Stripe publishable key required to initialize the Stripe.js client on the frontend. Returns `503 CHECKOUT_FAILED` if Stripe is not configured for the deployment.
Auth Session JWT (Bearer)
Responses
| Status | Description |
200 | Stripe client configuration. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
503 | `CHECKOUT_FAILED`: Stripe is not configured for this deployment. |
200 response body: data fields
| Field | Type | Description |
publishable_key | string | Stripe publishable key (pk_live_* or pk_test_*) used to initialize Stripe.js. |
POST /v1/billing/create-checkout-session
Create a hosted Stripe Checkout session
Creates a hosted Stripe Checkout session for a subscription plan. Returns a `checkout_url` to redirect the user to Stripe. `success_url` and `cancel_url` must be `https://` URLs targeting an allowlisted application domain; other values return `400 VALIDATION_ERROR`. `trial_period_days` is a client hint that is server-clamped against the plan's configured maximum: the effective value can be less than requested.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
price_id | string | yes | Stripe price id for the plan. |
package_id | string<uuid> | yes | SaaS package id (UUIDv7). |
success_url | string<uri> | no | HTTPS URL to redirect to after successful checkout. Must target an allowlisted application domain. |
cancel_url | string<uri> | no | HTTPS URL to redirect to after checkout cancellation. Must target an allowlisted application domain. |
trial_period_days | integer | no | Client-suggested free trial length in days. Server-clamped to the plan's configured maximum; the effective value may be lower than requested. |
Responses
| Status | Description |
201 | Checkout session created. Redirect the user to `checkout_url`. |
400 | `VALIDATION_ERROR` (invalid fields, disallowed redirect domain, or invalid `trial_period_days`), `STRIPE_CREDENTIALS_MISSING` (Stripe not configured for the package). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | `CHECKOUT_FAILED`: Stripe session creation failed or server misconfiguration. |
201 response body: data fields
| Field | Type | Description |
checkout_url | string | null | Stripe-hosted Checkout URL. Redirect the user here. |
POST /v1/billing/create-embedded-checkout-session
Create an embedded Stripe Checkout session
Creates an embedded Stripe Checkout session. Returns a `client_secret` for initializing Stripe.js Embedded Checkout on the page. Supports `mode=subscription` (default, requires `price_id`) and `mode=setup` (card collection without a charge). `return_url` must target an allowlisted application domain; other values return `400 VALIDATION_ERROR`.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
price_id | string | no | Stripe price id. Required for `mode=subscription`; ignored for `mode=setup`. |
package_id | string<uuid> | yes | SaaS package id (UUIDv7). |
return_url | string<uri> | no | HTTPS URL the embedded checkout redirects to on completion. Must target an allowlisted application domain. |
trial_period_days | integer | no | Client-suggested trial length in days. Server-clamped to the plan's maximum. |
mode | string (enum) subscriptionsetup | no | Checkout mode. `subscription` (default) charges immediately; `setup` collects a card for later use. |
Responses
| Status | Description |
201 | Embedded checkout session created. |
400 | `VALIDATION_ERROR` (invalid fields, disallowed redirect domain, or `price_id` missing for `mode=subscription`), `STRIPE_CREDENTIALS_MISSING` (Stripe not configured for the package). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | `CHECKOUT_FAILED`: Stripe session creation failed or server misconfiguration. |
201 response body: data fields
| Field | Type | Description |
client_secret | string | Stripe Checkout client secret for initializing Stripe.js Embedded Checkout. |
mode | string (enum) subscriptionsetup | Mode of the session (`setup` only present when mode=setup was requested). |
POST /v1/billing/credit-packs/{planId}/purchase
Purchase a credit pack
Initiates a one-time Stripe Checkout session (`mode=payment`) to purchase an AI credit pack identified by `planId`. Returns `checkout_url` to redirect the user and an `order_id` for status polling. The plan must be of type `credit_pack` with a configured Stripe price. `return_url` and `cancel_url` must be `https://` URLs targeting an allowlisted application domain.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
planId | path | string<uuid> | yes | Credit pack pricing plan identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
return_url | string<uri> | no | HTTPS URL to redirect to after successful purchase. Must target an allowlisted application domain. |
cancel_url | string<uri> | no | HTTPS URL to redirect to on cancellation. Must target an allowlisted application domain. |
Responses
| Status | Description |
201 | Credit pack checkout session created. |
400 | `INVALID_ID_FORMAT` (malformed `planId`), `VALIDATION_ERROR` (invalid redirect URLs or plan not eligible for credit-pack purchase). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | `FORBIDDEN`: caller lacks permission to purchase credit packs for the org. |
404 | `NOT_FOUND`: plan not found. |
500 | `CHECKOUT_FAILED`: Stripe session creation failed. |
503 | `CHECKOUT_FAILED`: credit pack plan not yet synced to Stripe (missing `stripe_price_id`). |
201 response body: data fields
| Field | Type | Description |
order_id | string<uuid> | Platform order id for status polling. |
checkout_url | string | null | Stripe Checkout URL to redirect the user to. |
checkout_session_id | string | Stripe Checkout session id. |
credit_amount | integer | Number of AI credits in the pack. |
price_cents | integer | Price in the smallest currency unit (e.g. cents). |
currency | string | ISO 4217 currency code. |
GET /v1/billing/credit-packs/orders/{orderId}
Get credit pack order status
Returns the status and metadata of a one-time credit pack order. Used for post-checkout polling to confirm that credits have been provisioned after Stripe payment.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
orderId | path | string<uuid> | yes | Credit pack order identifier (UUIDv7). |
Responses
| Status | Description |
200 | Credit pack order record. |
400 | `INVALID_ID_FORMAT`: malformed `orderId`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | `FORBIDDEN`: order does not belong to this organization. |
404 | `NOT_FOUND`: order not found. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
org_id | string<uuid> | |
plan_id | string<uuid> | |
status | string | Order status (e.g. `pending`, `completed`, `failed`). |
credit_amount | integer | |
price_cents | integer | |
currency | string | |
credits_applied | boolean | Whether credits have been applied to the organization balance. |
credits_applied_at | string | null | |
stripe_checkout_session_id | string | null | |
failure_reason | string | null | |
created_at | string<date-time> | |
completed_at | string | null | |
GET /v1/billing/credits
Get credit balance
Returns the current AI credit balance for the authenticated organization.
Auth Session JWT (Bearer)
Responses
| Status | Description |
200 | Credit balance. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
balance | number | Current available credit balance. |
currency | string | Currency code (e.g. `usd`). |
GET /v1/billing/invoices
List invoices
Returns invoices for the authenticated organization with optional status and date-range filters. Default limit 25, max 100.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
status | query | string (enum) | no | Filter by invoice status. |
from_date | query | string<date-time> | no | Inclusive start of the invoice date range (ISO 8601 datetime). |
to_date | query | string<date-time> | no | Inclusive end of the invoice date range (ISO 8601 datetime). |
limit | query | integer | no | Maximum number of invoices to return (1 to 100, default 25). |
offset | query | integer | no | Number of invoices to skip for pagination. |
Responses
| Status | Description |
200 | Paginated list of invoices. |
400 | 400 Bad Request: a request parameter, body field, or the JSON body itself failed validation. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
GET /v1/billing/payment-methods
List saved payment methods
Returns all saved Stripe payment methods for the authenticated organization.
Auth Session JWT (Bearer)
Responses
| Status | Description |
200 | List of saved payment methods. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
PUT /v1/billing/payment-methods
Upsert a payment method
Records a Stripe payment method (`pm_*`) for the organization. The API verifies the payment method belongs to this organization's Stripe customer before writing; a cross-customer or unknown `pm_*` returns `400 PAYMENT_METHOD_NOT_OWNED`. All `pm_*` and billing-address string fields are capped at Stripe's documented field lengths.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
payment_method_id | string | yes | Stripe `pm_*` identifier. Must belong to this organization's Stripe customer. |
set_as_default | boolean | no | Whether to set this as the default payment method (default true when omitted). |
billing_details | object | no | Optional billing detail overrides. |
Responses
| Status | Description |
200 | Payment method upserted. |
400 | `VALIDATION_ERROR` (invalid body fields), `PAYMENT_METHOD_NOT_OWNED` (the `pm_*` id does not belong to this org's Stripe customer or cannot be found). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | Platform-assigned record id. |
stripe_payment_method_id | string | Stripe `pm_*` identifier. |
type | string | Payment method type (e.g. `card`). |
is_default | boolean | Whether this is the default payment method. |
created_at | string<date-time> | When the record was created. |
GET /v1/billing/subscription-status
Poll subscription activation status
Returns whether the organization's subscription has been activated. Intended for short-polling after Stripe Checkout redirect to confirm the Stripe webhook has been processed (typically called every 3 s for up to 2 min).
Auth Session JWT (Bearer)
Responses
| Status | Description |
200 | Current activation state. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
active | boolean | True once the Stripe webhook has confirmed activation. |
status | string | null | Current subscription status string. |
GET /v1/billing/subscriptions
List subscriptions
Returns the organization's subscriptions with optional status filter and offset pagination. Default limit 25, max 100.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
status | query | string (enum) | no | Filter by subscription status. Must be one of the canonical values matching the underlying platform subscription state. |
limit | query | integer | no | Maximum number of records to return (1 to 100, default 25). |
offset | query | integer | no | Number of records to skip for pagination. |
Responses
| Status | Description |
200 | Paginated list of subscriptions. |
400 | 400 Bad Request: a request parameter, body field, or the JSON body itself failed validation. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/billing/subscriptions
Create a subscription
Creates a new subscription for the authenticated organization using a Stripe payment method. All Stripe identifier and billing-address fields are bounded at Stripe's documented field lengths.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
plan_id | string | yes | Pricing plan key or ID. |
interval | string (enum) monthyear | yes | Billing interval. |
payment_method_id | string | yes | Stripe `pm_*` identifier. |
billing_details | object | yes | |
seats | integer | no | Number of seats (default 1). |
metadata | object | no | Optional metadata JSONB. |
Responses
| Status | Description |
201 | Subscription created. |
400 | 400 Bad Request: a request parameter, body field, or the JSON body itself failed validation. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
201 response body: data fields
| Field | Type | Description |
id | string<uuid> | Platform-assigned subscription id. |
org_id | string<uuid> | Owning organization. |
plan_id | string | Pricing plan key. |
interval | string (enum) monthyear | Billing interval. |
status | string (enum) activetrialingpast_duecanceledincompletepaused | Subscription lifecycle status. |
seats | integer | Number of seats. |
cancel_at_period_end | boolean | Whether the subscription is scheduled to cancel at period end. |
current_period_end | string | null | End of the current billing period. |
created_at | string<date-time> | |
updated_at | string<date-time> | |
PUT /v1/billing/subscriptions/{subscriptionId}
Update a subscription
Updates an existing subscription's plan, seat count, or cancellation scheduling. At least one body field must be supplied. Returns `404 NOT_FOUND` when the subscription does not exist or belongs to another organization.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
subscriptionId | path | string<uuid> | yes | Subscription identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
plan_id | string | no | New pricing plan key. |
seats | integer | no | New seat count. |
cancel_at_period_end | boolean | no | Schedule cancellation at the current period end. |
metadata | object | no | Optional metadata JSONB. |
Responses
| Status | Description |
200 | Updated subscription record. |
400 | `INVALID_ID_FORMAT` (malformed `subscriptionId`), `VALIDATION_ERROR` (invalid body fields). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
404 | `NOT_FOUND`: subscription not found or belongs to another organization. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | Platform-assigned subscription id. |
org_id | string<uuid> | Owning organization. |
plan_id | string | Pricing plan key. |
interval | string (enum) monthyear | Billing interval. |
status | string (enum) activetrialingpast_duecanceledincompletepaused | Subscription lifecycle status. |
seats | integer | Number of seats. |
cancel_at_period_end | boolean | Whether the subscription is scheduled to cancel at period end. |
current_period_end | string | null | End of the current billing period. |
created_at | string<date-time> | |
updated_at | string<date-time> | |
GET /v1/billing/usage
Get billable usage summary
Returns billable usage rollup for the authenticated organization. When `env_id` is absent or `all`, the global aggregate across all environments is returned. The `window` parameter controls the time range (default `30d`).
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
env_id | query | string | no | Environment UUID to scope usage to, or the literal `all` (default) for a global aggregate across all environments. Any other non-UUID value returns `400 VALIDATION_ERROR`. |
window | query | string (enum) | no | Aggregation window. One of `24h`, `7d`, `30d` (default), `90d`, `all`. |
Responses
| Status | Description |
200 | Usage summary. |
400 | `VALIDATION_ERROR`: `env_id` is not a UUID or `all`; `window` is not one of the allowed values. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
window | string | Requested aggregation window. |
env_id | string | null | Environment the rollup is scoped to (null for global aggregate). |
total_credits_used | number | Total AI credits consumed in the window. |
by_env | array of object | Per-environment breakdown (only present in global-aggregate responses). |
GET /v1/pricing
List public pricing plans
Returns pricing plans sourced from Stripe, optionally filtered by `package_id` and `environment`. This is a public endpoint: no authentication required. Results are KV-cached; `cached` and `cacheAge` indicate whether the response is from cache. Rate-limited to 50 requests per minute per IP (IPv6 normalized to /64).
Public Public: no authentication
Parameters
| Name | In | Type | Required | Description |
package_id | query | string<uuid> | no | Filter to a specific SaaS package (UUIDv7). When omitted all published packages are returned. |
environment | query | string (enum) | no | Filter to a specific deployment environment. |
Responses
| Status | Description |
200 | Pricing plans. The `cached` flag indicates whether the response was served from the KV cache. |
400 | 400 Bad Request: a request parameter, body field, or the JSON body itself failed validation. |
429 | 429 Too Many Requests: a rate limit or usage quota was exceeded. Retry after the indicated window. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/signup-checkout/intents
Create a pre-auth signup checkout intent
Creates a signup checkout intent before the user authenticates, capturing plan and package selection. No authentication is required; rate-limiting (20/hour per IP) is enforced by the platform. A Cloudflare Turnstile token is required (supplied as `cf-turnstile-response` header by the frontend). Only `https://` requests traversing the Cloudflare edge are accepted (the source IP is derived exclusively from `cf-connecting-ip`). At least one of `org_id`, `org_slug_or_id`, or `domain` must be supplied as a resolved UUID; slug/domain resolution is not performed server-side on this endpoint.
Public Public: no authentication
Request Body
| Field | Type | Required | Description |
anonymous_session_id | string | yes | Stable anonymous session identifier to link the pre-auth intent to a post-auth session. |
org_id | string<uuid> | no | |
org_slug_or_id | string | no | |
domain | string | no | |
package_id | string<uuid> | no | |
package_slug | string | no | |
pricing_plan_price_id | string | no | |
billing_interval | string (enum) monthyear | no | |
trial_period_days | integer | no | Requested trial length (server-clamped to plan maximum). |
metadata | object | no | |
Responses
| Status | Description |
201 | Signup checkout intent created. |
400 | `INVALID_JSON` (unparseable body), `VALIDATION_ERROR` (schema violation), `INVALID_PARAMETER` (source IP could not be determined: request must traverse Cloudflare edge). |
403 | `FORBIDDEN`: academic-verification prerequisite not met, or prerequisite plan not satisfied. |
404 | `NOT_FOUND`: the specified plan or package does not exist. |
409 | `CONFLICT`: org conflict or intent lifecycle state conflict (e.g. intent expired or already consumed). `error.details` may carry `intent_org_id` and `caller_org_id` for org-conflict disambiguation. |
429 | `RATE_LIMIT_EXCEEDED`: per-IP rate limit exceeded. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
201 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
status | string (enum) pendingprocessingcompletedexpiredcancelled | |
checkout_url | string | null | Hosted checkout redirect URL. |
client_secret | string | null | Stripe embedded checkout client_secret. |
created_at | string<date-time> | |
expires_at | string | null | |
POST /v1/signup-checkout/intents/{id}/resume
Resume a signup checkout intent (authenticated)
Resumes a previously created signup checkout intent after user authentication. The caller must be an active member of the intent's owning organization. When `resolved_org_id` differs from the session's `org_id`, the caller's membership in `resolved_org_id` is pre-checked at the route layer before the underlying platform call.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
id | path | string<uuid> | yes | Signup checkout intent identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
pricing_plan_price_id | string | no | |
billing_interval | string (enum) monthyear | no | |
Responses
| Status | Description |
200 | Intent resumed; checkout session details returned. |
400 | `INVALID_ID_FORMAT` (malformed `id`), `INVALID_JSON` (unparseable body), `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | `FORBIDDEN`: caller is not an active member of the resolved org. |
404 | `NOT_FOUND`: intent not found. |
409 | `CONFLICT`: intent expired, already consumed, or org conflict. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
status | string (enum) pendingprocessingcompletedexpiredcancelled | |
checkout_url | string | null | Hosted checkout redirect URL. |
client_secret | string | null | Stripe embedded checkout client_secret. |
created_at | string<date-time> | |
expires_at | string | null | |
GET /v1/stripe-connect
Get legacy Stripe Connect configuration
Returns the Stripe Connect configuration for the given SaaS package and environment. Requires `stripe_connect.view` permission. This is the legacy per-(package, environment) endpoint; new integrations should use `/v1/stripe-profiles`.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
saas_package_id | query | string<uuid> | yes | SaaS package identifier (UUIDv7). |
environment | query | string (enum) | no | Deployment environment. |
Responses
| Status | Description |
200 | Stripe Connect configuration. |
400 | 400 Bad Request: a request parameter, body field, or the JSON body itself failed validation. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | `NOT_FOUND`: Stripe Connect configuration not found for this package and environment. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
saas_package_id | string<uuid> | |
environment | string (enum) developmentstagingpreprodproduction | |
stripe_account_id | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
created_at | string<date-time> | |
updated_at | string<date-time> | |
POST /v1/stripe-connect
Create legacy Stripe Connect configuration
Creates a Stripe Connect configuration for a SaaS package and environment. Requires `stripe_connect.create` permission. This is the legacy per-(package, environment) endpoint; new integrations should use `/v1/stripe-profiles`.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | yes | |
environment | string (enum) developmentstagingpreprodproduction | no | |
stripe_account_id | string | yes | Stripe account id (`acct_...`). Required for the legacy endpoint. |
account_type | string (enum) standardexpresscustom | no | Stripe Connect account type (default `express`). |
publishable_key | string | no | |
business_profile | object | no | |
webhook_secret_ref | string | no | Reference name for the stored webhook secret. |
Responses
| Status | Description |
201 | Stripe Connect configuration created. |
400 | `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
409 | `CONFLICT`: a Stripe Connect configuration already exists for this package and environment. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
201 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
saas_package_id | string<uuid> | |
environment | string (enum) developmentstagingpreprodproduction | |
stripe_account_id | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
created_at | string<date-time> | |
updated_at | string<date-time> | |
PUT /v1/stripe-connect
Update legacy Stripe Connect configuration
Updates the Stripe Connect configuration for a SaaS package and environment. At least one mutable field (beyond `saas_package_id` and `environment`) must be supplied. Requires `stripe_connect.update` permission. A body-supplied `stripe_account_id` that does not match the verified stored value is rejected with `403 FORBIDDEN`. This is the legacy endpoint; new integrations should use `/v1/stripe-profiles`.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | yes | |
environment | string (enum) developmentstagingpreprodproduction | no | |
stripe_account_id | string | no | |
account_type | string (enum) standardexpresscustom | no | |
publishable_key | string | no | |
business_profile | object | no | |
webhook_secret_ref | string | no | |
Responses
| Status | Description |
200 | Updated Stripe Connect configuration. |
400 | `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | `FORBIDDEN`: missing permission or submitted `stripe_account_id` does not match the verified record. |
404 | `NOT_FOUND`: configuration not found. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
saas_package_id | string<uuid> | |
environment | string (enum) developmentstagingpreprodproduction | |
stripe_account_id | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
created_at | string<date-time> | |
updated_at | string<date-time> | |
PUT /v1/stripe-connect/fees
Update legacy Stripe Connect fee percentages
Updates the platform and/or client fee percentages for a Stripe Connect configuration. At least one fee field must be supplied. Requires `stripe_connect.update` permission. This is the legacy endpoint; prefer `/v1/stripe-profiles/:profileId/fees`.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | yes | |
environment | string (enum) developmentstagingpreprodproduction | no | |
platform_fee_percent | number | no | |
client_fee_percent | number | no | |
Responses
| Status | Description |
200 | Fee percentages updated. |
400 | `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | `NOT_FOUND`: configuration not found. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/stripe-connect/onboard
Start legacy Stripe Connect onboarding (deprecated: 410 Gone)
**Permanently retired.** This endpoint is disabled by default and returns `410 GONE` with `Sunset`, `Link`, and `Deprecation` headers pointing to the successor `/v1/stripe-profiles/:profileId/onboard/start`. It accepted a tenant-supplied `stripe_account_id`, making account-claim races possible. Use `POST /v1/stripe-profiles` and `POST /v1/stripe-profiles/:profileId/onboard/start` instead.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | yes | |
environment | string (enum) developmentstagingpreprodproduction | no | |
stripe_account_id | string | yes | |
refresh_url | string<uri> | no | |
return_url | string<uri> | no | |
Responses
| Status | Description |
200 | Onboarding started (only returned when the endpoint is explicitly re-enabled by the operator). |
400 | `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
409 | `CONFLICT`: a Stripe Connect record already exists for this package/environment. |
410 | `GONE`: this endpoint is permanently retired. Use `POST /v1/stripe-profiles` + `POST /v1/stripe-profiles/:profileId/onboard/start`. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/stripe-connect/onboard/complete
Complete legacy Stripe Connect onboarding
Marks the Stripe Connect onboarding as complete by recording the `charges_enabled`, `payouts_enabled`, and `details_submitted` states reported by Stripe. Requires `stripe_connect.update` permission. This is the legacy endpoint; prefer `/v1/stripe-profiles/:profileId/onboard/complete`.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | yes | |
environment | string (enum) developmentstagingpreprodproduction | no | |
charges_enabled | boolean | no | |
payouts_enabled | boolean | no | |
details_submitted | boolean | no | |
Responses
| Status | Description |
200 | Onboarding completion recorded. |
400 | `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | `NOT_FOUND`: Stripe Connect configuration not found. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/stripe-connect/sync
Sync legacy Stripe Connect account status
Syncs Stripe account status fields (charges/payouts enabled, business profile, currency) for a legacy Stripe Connect configuration. Requires `stripe_connect.update` permission. This is the legacy endpoint; prefer `/v1/stripe-profiles/:profileId/sync`.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | yes | |
environment | string (enum) developmentstagingpreprodproduction | no | |
Responses
| Status | Description |
200 | Stripe Connect status synced. |
400 | `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | `NOT_FOUND`: configuration not found. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
GET /v1/stripe-profiles
List Stripe Connect profiles
Returns the organization's Stripe Connect profiles with optional status filter and page/pageSize pagination. Permission gating is enforced inside the platform; the caller must hold `stripe.profiles.view` or equivalent.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
page | query | integer | no | 1-based page number. |
page_size | query | integer | no | Records per page (1 to 100). |
status | query | string (enum) | no | Filter by profile onboarding status. |
Responses
| Status | Description |
200 | Paginated list of Stripe Connect profiles. |
400 | 400 Bad Request: a request parameter, body field, or the JSON body itself failed validation. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/stripe-profiles
Create a Stripe Connect profile
Creates a new Stripe Connect profile for the organization. `stripe_account_id` may not be supplied on creation: the Stripe account is created server-side by calling `POST /v1/stripe-profiles/:profileId/onboard/start`. Permission gating is enforced inside the platform.
Auth Session JWT (Bearer)
Request Body
| Field | Type | Required | Description |
name | string | yes | |
description | string | no | |
country | string | no | |
default_currency | string | no | |
Responses
| Status | Description |
201 | Stripe Connect profile created. |
400 | `INVALID_JSON`, `VALIDATION_ERROR` (including rejection of a supplied `stripe_account_id`). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
409 | `CONFLICT`: a profile with a conflicting constraint already exists. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
201 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
org_id | string<uuid> | |
name | string | |
description | string | null | |
stripe_account_id | string | null | |
status | string (enum) pendingonboardingrestrictedactivedisabled | |
country | string | null | |
default_currency | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
version | integer | Optimistic concurrency version. |
created_at | string<date-time> | |
updated_at | string<date-time> | |
GET /v1/stripe-profiles/{profileId}
Get a Stripe Connect profile
Returns a single Stripe Connect profile by id. Returns `404 NOT_FOUND` for profiles not belonging to the caller's organization.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Responses
| Status | Description |
200 | Stripe Connect profile. |
400 | `INVALID_ID_FORMAT`: malformed `profileId`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
org_id | string<uuid> | |
name | string | |
description | string | null | |
stripe_account_id | string | null | |
status | string (enum) pendingonboardingrestrictedactivedisabled | |
country | string | null | |
default_currency | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
version | integer | Optimistic concurrency version. |
created_at | string<date-time> | |
updated_at | string<date-time> | |
PATCH /v1/stripe-profiles/{profileId}
Update a Stripe Connect profile
Updates mutable fields on a Stripe Connect profile (name, description, country, default currency, business profile). `expected_version` is required for optimistic concurrency. At least one mutable field (beyond `expected_version`) must be supplied.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
expected_version | integer | yes | |
name | string | no | |
description | string | no | |
country | string | no | |
default_currency | string | no | |
Responses
| Status | Description |
200 | Updated profile. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: `expected_version` is stale (concurrent modification). |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
org_id | string<uuid> | |
name | string | |
description | string | null | |
stripe_account_id | string | null | |
status | string (enum) pendingonboardingrestrictedactivedisabled | |
country | string | null | |
default_currency | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
version | integer | Optimistic concurrency version. |
created_at | string<date-time> | |
updated_at | string<date-time> | |
DELETE /v1/stripe-profiles/{profileId}
Delete (archive) a Stripe Connect profile
Soft-deletes a Stripe Connect profile. `expected_version` is required for optimistic concurrency. The platform enforces that the profile has no active subscriber dependencies before deletion.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
expected_version | integer | yes | |
Responses
| Status | Description |
200 | Profile archived. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: `expected_version` is stale, profile has dependents, or profile is already archived. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
PATCH /v1/stripe-profiles/{profileId}/fees
Update Stripe Connect profile fee percentages
Updates the platform and/or client fee percentages for a Stripe Connect profile. Requires a fresh MFA step-up (the session's last MFA step-up must be within the configured window). `expected_version` is required for optimistic concurrency. At least one fee field must be supplied.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
expected_version | integer | yes | |
platform_fee_percent | number | null | no | Platform application fee percentage (0 to 50). Nullable to clear. |
client_fee_percent | number | no | Client-side fee percentage (0 to 50). |
Responses
| Status | Description |
200 | Fee percentages updated. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | `AUTH_REQUIRED` or `MFA_STEPUP_REQUIRED`: a fresh MFA step-up is required for fee changes. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: `expected_version` is stale. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
org_id | string<uuid> | |
name | string | |
description | string | null | |
stripe_account_id | string | null | |
status | string (enum) pendingonboardingrestrictedactivedisabled | |
country | string | null | |
default_currency | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
version | integer | Optimistic concurrency version. |
created_at | string<date-time> | |
updated_at | string<date-time> | |
GET /v1/stripe-profiles/{profileId}/links
List package links for a Stripe Connect profile
Returns all (SaaS package, environment) links associated with this Stripe Connect profile.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Responses
| Status | Description |
200 | List of package links. |
400 | `INVALID_ID_FORMAT`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/stripe-profiles/{profileId}/links
Link a package to a Stripe Connect profile
Associates a (SaaS package, environment) combination with this Stripe Connect profile.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
saas_package_id | string<uuid> | yes | |
environment | string (enum) developmentstagingpreprodproduction | yes | |
Responses
| Status | Description |
201 | Package linked. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: link already exists or live/test mode conflict. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
201 response body: data fields
| Field | Type | Description |
profile_id | string<uuid> | |
saas_package_id | string<uuid> | |
environment | string (enum) developmentstagingpreprodproduction | |
created_at | string<date-time> | |
DELETE /v1/stripe-profiles/{profileId}/links/{pkgId}/{env}
Unlink a package from a Stripe Connect profile
Removes the link between a (SaaS package, environment) combination and this Stripe Connect profile. The `archive_stripe_products` query parameter controls whether linked Stripe products are archived.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
pkgId | path | string<uuid> | yes | SaaS package identifier (UUIDv7). |
env | path | string (enum) | yes | Deployment environment. |
archive_stripe_products | query | string (enum) | no | When `true`, archive the associated Stripe products/prices for this link. |
Responses
| Status | Description |
200 | Package unlinked. |
400 | `INVALID_ID_FORMAT` (malformed `profileId` or `pkgId`), `INVALID_PARAMETER` (invalid `env` value). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: link has active subscribers or active Stripe products (archive first or use `archive_stripe_products=true`). |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/stripe-profiles/{profileId}/onboard/complete
Complete Stripe Connect profile onboarding
Syncs the Stripe account's current state (charges/payouts/details, onboarding status, tax info) from Stripe into the platform record. `expected_version` is required for optimistic concurrency.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
expected_version | integer | yes | |
Responses
| Status | Description |
200 | Onboarding state synced from Stripe. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: profile has no Stripe account yet (call `/onboard/start` first) or `expected_version` is stale. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
502 | `EXTERNAL_SERVICE_ERROR`: Stripe account lookup failed. |
503 | `SERVICE_UNAVAILABLE`: Stripe is not configured. |
200 response body: data fields
| Field | Type | Description |
id | string<uuid> | |
org_id | string<uuid> | |
name | string | |
description | string | null | |
stripe_account_id | string | null | |
status | string (enum) pendingonboardingrestrictedactivedisabled | |
country | string | null | |
default_currency | string | null | |
charges_enabled | boolean | |
payouts_enabled | boolean | |
details_submitted | boolean | |
platform_fee_pct | number | null | |
client_fee_pct | number | null | |
version | integer | Optimistic concurrency version. |
created_at | string<date-time> | |
updated_at | string<date-time> | |
POST /v1/stripe-profiles/{profileId}/onboard/start
Start Stripe Connect profile onboarding
Orchestrates server-side Stripe account creation (if the profile has no `stripe_account_id` yet) and returns a fresh Stripe AccountLink URL for the operator to complete KYC. Uses a stable Idempotency-Key so a retry after a transient failure does not create a duplicate Stripe account. `refresh_url` and `return_url` must use `https://` and target the configured application domain.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
refresh_url | string<uri> | no | |
return_url | string<uri> | no | |
Responses
| Status | Description |
200 | Onboarding URL returned. Redirect the operator to `onboarding_url`. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR` (disallowed redirect URL). |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: profile state conflict (e.g. `stripe_account_id` already claimed by another profile). |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
502 | `EXTERNAL_SERVICE_ERROR`: Stripe account creation or AccountLink creation failed. |
503 | `SERVICE_UNAVAILABLE`: Stripe is not configured for this deployment. |
200 response body: data fields
| Field | Type | Description |
onboarding_url | string<uri> | Stripe AccountLink URL for the operator. |
expires_at | string<date-time> | |
POST /v1/stripe-profiles/{profileId}/sync
Queue Stripe Connect profile sync
Enqueues a re-sync of linked Stripe plans for this profile, optionally filtered to specific environments. Returns `202 Accepted`.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
environments | array of enum developmentstagingpreprodproduction | no | |
Responses
| Status | Description |
202 | Sync enqueued. |
400 | `INVALID_ID_FORMAT`, `VALIDATION_ERROR`. |
401 | 401 Unauthorized: missing, expired, malformed, or revoked credentials. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: profile is not in a state that allows sync (e.g. onboarding not yet complete). |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |
POST /v1/stripe-profiles/{profileId}/webhook-secret/rotate
Rotate Stripe Connect profile webhook secret
Rotates the webhook signing secret for a Stripe Connect profile. Requires a fresh MFA step-up (PCI DSS Req 8.6 / SOC 2 CC6.6 key management control). `expected_version` is required for optimistic concurrency.
Auth Session JWT (Bearer)
Parameters
| Name | In | Type | Required | Description |
profileId | path | string<uuid> | yes | Stripe Connect profile identifier (UUIDv7). |
Request Body
| Field | Type | Required | Description |
expected_version | integer | yes | |
Responses
| Status | Description |
200 | Webhook secret rotated. |
400 | `INVALID_ID_FORMAT`, `INVALID_JSON`, `VALIDATION_ERROR`. |
401 | `AUTH_REQUIRED` or `MFA_STEPUP_REQUIRED`: a fresh MFA step-up is required. |
403 | 403 Forbidden: authenticated but lacking the required membership, role, permission, MFA step-up, or feature entitlement. |
404 | 404 Not Found: the requested resource does not exist or is not visible to the caller. |
409 | `CONFLICT`: `expected_version` is stale. |
500 | 500 Internal Server Error: an unexpected server-side error. The message is sanitized; the correlation id is logged server-side. |