Skip to main content
Internal operator API. Served by the deploy gateway on the private control-plane origin, authenticated per named human operator through the operator WorkOS client. There is no self-service tenant creation and no customer-facing route on this surface. Do not proxy it from the product dashboard.

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

provisioned is the state that misleads. It means the tenant has a home, not that it has a database. The one reliable signal is a non-null placement.schema_version. A console that treats provisioned as ready will show a working workspace for a tenant with no database behind it, and a token minted that early would authenticate and drop every event — which is why the mint route refuses with 409 before db-ready.
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.
  1. Poll poll_url, don’t guess. The 202 hands back the path. Read state and presentation; next_wake_at tells you when the flow will next be tried, and is null when nothing is scheduled.
  2. Treat absent keys as absent, not as zero. placement is missing until the registry row exists. intake_summary is missing once deprovisioning redacts the intake. Both must render without 500-ing.
  3. 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

Each frame is a JSON object carrying state, attempts and reason. Use curl -N rather than Postman, which buffers SSE and renders it poorly.

Ingest setup

201
token is returned exactly once. It is never written to the flow journal, the tenant event log, or service logs — the stored record is a SHA-256 hash plus the prefix, and the event log records only the prefix and which operator minted it. There is no route that reveals it again. Capture it from this response or mint another.
Two refusals: minting before 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).
Approval reuses the onboarding route. There is no separate offboard approve endpoint: POST /v1/tenants/{slug}/approve resolves whichever lifecycle flow is live from the slug. So while a deprovision is in flight, reject, cancel, resume and the event stream all act on the offboard flow, not on the completed provisioning flow.Once the offboard reaches a final class, the detail route falls back to the onboarding row. Confirm a completed deprovision by its export artifact, not by the detail route.

Postman collection

Committed alongside this page:
  • api/collections/quint-deploy-gateway.postman_collection.json
  • api/collections/quint-deploy-gateway.remote.postman_environment.json
Source of truth is 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:
Run it twice — once as the requesting operator, once as the approver — because no single operator can drive the collection end to end.
Use the operator WorkOS environment, not the product’s. These are separate WorkOS clients, and that separation is the security property: the gateway pins the token issuer to the operator client, so nothing the product mints verifies here.The failure is quiet. A product-environment API key returns a 200-shaped success for a user it cannot see, so you get what looks like a valid token and a 403 at the gateway with no clue why. If the exchange succeeds and the gateway still refuses, check which client you authenticated against before anything else.

Traps worth knowing

  • Postman environments must hold configuration only. Environment scope resolves before collection variables, so an environment that also declares slug shadows 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.iteration is not a poll counter. It is the Runner’s iteration index, so in a single-iteration run it stays 0 and an iteration < N budget never fires. Count in a variable. And never setNextRequest(null) to leave a poll loop — it ends the entire run and silently skips every later request.