Control API
Base URL: https://control.opteryx.app
Overview
Billing accounts and membership, payment methods and invoices, workspace lifecycle, and the access policies that govern who can reach what.
Endpoints
| Service | Docs |
|---|---|
List workspace policiesget/v1/access/workspace/{workspace} |
View |
Export effective permissionsget/v1/access/workspace/{workspace}/effective-permissions.csv |
View |
Bootstrap a new workspace's initial policiespost/v1/access/workspace/{workspace}/genesis |
View |
Create policypost/v1/access/workspace/{workspace}/policies |
View |
Get policy detailsget/v1/access/workspace/{workspace}/policies/{policy_id} |
View |
Update policyput/v1/access/workspace/{workspace}/policies/{policy_id} |
View |
Delete policydelete/v1/access/workspace/{workspace}/policies/{policy_id} |
View |
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 Domainsget/v1/accounts/{account_id}/domains |
View |
Claim Domainpost/v1/accounts/{account_id}/domains |
View |
Release Domaindelete/v1/accounts/{account_id}/domains/{domain} |
View |
Verify Domainpost/v1/accounts/{account_id}/domains/{domain}/verify |
View |
Get Idp Policyget/v1/accounts/{account_id}/idp-policy |
View |
Put Idp Policyput/v1/accounts/{account_id}/idp-policy |
View |
Delete Idp Policydelete/v1/accounts/{account_id}/idp-policy |
View |
List Denialsget/v1/accounts/{account_id}/idp-policy/denials |
View |
Preview Idp Policypost/v1/accounts/{account_id}/idp-policy/preview |
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 |
Get Workspace Guard Propertiesget/v1/workspaces/{name}/properties |
View |
List workspace policies
Request: get/v1/access/workspace/{workspace}
Tags: Access Control
Get all access policies for a workspace. Requires owner or admin access.
Path Parameters
- workspace
string[path; required] Workspace name
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonWorkspacePoliciesResponse) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://control.opteryx.app/v1/access/workspace/{workspace}
Export effective permissions
Request: get/v1/access/workspace/{workspace}/effective-permissions.csv
Tags: Access Control
Export a CSV of who has access to every dataset and view in the workspace, resolving each policy's pattern against the workspace's actual catalog. A policy scoped to a wildcard pattern appears on one row per dataset it covers, so the same policy_id can repeat across many rows. Owner-only: this is a full map of who can reach every resource in the workspace, not a single grant.
Path Parameters
- workspace
string[path; required] Workspace name
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — CSV export of effective permissions. (
application/jsonobject,text/csvstring) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://control.opteryx.app/v1/access/workspace/{workspace}/effective-permissions.csv
Bootstrap a new workspace's initial policies
Request: post/v1/access/workspace/{workspace}/genesis
Tags: Access Control
Create the initial set of access policies for a brand-new workspace, one policy per {identity, role} pair, each scoped to the whole workspace (pattern {workspace}.*). This is a trusted bootstrap operation for whatever creates the workspace record in the first place (e.g. billing.opteryx's workspace-create endpoint) to hand it an explicit member list at creation time, rather than every caller becoming sole owner. Refuses with 409 if the workspace already has any policy at all -- this can only be used once, to bootstrap a workspace that doesn't have policies yet, not to add owners to one that already does.
Path Parameters
- workspace
string[path; required] Workspace name
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:GenesisGrantRequest- grants
array<GenesisGrant>[required] Identity/role pairs to grant over the whole workspace
- grants
Responses
- 201 — Successful Response (
application/jsonGenesisGrantResponse) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://control.opteryx.app/v1/access/workspace/{workspace}/genesis
Create policy
Request: post/v1/access/workspace/{workspace}/policies
Tags: Access Control
Create a new access policy for a user in the workspace.
Path Parameters
- workspace
string[path; required] Workspace name
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:CreatePolicyRequest- principal
Principal[required] User to grant access to - role
string[required] Role to grant. See Security & Permissions for what each role can do. Allowed values:owner,admin,writer,reader - pattern
string[required] Resource pattern (e.g., 'analytics.*')
- principal
Responses
- 201 — Successful Response (
application/jsonCreatePolicyResponse) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://control.opteryx.app/v1/access/workspace/{workspace}/policies
Get policy details
Request: get/v1/access/workspace/{workspace}/policies/{policy_id}
Tags: Access Control
Get detailed information about a specific policy.
Path Parameters
- workspace
string[path; required] Workspace name - policy_id
string[path; required] Policy ID
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonPolicyDetail) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://control.opteryx.app/v1/access/workspace/{workspace}/policies/{policy_id}
Update policy
Request: put/v1/access/workspace/{workspace}/policies/{policy_id}
Tags: Access Control
Update an existing access policy.
Path Parameters
- workspace
string[path; required] Workspace name - policy_id
string[path; required] Policy ID
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:UpdatePolicyRequest- role
string[required] Updated role. See Security & Permissions for what each role can do. Allowed values:owner,admin,writer,reader - pattern
string[required] Updated resource pattern
- role
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
put https://control.opteryx.app/v1/access/workspace/{workspace}/policies/{policy_id}
Delete policy
Request: delete/v1/access/workspace/{workspace}/policies/{policy_id}
Tags: Access Control
Remove an access policy from the workspace.
Path Parameters
- workspace
string[path; required] Workspace name - policy_id
string[path; required] Policy ID
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
delete https://control.opteryx.app/v1/access/workspace/{workspace}/policies/{policy_id}
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://control.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://control.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://control.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://control.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://control.opteryx.app/v1/accounts/{account_id}
List Domains
Request: get/v1/accounts/{account_id}/domains
List claimed domains and their verification state.
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://control.opteryx.app/v1/accounts/{account_id}/domains
Claim Domain
Request: post/v1/accounts/{account_id}/domains
Claim a domain, returning the TXT record to publish.
Idempotent: re-claiming a domain this account already holds returns the same token and leaves any existing verification intact, rather than resetting it. Claiming a domain another account has verified is a 409 - an unverified claim elsewhere is not an obstacle, since it proves nothing.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:DomainClaimRequest- domain
string[required]
- domain
Responses
- 201 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://control.opteryx.app/v1/accounts/{account_id}/domains
Release Domain
Request: delete/v1/accounts/{account_id}/domains/{domain}
Release a claimed domain.
Note this can widen who may sign up - a domain that was routing new
@customer.com users into this account stops doing so - which is why it
is billing_admin-only and audited.
Path Parameters
- account_id
string[path; required] - domain
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://control.opteryx.app/v1/accounts/{account_id}/domains/{domain}
Verify Domain
Request: post/v1/accounts/{account_id}/domains/{domain}/verify
Check DNS for the expected TXT record and mark the domain verified.
A DNS failure and a missing record are reported differently on purpose: "we could not resolve this" is an operator's cue to wait and retry, while "we resolved it and the record is not there" is a cue to go and publish it. Collapsing both into one message is how domain verification gets a reputation for being flaky.
Path Parameters
- account_id
string[path; required] - domain
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://control.opteryx.app/v1/accounts/{account_id}/domains/{domain}/verify
Get Idp Policy
Request: get/v1/accounts/{account_id}/idp-policy
Read the account's sign-in requirements.
Any active member may read: knowing the rules you must satisfy to sign in is not privileged, and hiding them from members only produces support tickets. Changing them is billing_admin-only.
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://control.opteryx.app/v1/accounts/{account_id}/idp-policy
Put Idp Policy
Request: put/v1/accounts/{account_id}/idp-policy
Replace the account's sign-in requirements.
Whole-document replace, not a patch: a policy that refuses sign-ins should be read in full at the point of change, not assembled from a diff against something the operator may not have looked at.
version is bumped on every write. A refresh session issued under a lower
version is refused at mint and sent back through interactive sign-in, so
tightening a policy cannot be outlived by sessions that predate it.
Path Parameters
- account_id
string[path; required]
Query Parameters
- force
boolean[query; optional] Proceed even if this would deny your own sign-in Default:false
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:IdpPolicyRequest- enforced
boolean[optional] Default:false - providers
array<string>[optional] - microsoft
MicrosoftPolicy[optional] - google
GooglePolicy[optional] - allow_password_auth
boolean[optional] Default:true - machine_credentials
MachineCredentialsPolicy[optional] - require_mfa
boolean[optional] Default:false - max_auth_age_hours
integer | null[optional] - max_session_hours
integer | null[optional] - email_domains
array<string>[optional]
- enforced
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
put https://control.opteryx.app/v1/accounts/{account_id}/idp-policy
Delete Idp Policy
Request: delete/v1/accounts/{account_id}/idp-policy
Remove the account's sign-in requirements entirely.
Deliberately unguarded, unlike the write path: removing a restriction can only widen who may sign in, so it cannot lock anybody out. It still needs a fresh interactive session, because turning the control off is exactly what an attacker holding a stale machine credential would want to do.
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://control.opteryx.app/v1/accounts/{account_id}/idp-policy
List Denials
Request: get/v1/accounts/{account_id}/idp-policy/denials
Recent sign-ins this account's policy refused.
Without this, a customer's admin cannot answer "why can't Bob sign in" and every denial becomes a support request that needs someone to read our logs. It is the endpoint most likely to be dropped for time and the one that most determines whether the feature is operable.
Denials are emitted to the audit stream by authenticate.opteryx at mint
(app/idp_policy.py::audit_decision); this reads them back from the
account's idp_denials subcollection, which the log pipeline populates.
Returns an empty list rather than 404 when nothing has been recorded - "no
denials" is the normal, healthy state and must not read as an error.
Path Parameters
- account_id
string[path; required]
Query Parameters
- limit
integer[query; optional] Default:50
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
get https://control.opteryx.app/v1/accounts/{account_id}/idp-policy/denials
Preview Idp Policy
Request: post/v1/accounts/{account_id}/idp-policy/preview
Report who a candidate policy would admit, writing nothing.
This is what makes enabling a policy something other than a blind lockout - an admin needs to see the three contractors who sign in with Google before they hear about it from a support ticket. The UI calls this on every change and blocks the save button on its result.
Path Parameters
- account_id
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Request Body
- Content-Type:
application/jsonSchema:IdpPolicyRequest- enforced
boolean[optional] Default:false - providers
array<string>[optional] - microsoft
MicrosoftPolicy[optional] - google
GooglePolicy[optional] - allow_password_auth
boolean[optional] Default:true - machine_credentials
MachineCredentialsPolicy[optional] - require_mfa
boolean[optional] Default:false - max_auth_age_hours
integer | null[optional] - max_session_hours
integer | null[optional] - email_domains
array<string>[optional]
- enforced
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://control.opteryx.app/v1/accounts/{account_id}/idp-policy/preview
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://control.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
string[required] - email
string[required] - role
string[required]
- identity
Responses
- 201 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)
Try it live
post https://control.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://control.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://control.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://control.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://control.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://control.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://control.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://control.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: this service's own
workspaces/{name} doc for billing_account/members, plus the
catalogs database's $properties doc (via an OpteryxCatalog handle)
for lock state and the two protection guards.
DROP WORKSPACE removes the catalog's $properties doc outright - it has
no reason to touch this service's own workspaces/{name} registration
doc (billing_account/members are this service's own concern, not the
catalog's) - so a row whose properties read comes back empty names a
workspace that no longer exists; it is skipped rather than listed.
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://control.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://control.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://control.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.
A workspace exists in THREE places, and all three are written here, in a deliberate order:
catalogs/{name}/$properties- the catalog namespace, stamped withbilling-account-id. This is whatopteryx_catalogmeans by a workspace; without it everyCREATE TABLEin the workspace fails withWorkspaceNotFoundno matter what the other two say.workspaces/{name}in this service's own database - billing_account and members.- The genesis access grants, bootstrapped in-process via
_bootstrap_genesis_policies(this was an HTTP call to policy.opteryx before that service merged into this one).
ORDERING. (3) is a one-way door: this service cannot revoke a grant it caused, genesis refuses a self-revoke, and it answers 409 for the rest of time once any policy exists on the name. (1) and (2) are local documents this service can re-read and overwrite. So both reversible writes run first and the irreversible one runs last, and a failure anywhere leaves a state a retry can recognize and finish.
(1) comes first because it is also the claim record. It is the one document
that says which billing account holds this name, it is where
scripts/reserve_workspace_names.py records a hold, and it is durable
before either of the other writes - so a retry after a crash at any point
can tell "my own half-finished attempt" from "someone else's name", which
is exactly what the code could not do before (see billing-test-suite/
STATUS.md: a genesis timeout stranded a workspace name permanently, because
nothing durable recorded the claim until after genesis had already
committed).
Failure modes, all recoverable by re-issuing the same PUT:
- (1) fails: 504, nothing else written, name still free.
- (2) fails: the namespace is claimed by this billing account and no grants exist. A retry adopts it; another account's PUT is refused.
- (3) fails: the workspace is visible in the account listing and can be deleted, and a retry re-attempts genesis - a 409 then means the earlier attempt's grants already landed, which is success. This is the trade named in STATUS.md: a workspace briefly without grants, rather than a name permanently without recourse. It carries no charge (billing is metered off usage events, and an empty workspace produces none).
Idempotency: if workspaces/{name} already exists with the same
billing_account/members as requested, this is treated as a retry 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 bootstrap attempted.
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://control.opteryx.app/v1/workspaces/{name}
Get Workspace Guard Properties
Request: get/v1/workspaces/{name}/properties
The workspace's guard flags, for ANY workspace the caller owns.
The account listing above answers the same question, but only for
workspaces registered with a billing account - it is driven by
where("billing_account", "==", ...), so a workspace that predates
billing.opteryx or was created any other way is simply absent from it.
That left the UI unable to read its own setting back for those, which is
not a state a protection control can be in: a switch that cannot report
what it is set to is a switch nobody can trust.
So this is keyed by workspace and nothing else. Authorization comes from
the access policies (_check_workspace_access, owner/admin on the
workspace itself), NOT from billing-account membership - the same reason
the route exists at all, since an unregistered workspace has no billing
account to be a member of. The catalog handle is built by name, which
needs no billing row either.
Returns the same resolved booleans as the listing, so a caller can use either source without knowing which one answered.
Path Parameters
- name
string[path; required]
Header Parameters
- authorization
string | null[header; optional]
Responses
- 200 — Successful Response (
application/jsonobject) - 422 — Validation Error (
application/jsonHTTPValidationError)