What this API does
One API covers a tenant’s whole life: request it, get it approved by a second operator, watch it build, mint its first ingest token, and eventually deprovision it with a recorded justification. Both mutating lifecycle transitions are maker-checker: the operator who requests a tenant cannot approve it, and the operator who requests a deprovision cannot approve that either.
Full field-level reference for the entry point: Request a Tenant.
States
failed is not a resting state. A flow that failed with retries remaining sits on a timer and
re-enters provisioning on its own; the detail route derives presentation: "backing-off" for
exactly that case. Only paused needs a human.
The integration contract
Three rules make a client correct across every state.- Poll
poll_url, don’t guess. The202hands back the path. Readstateandpresentation;next_wake_attells you when the flow will next be tried, and isnullwhen nothing is scheduled. - Treat absent keys as absent, not as zero.
placementis missing until the registry row exists.intake_summaryis missing once deprovisioning redacts the intake. Both must render without 500-ing. - Never derive readiness from the state name. Use
placement.schema_version.
Reading the detail route
GET /v1/tenants/{slug} returns the flow’s state plus two optional blocks:
presentation is derived from the state’s class in the flow definition rather than read from a
stored column, so it cannot drift from the state it describes.
Streaming
state, attempts and reason. Use curl -N rather than
Postman, which buffers SSE and renders it poorly.
Ingest setup
201
db-ready is 409, and a duplicate token name for the same
tenant is 409 (names are unique per tenant so an operator can revoke by name).
Deprovisioning
justification is required and must be at least 20 characters — 19 is a 422. It is not
ceremony: the justification is rendered as the primary content of the approval card, so it is the
only thing the approving operator reads about why a tenant is being destroyed.
Deprovisioning exports before it drops, and redacts the intake record (which is why
intake_summary disappears from the detail route afterwards).
Postman collection
Committed alongside this page:api/collections/quint-deploy-gateway.postman_collection.jsonapi/collections/quint-deploy-gateway.remote.postman_environment.json
docs/api/quint-deploy-gateway.postman_collection.json in the platform repo.
Re-copy rather than edit the copy — with one deliberate difference: the published copy carries no
operator WorkOS client id, no control-plane hostname, and no runner URL, because this docs site is
public. Fill those in from the operator console’s configuration.
Five folders:
Every secret-typed value in the committed environment ships empty. Fill
base_url,
workos_client_id, workos_client_secret and the operator/approver emails for the gateway you
are pointing at.
Getting a token
The Session folder runs a two-step exchange against WorkOS:Traps worth knowing
- Postman environments must hold configuration only. Environment scope resolves before
collection variables, so an environment that also declares
slugshadows anything the collection scripts set — the request then interpolates a stale value and fails for a reason absent from the request you are reading. pm.info.iterationis not a poll counter. It is the Runner’s iteration index, so in a single-iteration run it stays0and aniteration < Nbudget never fires. Count in a variable. And neversetNextRequest(null)to leave a poll loop — it ends the entire run and silently skips every later request.