DocsManagement API
Management API resources
Fields, operations and delete behaviour for every management API resource, with curl examples: projects, keys, connections, routes, budgets and more.
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.
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. 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
namestringrequired- Up to 80 characters.
slugstring- Create only. Up to 60 characters.
model_allowliststring[]- Patch only. Models this project's keys may call; see model allowlists.
spend_limitobject | null- Patch only.
{monthly, behavior, alert_at?}:behaviorisalertorblock,alert_atis a list of fractions up to 1.nullremoves it. archivedboolean- Patch only. Archiving refuses the project's keys with
403 project_archived;falserestores 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
labelstringrequired- Up to 80 characters.
project_idstring- Create only. The project the key belongs to.
credit_limitobject | null{amount, period}:amountfrom 0.01 to 1,000,000,000 USD,periodisdaily,weekly,monthlyornever. See spend controls.rate_limit_rpminteger- 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_allowliststring[]- Models this key may call.
allowed_routesstring[]- Route slugs the key is pinned to.
allowed_providersstring[]- Provider ids the key may use, such as
openaioropenrouter(asGET /console/providers/kindslists them).[]or absent: every provider, including ones connected later. An unknown id is refused with400unknown_provider. See Authentication. environmentstring | null- The identifier of ONE of your organization's environments (
production,staginganddevelopmentby 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_atstring | null- An ISO 8601 time after which the key stops working.
observability_accessstringnone,scopedororganization: which request history the key may read.payload_accessboolean- Whether the key may read stored prompts and completions.
disabledboolean- Patch only.
trueturns the key off: requests with it return403key_disabled, and nothing else about it changes.falseturns it on again. Refused for a revoked key. allowed_ipsstring[]- CIDR ranges or addresses the key accepts requests from, up to 32;
[]accepts any address. Stored in canonical form (203.0.113.7becomes203.0.113.7/32). See Authentication.
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.
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
kindstringrequired- Create only. A provider kind from
GET /provider_kinds. namestringrequired- Up to 80 characters. Unique among your connections of the same
kind, ignoring case and repeated spaces: two providers can each have a key calledProduction. A clash answers400duplicate_connection_name. secretstring- Create only, write-only. The provider credential.
fallback_secretsstring[]- 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.
environmentsstring[] | null- The environments whose keys may use this connection, by identifier.
[]ornullis 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. environmentstring | null- Deprecated: the single-value form. On write it means
[environment](or every environment whennull); sent besideenvironments, it must be that list's first entry. On read it is the first entry ofenvironments, ornullwhen that is empty. configobject- Kind-specific settings, such as a base URL where the kind allows one.
model_mapobject- For kinds that serve the models you name.
timeout_msinteger- 1,000 to 600,000.
alternate_base_urlsstring[]- 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 (
400alternate_host_off_domain, oralternate_host_not_allowedfor a kind that takes none); any HTTPS public host for a server whose URL you type. OnPATCHit replaces the list;[]removes every alternate. See Standby keys and alternate hosts. pool_rolestringactive(default) shares the requests that name a model directly with the provider's other active keys;standbyis tried only after every active key failed.validateboolean- Create only. Check the credential with the provider before answering.
statusstring- Patch only.
enabledordisabled.
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 for what each kind needs.
Routes
slugstringrequired- Create only. The name your application sends as
model. Up to 64 characters. namestring- Up to 80 characters.
descriptionstring | null- Up to 500 characters.
strategystringpriorityorweighted.environmentsstring[] | null- The environments whose keys may call this route.
[]ornullis every environment. Same rule as a connection's. environmentstring | null- Deprecated single-value form, as on a connection.
timeout_msinteger | null- 1,000 to 600,000.
max_retriesinteger- 0 to 3.
targetsobject[]required{connection_id, model, weight?, enabled?}, in order. OnPATCH,targetsreplaces the whole target list.rulesobject[]- Up to 20 if-rules checked in order before
targets:{name, enabled?, match?, conditions, strategy?, fallthrough?, targets}. OnPATCH,rulesreplaces every rule; leaving it out keeps them. See Rules. failoverobject- Failure kind (
rate_limit,server_error,timeout,connection,credential,moderation) to action (retry,failover,stop). Kinds left out keepretry. Reads return every kind.
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
urlstringrequired- Where deliveries are sent.
descriptionstring- Up to 200 characters.
eventsstring[]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 for how the previous one keeps verifying during the changeover.
Model prices
modelstringrequired- Create only. The model id.
providerstring | null- Create only. A provider kind, or
nullfor the rate on any provider. input_per_millionnumber | null- USD per million input tokens.
output_per_millionnumber | null- USD per million output tokens.
cached_input_per_millionnumber | null- USD per million cached input tokens.
unitnumber | null- USD per unit, for models billed per image, second or character.
notestring | 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.
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.
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.
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.
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 for the rules and their detectors.
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'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
slugstringrequired- 1 to 12 lower-case letters or digits, unique in the organization. Cannot be changed.
namestringrequired- Up to 80 characters.
urlstringrequired- The server's public HTTPS URL.
credentialobject- Write-only.
{"type": "none"}(the default) or{"type": "headers", "headers": {"Authorization": "Bearer …"}}, up to 8 headers. Sealed and never returned; items carryauth_typeandheader_namesonly. project_idsstring[]- Projects whose keys may reach the server. Empty means every project.
environmentsstring[]- Environments whose keys may reach the server. Empty means every environment.
reads_private_data, reads_untrusted_content, can_send_data_outboolean- Risk labels, used for the lethal-trifecta check on grants.
timeout_msinteger- 1000 to 300000; default 60000.
max_result_bytesinteger- 1024 to 5242880; default 1048576.
rate_limit_rpminteger- 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}/credentialwith{"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 arrivepending; an approved tool whose definition changed becomeschanged.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}/approvewith{"access_class": "read" | "write", "expected_sha256": "<hash you reviewed>"}. A definition that changed since that hash is refused withmcp_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 toapprovedwhen its definition is still the approved one, elsepending.
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.