# Management API resources

> Fields, operations and delete behaviour for every management API resource, with curl examples: projects, keys, connections, routes, budgets and more.

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

> **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.

Every resource lives under `https://api.infro.io/manage/v1/<type>`. List with `GET /<type>`, read with `GET /<type>/{id}`, create with `POST /<type>`, change with `PATCH /<type>/{id}` and remove with `DELETE /<type>/{id}`, where the resource allows it. `PATCH` and `DELETE` need `If-Match`; `POST` accepts `Idempotency-Key`. Those rules are on [the overview](https://infro.io/docs/manage/overview#writing-safely). The field lists below are the request bodies; the complete schemas are in the OpenAPI document at `https://api.infro.io/manage/v1/openapi.json`.

Responses carry each resource's fields as the console shows them, plus `id`, `object`, `version` and `managed_by`. Money fields are US dollars as numbers.

## Data

Besides configuration, the API reads what happened. These are not resources: they have no `version`, take no `If-Match` and are not in `/export` or Terraform. Prompt and completion content is never returned, whatever the organization's capture setting. History is limited to the plan's window. Paged answers carry `{object: "list", data, has_more, next_cursor}`.

| Operation | Scope | What it returns |
| --- | --- | --- |
| `GET /usage` | `analytics:read` | The same aggregation as `GET /v1/usage`, as JSON: buckets, optional `group_by`, estimated provider cost and unpriced requests. `format=csv` is refused. |
| `GET /requests` | `analytics:read` | The request log with the filters of `GET /v1/requests`, at most 100 a page (`after` = the previous `next_cursor`). |
| `GET /requests/{id}` | `analytics:read` | One request's metadata: attempts, route, guardrail outcomes, cost. |
| `GET /traces/{id}` | `analytics:read` | Every request of one run, in order, with the run's estimated cost. |
| `GET /findings`, `GET /findings/{id}` | `findings:read` | What INFRO noticed: anomalies, drift, budget crossings, failing connections, traffic changes. Filter by `status`, `type`, `severity`. |
| `POST /findings/{id}/acknowledge` | `findings:write` | Acknowledges a finding; audited, takes no ownership. |
| `GET /budget_status` | `budgets:read` | Every enforced spend limit with estimated spend in the current period. |
| `GET /audit_events` | `audit:read` | The audit log: `q`, `action`, `actor`, `from`, `to`, `limit`, `cursor`. |

## Secrets

| Secret | How it is handled |
| --- | --- |
| Gateway key `secret` | Returned once, in the response to `POST /gateway_keys` and to `POST /gateway_keys/{id}/rotate`. Never readable afterwards. |
| Webhook endpoint `secret` | Returned once, in the response to `POST /webhook_endpoints` and to `POST /webhook_endpoints/{id}/rotate_secret`. |
| Provider connection `secret` | Write-only. Accepted on create and on `POST /provider_connections/{id}/credential`, sealed, never returned. |
| Slack `slack_url` on a notification channel | Write-only. Accepted on create and patch, sealed, never returned. |

## Projects

- `name` (string, required) — Up to 80 characters.
- `slug` (string) — Create only. Up to 60 characters.
- `model_allowlist` (string[]) — Patch only. Models this project's keys may call; see [model allowlists](https://infro.io/docs/org/model-allowlists).
- `spend_limit` (object | null) — Patch only. `{monthly, behavior, alert_at?}`: `behavior` is `alert` or `block`, `alert_at` is a list of fractions up to 1. `null` removes it.
- `archived` (boolean) — Patch only. Archiving refuses the project's keys with `403 project_archived`; `false` restores it.

`DELETE /projects/{id}` archives the project. `DELETE /projects/{id}?purge=true` deletes it outright, but only if it has never had a gateway key and never served a request; otherwise it is refused with `409` and `code: "project_has_usage"`, and archiving is the way to remove it.

## Gateway keys

- `label` (string, required) — Up to 80 characters.
- `project_id` (string) — Create only. The project the key belongs to.
- `credit_limit` (object | null) — `{amount, period}`: `amount` from 0.01 to 1,000,000,000 USD, `period` is `daily`, `weekly`, `monthly` or `never`. See [spend controls](https://infro.io/docs/org/spend-controls).
- `rate_limit_rpm` (integer) — Requests per minute, one of 60, 100, 200 (the default), 300, 500, 1,000, 2,000, 5,000, 10,000, 20,000 or 50,000. Any other number is refused.
- `model_allowlist` (string[]) — Models this key may call.
- `allowed_routes` (string[]) — Route slugs the key is pinned to.
- `allowed_providers` (string[]) — Provider ids the key may use, such as `openai` or `openrouter` (as `GET /console/providers/kinds` lists them). `[]` or absent: every provider, including ones connected later. An unknown id is refused with `400` `unknown_provider`. See [Authentication](https://infro.io/docs/authentication#providers-a-key-may-use).
- `environment` (string | null) — The identifier of ONE of your organization's environments (`production`, `staging` and `development` by default). A key belongs to one environment because it is stamped on every request the key makes. `null`: no environment, which reaches only connections and routes set to every environment.
- `expires_at` (string | null) — An ISO 8601 time after which the key stops working.
- `observability_access` (string) — `none`, `scoped` or `organization`: which request history the key may read.
- `payload_access` (boolean) — Whether the key may read stored prompts and completions.
- `disabled` (boolean) — Patch only. `true` turns the key off: requests with it return `403` `key_disabled`, and nothing else about it changes. `false` turns it on again. Refused for a revoked key.
- `allowed_ips` (string[]) — CIDR ranges or addresses the key accepts requests from, up to 32; `[]` accepts any address. Stored in canonical form (`203.0.113.7` becomes `203.0.113.7/32`). See [Authentication](https://infro.io/docs/authentication#ip-allowlist).

The create response carries the key's `secret` once. `DELETE` revokes the key, and a revoked key no longer appears in lists or reads.

`POST /gateway_keys/{id}/rotate` replaces a key in one step and answers with the NEW key: its `secret` once, the same configuration, `rotated_from_key_id`, and `previous_key` with the old key's `expires_at` or `revoked_at`. `overlap_seconds` (default 86400, at most 604800) is how long the old key keeps working; `0` revokes it at once. The spend limit is shared across the rotation, so spend already counted stays counted. An optional `If-Match` refuses a key changed since it was read; a key that is revoked, expired, already rotated or in an archived project answers `409 key_not_rotatable`.

```bash
curl https://api.infro.io/manage/v1/gateway_keys \
  -H "Authorization: Bearer $INFRO_MANAGEMENT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: tenant-acme-key-1" \
  -d '{
    "label": "acme production",
    "project_id": "prj_01J9Z4",
    "environment": "production",
    "credit_limit": {"amount": 250, "period": "monthly"}
  }'
```

## Provider connections

- `kind` (string, required) — Create only. A provider kind from `GET /provider_kinds`.
- `name` (string, required) — Up to 80 characters. Unique among your connections of the same `kind`, ignoring case and repeated spaces: two providers can each have a key called `Production`. A clash answers `400` `duplicate_connection_name`.
- `secret` (string) — Create only, write-only. The provider credential.
- `fallback_secrets` (string[]) — Create only, write-only. Additional credentials pooled on this one connection, tried only when an earlier one is rejected or rate-limited. To name several keys of one provider and choose between them in a route, create one connection per key instead.
- `environments` (string[] | null) — The environments whose keys may use this connection, by identifier. `[]` or `null` is every environment, including keys with no environment. Listing every environment you have is not the same: a key with no environment, or in an environment added later, does not reach it.
- `environment` (string | null) — Deprecated: the single-value form. On write it means `[environment]` (or every environment when `null`); sent beside `environments`, it must be that list's first entry. On read it is the first entry of `environments`, or `null` when that is empty.
- `config` (object) — Kind-specific settings, such as a base URL where the kind allows one.
- `model_map` (object) — For kinds that serve the models you name.
- `timeout_ms` (integer) — 1,000 to 600,000.
- `alternate_base_urls` (string[]) — Up to 5 more API hosts for the same key, tried in order when its host cannot be reached. Only on a domain the provider owns and INFRO lists for its kind (`400` `alternate_host_off_domain`, or `alternate_host_not_allowed` for a kind that takes none); any HTTPS public host for a server whose URL you type. On `PATCH` it replaces the list; `[]` removes every alternate. See [Standby keys and alternate hosts](https://infro.io/docs/providers).
- `pool_role` (string) — `active` (default) shares the requests that name a model directly with the provider's other active keys; `standby` is tried only after every active key failed.
- `validate` (boolean) — Create only. Check the credential with the provider before answering.
- `status` (string) — Patch only. `enabled` or `disabled`.

Replace a credential with `POST /provider_connections/{id}/credential` and `{"secret": "...", "validate": true}`. It is a separate call so a rotation never rides along with a rename. See [Connect a provider](https://infro.io/docs/providers) for what each kind needs.

## Routes

- `slug` (string, required) — Create only. The name your application sends as `model`. Up to 64 characters.
- `name` (string) — Up to 80 characters.
- `description` (string | null) — Up to 500 characters.
- `strategy` (string) — `priority` or `weighted`.
- `environments` (string[] | null) — The environments whose keys may call this route. `[]` or `null` is every environment. Same rule as a connection's.
- `environment` (string | null) — Deprecated single-value form, as on a connection.
- `timeout_ms` (integer | null) — 1,000 to 600,000.
- `max_retries` (integer) — 0 to 3.
- `targets` (object[], required) — `{connection_id, model, weight?, enabled?}`, in order. On `PATCH`, `targets` replaces the whole target list.
- `rules` (object[]) — Up to 20 if-rules checked in order before `targets`: `{name, enabled?, match?, conditions, strategy?, fallthrough?, targets}`. On `PATCH`, `rules` replaces every rule; leaving it out keeps them. See [Rules](https://infro.io/docs/routes).
- `failover` (object) — Failure kind (`rate_limit`, `server_error`, `timeout`, `connection`, `credential`, `moderation`) to action (`retry`, `failover`, `stop`). Kinds left out keep `retry`. Reads return every kind.

```bash
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 '{
    "targets": [
      {"connection_id": "conn_01J9Z1", "model": "openai/gpt-5.6-sol"},
      {"connection_id": "conn_01J9Z2", "model": "anthropic/claude-sonnet-5"}
    ]
  }'
```

## Organization settings

One object per organization: `GET /org_settings` and `PATCH /org_settings` (with `If-Match`), no id. Fields include `monthly_target`, `hard_cap` (`{monthly, behavior, alert_at?}` or `null`), `unpriced_budget_policy` (`block` or `allow`), `prompt_logging_enabled`, `prompt_logging_retention_days`, `model_allowlist` and `billing_day_of_month` (1 to 28). The console's role rules apply: loosening `unpriced_budget_policy` to `allow` needs an owner, as it does in the console.

## Member budgets

One entry per member of the organization; the id is the member's id. Each carries `user_id`, `email`, `role` and `spend_limit`. There is no create or delete: `PATCH /member_budgets/{member_id}` with `{"spend_limit": {"monthly": 300, "behavior": "block"}}` sets a limit and `{"spend_limit": null}` removes it. Members themselves are not managed through this API.

## Webhook endpoints

- `url` (string, required) — Where deliveries are sent.
- `description` (string) — Up to 200 characters.
- `events` (string[]) — `job.succeeded`, `job.failed`, `job.canceled`.

Endpoints are created and deleted, not patched. `POST /webhook_endpoints/{id}/rotate_secret` returns a new signing secret once; see [Webhooks](https://infro.io/docs/api/webhooks) for how the previous one keeps verifying during the changeover.

## Model prices

- `model` (string, required) — Create only. The model id.
- `provider` (string | null) — Create only. A provider kind, or `null` for the rate on any provider.
- `input_per_million` (number | null) — USD per million input tokens.
- `output_per_million` (number | null) — USD per million output tokens.
- `cached_input_per_million` (number | null) — USD per million cached input tokens.
- `unit` (number | null) — USD per unit, for models billed per image, second or character.
- `note` (string | null) — Up to 200 characters.

The id is the stored rate's own id, returned on create. A custom rate is your provider's rate as you enter it; INFRO uses it to estimate provider cost and bills none of it.

## Notification channels and rules

A channel is one of `{"kind": "slack", "label", "slack_url"}`, `{"kind": "webhook", "label", "webhook_endpoint_id"}` or `{"kind": "email", "label", "emails"}`. A patch can change `label`, `status` (`active` or `disabled`), `slack_url` or `emails`.

A rule routes one event type to one channel: `event_type` (`spend.anomaly`, `spend.forecast_over_budget`, `cost.drift`, `budget.threshold_crossed`, `connection.failing`, `unpriced.traffic`, `traffic.model_changed` or `traffic.regression`), `channel_id`, `min_severity`, optional `scope_kind` and `scope_id`, and `include_end_user`. A patch can change `min_severity`, `include_end_user` and `mute_days` (1, 7, 30 or `null`). See [Alerts](https://infro.io/docs/observability/alerts).

## Cost centers and rules

A cost center has `name`, `code` and `owner_email`. `DELETE` archives it, because a closed month's statement still names it.

A cost center rule has `match`, `priority`, `note`, and either `cost_center_id` or an `allocation`. A rule is versioned by adding a new version each time it changes, so its management API id is its **lineage id**, which stays the same across versions; the current version's own id is in `version_id`. A `PATCH` is merged over the current rule and saved as a new version. See [Cost centers](https://infro.io/docs/features/cost-centers).

## Commitments

A commitment has `provider_kind`, `label`, `amount_usd`, `period_start`, `period_end`, and optional `discount_pct`, `models` and `notes`. Every field can be patched, and `DELETE` removes it. See [Commitments](https://infro.io/docs/cost/commitments).

## End-customer caps

An end-customer rule (`end_user_cap_rules`) has `project_id` (null for the organization-wide rule), `amount` (USD per period as a decimal string, or null for no default cap), `period` (`daily`, `weekly` or `monthly`), `behavior` (`block` or `alert`), `missing_id` (`serve` or `refuse`) and `enabled`. There is one per organization and one per project. The overrides under a rule are addressed by your own end-customer id rather than a generated one, and are paged: `GET /end_user_cap_rules/{rule}/overrides?limit=&starting_after=`, and `GET`, `PUT` or `DELETE /end_user_cap_rules/{rule}/overrides/{end_user_id}`. `PUT` is idempotent and `If-Match` is optional on overrides. A token holding only `end_users:write` can do this and nothing else. See [End-customer caps](https://infro.io/docs/org/end-customer-caps).

## Guardrail policies

A guardrail policy is attached to the organization, a project or a gateway key, and its id says which: `organization`, `project:<project_id>` or `key:<key_id>`. Create it with `POST /guardrail_policies` and `{"target": "project:prj_01J9Z4", "enabled": true, "on_engine_error": "block", "rules": [...]}`. A patch is merged over the current policy. See [Guardrails](https://infro.io/docs/features/guardrails) for the rules and their detectors.

```bash
curl https://api.infro.io/manage/v1/guardrail_policies/project:prj_01J9Z4 \
  -X PATCH \
  -H "Authorization: Bearer $INFRO_MANAGEMENT_TOKEN" \
  -H 'If-Match: "7"' \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
```

Because a policy has no row of its own, its version is the version of what it is attached to. A change to that project or key also moves the policy's version.

## MCP gateway

The [MCP gateway](https://infro.io/docs/mcp-gateway)'s configuration, in the `mcp` scope family. Reads need `mcp:read` (or `*:read`); writes need `mcp:write` and the Team plan's management API, and enabling anything (registering a server, replacing its credential, approving or unblocking a tool, granting a key) needs the Business plan. Disabling a server, blocking a tool and deleting a grant are never plan-gated.

### MCP servers

- `slug` (string, required) — 1 to 12 lower-case letters or digits, unique in the organization. Cannot be changed.
- `name` (string, required) — Up to 80 characters.
- `url` (string, required) — The server's public HTTPS URL.
- `credential` (object) — Write-only. `{"type": "none"}` (the default) or `{"type": "headers", "headers": {"Authorization": "Bearer …"}}`, up to 8 headers. Sealed and never returned; items carry `auth_type` and `header_names` only.
- `project_ids` (string[]) — Projects whose keys may reach the server. Empty means every project.
- `environments` (string[]) — Environments whose keys may reach the server. Empty means every environment.
- `reads_private_data, reads_untrusted_content, can_send_data_out` (boolean) — Risk labels, used for the lethal-trifecta check on grants.
- `timeout_ms` (integer) — 1000 to 300000; default 60000.
- `max_result_bytes` (integer) — 1024 to 5242880; default 1048576.
- `rate_limit_rpm` (integer) — 1 to 50000; default 300.

Creating a server lists its tools with the credential given and fails with `mcp_discovery_failed` if it cannot. A `PATCH` can change every field above except `slug` and `credential`, and `status` (`active` or `disabled`). Items also carry `protocol_era`, `health`, `last_synced_at`, `last_sync_error` and `version`. Actions:

- `POST /mcp_servers/{id}/credential` with `{"credential": {...}}`: replace the credential. The response carries header names, never a value.
- `POST /mcp_servers/{id}/sync`: list the server's tools now. New tools arrive `pending`; an approved tool whose definition changed becomes `changed`.
- `POST /mcp_servers/disable_all`: disable every server in one audited action. Every tool call is refused from the next call.

### MCP key grants

A grant has `key_id`, `server_id`, `mode` (`read`, `all` or `selected`) and, for `selected`, `tools` (upstream names). It is stored against the key's lineage (`key_lineage_id` on the item), so rotating the key keeps it. A patch can change `mode` and `tools`. A grant that would give one key all three risk labels is refused with `mcp_trifecta_unconfirmed` unless the body carries `confirm_trifecta: true`.

### MCP server tools

List and read only: tools are created by discovery. An item carries `server_id`, `upstream_name`, `exposed_name`, `state`, `access_class`, `suggested_class`, `definition` and `definition_sha256` (the latest the server listed), `approved_definition` and `approved_sha256` (what agents are served), `scan_findings`, and when it was first seen, changed and approved. Actions, each needing a token whose creator is an owner or admin:

- `POST /mcp_server_tools/{id}/approve` with `{"access_class": "read" | "write", "expected_sha256": "<hash you reviewed>"}`. A definition that changed since that hash is refused with `mcp_tool_definition_stale`.
- `POST /mcp_server_tools/{id}/block`: hidden from agents and refused, whatever the server lists.
- `POST /mcp_server_tools/{id}/unblock`: back to `approved` when its definition is still the approved one, else `pending`.

```bash
curl https://api.infro.io/manage/v1/mcp_server_tools/mcpt_01JA1W3D9P/approve \
  -X POST \
  -H "Authorization: Bearer $INFRO_MANAGEMENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"access_class": "read", "expected_sha256": "<definition_sha256 from GET /mcp_server_tools/mcpt_01JA1W3D9P>"}'
```

Notification rules also accept the event types `mcp.tool_changed` and `mcp.server_failing`. Terraform resources for the MCP gateway come later.
