# Management API

> Configure an INFRO organization as code: projects, gateway keys, connections, routes, budgets and policies through one versioned, token-authenticated API.

Source: https://infro.io/docs/manage/overview

> **Note:** INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See [Early access](https://infro.io/early-access).

> **Warning:** **Built, not yet available.** The management API is built and not deployed: `/manage/v1` does not answer requests yet, and nothing on this page can be used today. These pages describe the contract it ships with.

The management API lets a program configure your organization: Terraform, a CI pipeline, the INFRO CLI or a script of your own. It lives at `https://api.infro.io/manage/v1`, takes a [management token](https://infro.io/docs/manage/tokens), and covers the configuration you set in the console today: projects, gateway keys, provider connections, routes, organization settings, member budgets, webhook endpoints, custom model rates, alert channels and rules, cost centers, commitments, guardrail policies and end-customer spend caps.

It does not serve inference. Model requests still go to `/v1` with a gateway key, and a management token is refused there.

## Same rules as the console

The management API does not re-implement anything. Each request is handed to the same handler the console uses, running as the person who created the token, with their role as it is now. Validation, plan limits, role checks and audit records are therefore the ones you already know from the console, and they cannot drift apart.

The consequence is that request bodies are the console's bodies, with the console's snake_case field names. A field the console form does not take, the API does not take either.

## Resources

| Resource type (URL segment) | Scope family | What delete does |
| --- | --- | --- |
| `projects` | `projects` | Archives the project. `?purge=true` deletes it, only if it has no gateway keys and no usage |
| `gateway_keys` | `keys` | Revokes the key |
| `provider_connections` | `connections` | Deletes the connection |
| `routes` | `routes` | Deletes the route |
| `org_settings` | `settings` | No delete: one object per organization, read and patched |
| `member_budgets` | `budgets` | No delete: patch `spend_limit` to `null` to remove a limit |
| `webhook_endpoints` | `webhooks` | Deletes the endpoint |
| `model_prices` | `prices` | Deletes the custom rate |
| `notification_channels` | `notifications` | Deletes the channel |
| `notification_rules` | `notifications` | Deletes the rule |
| `cost_centers` | `cost_centers` | Archives the cost center |
| `cost_center_rules` | `cost_centers` | Deletes the rule |
| `commitments` | `commitments` | Deletes the commitment |
| `guardrail_policies` | `guardrails` | Removes the policy |
| `end_user_cap_rules` | `end_users` | Deletes the rule and its per-end-customer overrides |

Every resource supports `GET /<type>` (a list, `{"object": "list", "data": [...]}`) and `GET /<type>/{id}`. Fields and operations for each are on [Resources](https://infro.io/docs/manage/resources). Three read-only data sources need any scope: `GET /organization` (the organization, its plan and this token), `GET /provider_kinds` and `GET /models`.

Three more resource types configure the [MCP gateway](https://infro.io/docs/mcp-gateway), all in the `mcp` scope family: `mcp_servers` (delete removes the server, its tools and its grants), `mcp_key_grants` (delete removes the grant) and `mcp_server_tools` (read and actions only; tools are created by discovery). They also need the Business plan's gateway to enable anything. See [Resources](https://infro.io/docs/manage/resources#mcp-gateway).

## Plans

| Plan | What a token can do |
| --- | --- |
| Developer | No management API. Every request returns `403` with `code: "plan_feature"`. |
| Startup | Read every resource, and create, change and delete projects and gateway keys. |
| Team and above | Every read and every write. |

A write the plan does not include returns `403` with `code: "plan_feature"` and the message names the plan it needs.

## Items, versions and managed_by

Every item carries `id`, `object`, `version` and `managed_by`, plus the resource's own fields. `version` is an integer that moves on every configuration change, whether made in the console or through the API. It does not move on runtime changes such as a connection's health or a key's last-used time, so a failover does not make your next plan report a change. Single-item reads and writes return the version as `ETag: "<version>"`.

`managed_by` records which tool last wrote the resource through this API: `terraform`, `cli` or `api`, or `null` when it was never written here. It is set from the `User-Agent` header (`terraform-provider-infro/<version>` or `infro-cli/<version>`; anything else is `api`). It is a label for people reading the console, not a lock and not a security control.

## Writing safely

- **`PATCH` and `DELETE` require `If-Match: "<version>"`** with the version you last read. Without it the request is refused with `400` and `code: "if_match_required"`. If the resource has changed since, the request is refused with `409`, type `conflict`, `code: "version_conflict"`, and the current version in the `ETag` header. Read it again, decide, and retry.
- **`PATCH` is a merge.** A field you leave out is unchanged; a field set to `null` is cleared.
- **`POST` honours `Idempotency-Key`** (up to 200 characters). A retry with the same key and the same body returns the first response with the header `Idempotent-Replayed: true`, and creates nothing new. A secret that was in the first response is `null` in the replay, with `secret_redacted: true`: it was shown once. The same key with a different body is refused with `400` and `code: "idempotency_key_reuse"`.
- Every write records `managed_by` and one `management_api.request` entry in the [audit log](https://infro.io/docs/org/audit-log), naming the token and its creator, next to the entry the console handler writes.

```bash
curl -i https://api.infro.io/manage/v1/routes/rt_01J9Z6 \
  -H "Authorization: Bearer $INFRO_MANAGEMENT_TOKEN"
# ETag: "4"

curl https://api.infro.io/manage/v1/routes/rt_01J9Z6 \
  -X PATCH \
  -H "Authorization: Bearer $INFRO_MANAGEMENT_TOKEN" \
  -H 'If-Match: "4"' \
  -H "Content-Type: application/json" \
  -d '{"max_retries": 2}'
```

## Export

`GET /export` returns every resource the token can read in one document: `{"object": "export", "organization_id", "exported_at", "resources": {"<type>": [...]}}`. Families the token cannot read are left out. It never contains a credential: provider secrets and Slack URLs are write-only, and a gateway key's or webhook endpoint's secret is shown only when it is created.

## OpenAPI document

The request bodies for every resource are published as an OpenAPI 3.1 document at `https://api.infro.io/manage/v1/openapi.json`. It needs no token.

## Known limits

- The `If-Match` check and the write are two separate steps. A change that lands in the milliseconds between them is not detected; the next read shows it.
- A guardrail policy has no row of its own. It uses the version of what it is attached to (the organization's settings, a project or a key), so an unrelated change to that project or key also moves the policy's version.
- Not managed through this API: SSO, SCIM, the admin credentials used for [cost reconciliation](https://infro.io/docs/cost/reconciliation), OpenTelemetry export settings, and members and invitations. Configure those in the console.
- Request bodies are the console's: a field the console form does not take, the API does not take either.

## Next

- [Management tokens](https://infro.io/docs/manage/tokens): creating one, scopes and expiry.
- [Resources](https://infro.io/docs/manage/resources): fields and operations for each resource type.
- [Terraform](https://infro.io/docs/manage/terraform) and [the CLI](https://infro.io/docs/manage/cli): the two tools built on this API.
- [AI assistants (MCP)](https://infro.io/docs/mcp): the same operations for Claude Code, Cursor, VS Code, Windsurf and Codex.
