Billing API
Base URL: https://billing.opteryx.app
Overview
Billing account and membership management, payment methods and charges, and workspace lifecycle (creation, deletion, locking).
Endpoints
| Service | Docs |
|---|---|
List Accountsget/v1/accounts |
View |
Create Accountpost/v1/accounts |
View |
Get Accountget/v1/accounts/{account_id} |
View |
Update Accountpatch/v1/accounts/{account_id} |
View |
Delete Accountdelete/v1/accounts/{account_id} |
View |
List Membersget/v1/accounts/{account_id}/members |
View |
Invite Memberpost/v1/accounts/{account_id}/members |
View |
Update Member Rolepatch/v1/accounts/{account_id}/members/{identity} |
View |
Remove Memberdelete/v1/accounts/{account_id}/members/{identity} |
View |
Accept Invitepost/v1/accounts/{account_id}/members/{identity}/accept |
View |
Get Payment Methodget/v1/accounts/{account_id}/payment-methods |
View |
Attach Payment Methodpost/v1/accounts/{account_id}/payment-methods |
View |
Detach Payment Methoddelete/v1/accounts/{account_id}/payment-methods |
View |
Create Paymentpost/v1/accounts/{account_id}/payments |
View |
List Account Workspacesget/v1/accounts/{account_id}/workspaces |
View |
List Invoicesget/v1/invoices |
View |
Get Invoiceget/v1/invoices/{invoice_id} |
View |
Create Workspaceput/v1/workspaces/{name} |
View |
Delete Workspacedelete/v1/workspaces/{name} |
View |
Lock Workspaceput/v1/workspaces/{name}/lock |
View |
Unlock Workspacedelete/v1/workspaces/{name}/lock |
View |
Restore Workspacepost/v1/workspaces/{name}/restore |
View |
List Accounts
Request: get/v1/accounts
List accounts the caller is an active member of.
Membership is exclusive (find_existing_membership), so this returns at
most one row in practice - still a list per the design doc's shape, not
collapsed to a single object.
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonarray<object>) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://billing.opteryx.app/v1/accounts
Create Account
Request: post/v1/accounts
Create a billing account. The caller becomes its first billing_admin
immediately as an active member (not a pending invite) - see
new_genesis_member_doc's docstring for why.
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:AccountCreateRequest- name
string[required] - tax_id
string | null[optional] - address
object | null[optional]
- name
Responses
- 201 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://billing.opteryx.app/v1/accounts
Get Account
Request: get/v1/accounts/{account_id}
Read. Only visible to active members of the account - an account's name/tax_id/address are not public to any authenticated caller who happens to know or guess an account id.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://billing.opteryx.app/v1/accounts/{account_id}
Update Account
Request: patch/v1/accounts/{account_id}
Partial update: name, tax_id, address, member_min_age_ms. Only billing_admins of the account may update it.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:AccountUpdateRequest- name
string | null[optional] - tax_id
string | null[optional] - address
object | null[optional] - member_min_age_ms
integer | null[optional]
- name
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
patch https://billing.opteryx.app/v1/accounts/{account_id}
Delete Account
Request: delete/v1/accounts/{account_id}
Soft-delete: status: deactivated. Only billing_admins may call this.
409 if any workspace still references this account. On success, cascades to delete every member subdocument - no "deactivated but still has members" state should exist afterward.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
delete https://billing.opteryx.app/v1/accounts/{account_id}
List Members
Request: get/v1/accounts/{account_id}/members
List members. Any active member of the account may list.
Expired pending rows (now > invite_expires_at) are filtered out of the
response and lazily deleted from Firestore when encountered - no
separate sweep job, per api-v2.md.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonarray<object>) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://billing.opteryx.app/v1/accounts/{account_id}/members
Invite Member
Request: post/v1/accounts/{account_id}/members
Invite a new member. Only existing billing_admins of this account may invite.
409 if the invitee already has an active/pending membership on another
account (checked via the collection-group find_existing_membership
query). Re-inviting an identity that's still pending on this same
account resets the 7-day clock instead of erroring - a fresh
new_member_doc is written over the existing pending doc.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:InviteMemberRequest- identity_or_email
string[required] - role
string[required]
- identity_or_email
Responses
- 201 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://billing.opteryx.app/v1/accounts/{account_id}/members
Update Member Role
Request: patch/v1/accounts/{account_id}/members/{identity}
{role: "billing_admin"|"member"}. Only billing_admins may change roles.
Path Parameters
- account_id
string[path; required] - identity
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:UpdateMemberRoleRequest- role
string[required]
- role
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
patch https://billing.opteryx.app/v1/accounts/{account_id}/members/{identity}
Remove Member
Request: delete/v1/accounts/{account_id}/members/{identity}
Remove a member (also how an unaccepted invite gets withdrawn).
Only billing_admins may remove other members. Self-removal (an identity removing itself, i.e. leaving the account) is allowed without the billing_admin check - api-v2.md is silent on this specifically, but it's a low-risk, clearly-reasonable capability (a member/admin should always be able to leave their own account).
Path Parameters
- account_id
string[path; required] - identity
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
delete https://billing.opteryx.app/v1/accounts/{account_id}/members/{identity}
Accept Invite
Request: post/v1/accounts/{account_id}/members/{identity}/accept
Invitee (authenticated as {identity}) flips pending -> active.
Stamps added_at/min_age_ms/eligible_at at acceptance time -
min_age_ms is read fresh from the account doc now, not from
whatever was on the invite, per api-v2.md's "snapshot at accept time"
behavior. 410 if the invite has expired; 404 if there's no pending
invite for this identity on this account.
Path Parameters
- account_id
string[path; required] - identity
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://billing.opteryx.app/v1/accounts/{account_id}/members/{identity}/accept
Get Payment Method
Request: get/v1/accounts/{account_id}/payment-methods
Return the single attached payment method, or null if none is attached.
A bare null (rather than e.g. a 404) is used because "no payment
method attached" is a normal, expected state for this resource - not an
error - matching how billing_account: null is treated as a normal
free-tier state elsewhere in this design.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject | null) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://billing.opteryx.app/v1/accounts/{account_id}/payment-methods
Attach Payment Method
Request: post/v1/accounts/{account_id}/payment-methods
Attach a Stripe payment method, replacing whatever's already attached.
Verifies the token against Stripe's API before trusting it, then - if a different payment method was already attached - detaches the old one on Stripe's side so it isn't left orphaned there.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:AttachPaymentMethodRequest- stripe_payment_method_id
string[required] - brand
string[required] - last4
string[required]
- stripe_payment_method_id
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://billing.opteryx.app/v1/accounts/{account_id}/payment-methods
Detach Payment Method
Request: delete/v1/accounts/{account_id}/payment-methods
Detach the account's payment method, if any. Idempotent - detaching when nothing is attached is a no-op 204, not an error.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 204 — Successful Response
- 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
delete https://billing.opteryx.app/v1/accounts/{account_id}/payment-methods
Create Payment
Request: post/v1/accounts/{account_id}/payments
Ad hoc charge against the account's attached payment method.
400 if no payment method is attached. A real PaymentIntent is created
and confirmed synchronously against the attached Stripe payment method
id - this moves real money, so this deliberately does not fake a
successful response; a Stripe failure surfaces as 402.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:CreatePaymentRequest- amount
integer[required] - currency
string[optional] Default:usd - description
string | null[optional]
- amount
Responses
- 201 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://billing.opteryx.app/v1/accounts/{account_id}/payments
List Account Workspaces
Request: get/v1/accounts/{account_id}/workspaces
Manage-workspaces listing. Caller must be an active member of the account (any role).
A genuine two-database read per workspace (api-v2.md): this service's
own workspaces/{name} doc for billing_account/members, plus the
catalogs database's $properties doc (via an OpteryxCatalog handle)
for lock/delete state. Active (non-deleted) workspaces are visible to
any account member; a soft-deleted-but-still-in-grace-period workspace
is included only for callers who are an owner in that specific
workspace's own members list.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonarray<object>) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://billing.opteryx.app/v1/accounts/{account_id}/workspaces
List Invoices
Request: get/v1/invoices
Placeholder: return an empty invoices list.
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonarray<object>) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://billing.opteryx.app/v1/invoices
Get Invoice
Request: get/v1/invoices/{invoice_id}
Placeholder: return a minimal invoice representation.
Path Parameters
- invoice_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://billing.opteryx.app/v1/invoices/{invoice_id}
Create Workspace
Request: put/v1/workspaces/{name}
Idempotent create. Caller must be an active billing_admin on
body.billing_account.
Writes workspaces/{name} (billing_account + members), then calls
policy.opteryx's genesis-grant endpoint (POL-2) for the explicit member
list, forwarding the caller's JWT.
Idempotency: if workspaces/{name} already exists with the same
billing_account/members as requested, this is treated as a retry -
the genesis call is re-attempted (policy.opteryx's own 409 in that case
just means the earlier attempt's genesis call already succeeded, so
that's treated as success too), and the existing doc is returned rather
than erroring. If the doc exists with different billing_account/
members, that's a real name conflict: 409, no genesis call made.
Path Parameters
- name
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:WorkspaceCreateRequest- billing_account
string[required] - members
array<WorkspaceMemberGrant>[required]
- billing_account
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
put https://billing.opteryx.app/v1/workspaces/{name}
Delete Workspace
Request: delete/v1/workspaces/{name}
Starts the 24h soft-delete. Caller must be an active billing_admin
on the workspace's billing account. 423 if the workspace is currently
locked.
Path Parameters
- name
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
delete https://billing.opteryx.app/v1/workspaces/{name}
Lock Workspace
Request: put/v1/workspaces/{name}/lock
Workspace owner sets the lock - workspace-level (checked against
this workspace's own members list), not billing-account-level.
Path Parameters
- name
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
put https://billing.opteryx.app/v1/workspaces/{name}/lock
Unlock Workspace
Request: delete/v1/workspaces/{name}/lock
A different owner clears the lock. Not a hard access-control
boundary (per api-v2.md) - the point is a second identity on record,
so a straightforward identity-inequality check against the current
locked-by is sufficient.
Path Parameters
- name
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
delete https://billing.opteryx.app/v1/workspaces/{name}/lock
Restore Workspace
Request: post/v1/workspaces/{name}/restore
Within the 24h grace window, un-deletes. Owner only.
Constructs the catalog handle with include_deleted=True - restoring a
deleted workspace requires reaching it in the first place.
Path Parameters
- name
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)