Skip to main content
POST
Internal operator API. This route is served by the deploy gateway on the private control-plane origin, not by api.quintai.dev. It is not a customer surface: there is no self-service tenant creation. Every call is authenticated as a named human operator holding the operator role, and creating a tenant requires a second operator to approve it.Do not confuse this with Create Tenant (POST /v1/tenant, singular), which is the product API’s organization record.

Why 202 and not 201

The response is 202 Accepted. Nothing has been created when it returns — an onboarding flow has been opened in state requested, and that flow is still subject to approval or rejection. Answering 201 with a Location header pointing at a tenant that a second operator may still reject would be a lie the console renders as success. The 202 body hands back a poll_url instead, and the tenant becomes real only after approval.

Base URL and authentication

The token is a WorkOS Magic Auth JWT minted against the operator WorkOS client, which is a different client from the product’s. The gateway pins the token issuer to that client id, so a token minted by the product client does not verify here regardless of the user’s roles. The operator’s role must be operator; a viewer token is refused on every write route.
A missing token and a malformed token both return the same byte-pinned 403. There is no 401 on this surface, and no response body distinguishes “no credential” from “bad credential”.

Request body

string
required
URL-safe tenant identifier, and the tenant’s permanent handle. Maximum 56 characters — the physical database is named tenant_<slug> and Postgres truncates identifiers at 63 bytes (NAMEDATALEN), so a longer slug would silently collide with another tenant’s database.
string
required
Human-readable organization name, shown on the approval card.
string
required
Email of the tenant’s owner. Must contain @.
string
required
Placement region. A cell in this region must exist and have spare capacity, or the flow fails placement with no_placeable_cell. Check placement before requesting.
string
required
One of core, team, enterprise.These three are the whole set — the value is enforced by a database CHECK constraint as well as by the API, so an unrecognised tier is refused rather than stored. trial is not a tier; a trial is a time window on a tenant, not a plan.
string
required
Must be shared. The schema also names dedicated and self-hosted, but only shared is implemented today and anything else is refused with 422.
string
default:"monitor"
monitor or enforce. Omitted means monitor — a new tenant observes before it blocks.
integer
Expected developer endpoint count, for capacity planning. Recorded, not enforced.
integer
Expected server endpoint count. Recorded, not enforced.
string
Free-text context for the approver. Maximum 2000 characters.
Unknown fields are rejected, not ignored. The decoder refuses any key it does not recognise with a 400, and refuses a second JSON document in the body. A typo in a field name fails loudly instead of silently dropping the value — so plan_teir is an error, not a tenant provisioned on a default plan.gateways is refused with 422 specifically: gateway wiring is not part of intake.

Response

string
The slug you requested, echoed back.
string
Always requested on a fresh request.
string
Path to the detail route. Poll this for progress.
string
Path to the SSE event stream for the same flow.

Errors

After the 202

The 202 is the start of a pipeline, not the end of a request. The states a tenant moves through:
Two states rest until a human acts: pending-approval waits for a second operator, and the flow engine deliberately will not wake it, because nothing but a decision can move it. paused holds a flow that exhausted its retries, and needs an explicit resume. Poll GET /v1/tenants/{slug} for the current state, or stream GET /v1/tenants/{slug}/events. The detail route’s presentation field is derived from the flow definition rather than stored, and reports backing-off for a flow retrying on a timer — which is why a state of failed is not terminal.
provisioned means placed, not usable. A tenant reaches provisioned when it has a cell and a database_name; the physical database may not exist yet. The signal that the database is real and migrated is a non-null placement.schema_version, which is the db-ready state.Anything that routes traffic must refuse a null schema_version rather than default it, and minting an ingest token before db-ready is refused with 409 — a token for a tenant with no database would authenticate and then drop every event.

Approval is a separate call, by a different person

The approver must not be the requester. Self-approval returns 403 — the rule working, not a misconfiguration.
The refusal code differs by surface. Self-approval on a tenant route returns 403; on a deploy route it returns 409. A console that treats every 403 as “session expired” will tell operators to log in again when the real answer is “ask a colleague to approve.”
To refuse instead, POST /v1/tenants/{slug}/reject with a reason — an empty or very short reason is 422, because the reason is what an auditor reads later. cancel and resume take the same path shape.

The full lifecycle

The token mint returns the raw token exactly once. It is never written to the flow journal, the tenant event log, or the service logs — only a SHA-256 hash and a short prefix are stored. If it is not captured from that one response body, mint another; there is no route that reveals it again. Deprovisioning requires a justification of at least 20 characters, and is approved through the same /approve route — the flow type is resolved from the slug, not from the path. While a deprovision is live, reject, cancel, resume and the event stream all act on the offboard flow rather than on the completed provisioning flow.

Postman

Every call above ships as a runnable request in the deploy gateway collection, including a Refusals folder that asserts each error in the table above.