API reference (OpenAPI)
How to find, generate, and use the OpenAPI 3.1 spec for the Infrawrench cloud API.
Web only. The desktop app has no public HTTP surface — it talks to plugins directly.
The Infrawrench cloud API is described by an OpenAPI 3.1 document that’s generated from the running server’s plugin registry. Plugin IDs and resource type IDs in the spec are real enums, not free-form strings — if a plugin isn’t installed, its ID won’t appear.
Where to find it
| What | URL | Notes |
|---|---|---|
| Interactive reference | /docs | Scalar UI. Browseable, with try-it-out. |
| Raw spec | /openapi.json | OpenAPI 3.1, served directly. |
| Build artifact | app/packages/web/openapi.json | Checked into the repo. Regenerate with pnpm --filter @infrawrench/web generate:openapi. |
Both runtime endpoints are public — they describe the API surface, not any private data. A running server advertises its own origin as the spec’s server, so the “try it” panel on /docs talks to the deployment you’re reading it on.
Internal routes
A handful of routes exist only so our own clients (and third-party webhook senders) can talk to the server. They carry no stability promise, so they’re excluded from /docs and /openapi.json:
| Route | Why it’s internal |
|---|---|
/api/admin/* | Platform-operator surface, gated on an email allowlist — 403 for everyone else. |
/api/v1/webhooks/* | Inbound, signature-verified calls from third parties. Never called by you. |
/api/auth/sign-in, /api/auth/sign-out, /callback | Browser redirect flow for the session cookie. |
/api/v1/sync/* | Bi-directional resource sync used by the desktop app. |
/api/org/{orgId}/ws-token | Mints short-lived tokens for our WebSocket gateway. |
/api/push/*, /api/org/{orgId}/push/* | Mobile push-notification device registration and preferences. |
They’re still in the source and in the checked-in app/packages/web/openapi.json, tagged with an x-internal: true extension — the server strips those operations (and any schemas only they used) before publishing the spec. Build against them at your own risk; they can change or disappear without notice.

SDK code samples
Every operation on /docs carries an x-codeSamples entry per client SDK, so the code panel shows the call the way each generated client actually spells it — client.accounts.sync({ id }) in TypeScript, client.Accounts.Sync(ctx, …) in Go, client.resources().secretVersions().add(…) in Java — alongside the generic curl/HTTP snippets. Pick the language in the client selector above the example.
The samples are rendered from the same intermediate representation the SDK generator consumes, so they cannot drift from the published packages: renaming a call in the generator renames it in the docs in the same commit. They exist only on the served spec (/openapi.json and /docs) — the committed app/packages/web/openapi.json stays snippet-free so its diffs show API surface changes rather than re-rendered examples.

Authentication
The published spec advertises a single scheme:
bearerAuth— a WorkOS access token (JWT), an Infrawrench API key (iwk_), or an agent credential (iwa_). Send it asAuthorization: Bearer <token>.
curl https://app.infrawrench.com/api/org/$ORG_ID/accounts \
-H "Authorization: Bearer $INFRAWRENCH_API_KEY"
The three /api/agent/* registration routes are the exception to all of this: POST /api/agent/identity takes no authentication at all, because it is how a client with no credentials gets one. It is rate limited per source address instead. See agent authentication.
The server also accepts the wos-session cookie — that’s how the web UI authenticates — but the only way to get one is the browser sign-in redirect, which is internal. It’s left out of the published spec so generated snippets and Scalar’s “try it” panel default to bearer auth.
API keys must include the right scope for the operation. Scopes are permission strings — the same vocabulary used for roles. Every operation that requires a permission carries an x-required-permission extension naming it, e.g.:
post:
summary: Create an account
x-required-permission: accounts:write
The Permission enum component lists every recognised scope. Granted scopes can use wildcards (resources:*:read, *). Older keys created with sync:read / sync:write are migrated automatically the next time they authenticate to resources:read / resources:write.
A key’s scopes are a ceiling, not a grant: the server intersects them with the current role of the user who created the key, so x-required-permission must be satisfied by both. See API keys.
What a key can reach
Every operation under /api/org/{orgId}/ accepts an API key, with five exceptions listed below. A key is pinned to the organization it was minted in — presenting it against another org’s {orgId} is a 403, whatever the key holds — and it is checked against exactly the same x-required-permission a signed-in user is. Widening authentication to keys did not widen authorization: an operation a Member cannot perform in the browser is one a *-scoped key held by that Member cannot perform either.
These operations are closed to API keys and answer 403 with an explanatory error, even for a key scoped * held by an Owner:
| Operations | Why |
|---|---|
All of /api/org/{orgId}/api-keys | A key that can mint keys can mint a longer-lived one, so revoking it would not end the access. Reads go with it — enumerating an org’s credentials is reconnaissance. |
All of /api/org/{orgId}/billing | Subscription and payment changes. Every response is a Stripe URL meant for a browser. |
All of /api/org/{orgId}/push | Device registration and per-device notification preferences describe a person’s phone. |
POST/PUT/PATCH/DELETE on /team | Invites, role assignment and member removal manufacture or destroy durable authority for other people. GET still works, so automation can inventory members. |
POST/PUT/PATCH/DELETE on /access-requests | Break-glass exists to put a human in the loop. GET still works, so a monitor can watch the queue. |
Three more surfaces are unreachable because they are not org-scoped at all and never accepted bearer credentials: /api/profile/* (password, MFA, email change, session revocation), /api/orgs (creating and deleting organizations), and /api/admin/*. The account-security operations under /api/profile additionally require a recent interactive sign-in, which no bearer principal can satisfy.
The one unauthenticated route
GET /api/status/{slug} takes no credentials at all. It serves a
public status page, which exists precisely for callers who have no
account, so it carries no x-required-permission and needs no bearer token. Sending one is
pointless and slightly worse than pointless — the endpoint is meant to be anonymous.
Everything else in the spec requires authentication. The slug is the page’s only access control, so treat it like a credential you have chosen to share: an unpublished page and an unknown slug both answer 404, and the page’s owner can revoke a leaked link by rotating the slug.
Generating client SDKs
There are first-party client SDKs for nine languages —
TypeScript, Python, Ruby, Go, Java, C#, PHP, Swift and Rust — generated from
this spec, MIT-licensed, with calls namespaced to match the routes
(client.accounts.sync({ id })). Reach for those first. Build them from a
checkout with:
pnpm --filter @infrawrench/web generate:sdk
For a language we don’t ship, the spec carries a stable operationId on every
operation, so any OpenAPI generator works:
openapi-generator-cli generate -i openapi.json -g kotlin -o ./client
Regenerating the spec after a change
The spec is built by walking the plugin registry, so any time you add/remove a plugin or resource type the spec changes. To refresh:
pnpm --filter @infrawrench/web generate:openapi
Commit the resulting openapi.json so PR diffs show API surface changes. That
command also refreshes the generated SDKs if the API
version changed; they’re build output and are not committed.
Strictness
- Every request and response body has a Zod schema. Object schemas use
additionalProperties: falseunless they wrap genuinely free-form plugin data (in which case the schema is namedJsonObjectand explicitly opted in toadditionalProperties: true). pluginIdandtypeIdpath parameters are typed as enums of the live plugin / resource-type IDs.resourceIdfollows the host’s composite shapepluginId:accountId:externalIdand is regex-validated in the schema.- Error responses share a single
Errorschema ({ error: string }), except the step-up 403 described below. - A handful of account-security operations under
/api/profilereturn aReauthenticationRequired403 ({ error, code: "reauthentication_required" }) when the caller’s sign-in is not recent enough. Branch oncode, not the message: the request was well-formed and will succeed once the user signs in again. Bearer principals never satisfy this check — these operations are browser-only by design.