Skip to content

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

OperationScopeWhat it returns
GET /usageanalytics:readThe same aggregation as GET /v1/usage, as JSON: buckets, optional group_by, estimated provider cost and unpriced requests. format=csv is refused.
GET /requestsanalytics:readThe request log with the filters of GET /v1/requests, at most 100 a page (after = the previous next_cursor).
GET /requests/{id}analytics:readOne request's metadata: attempts, route, guardrail outcomes, cost.
GET /traces/{id}analytics:readEvery request of one run, in order, with the run's estimated cost.
GET /findings, GET /findings/{id}findings:readWhat INFRO noticed: anomalies, drift, budget crossings, failing connections, traffic changes. Filter by status, type, severity.
POST /findings/{id}/acknowledgefindings:writeAcknowledges a finding; audited, takes no ownership.
GET /budget_statusbudgets:readEvery enforced spend limit with estimated spend in the current period.
GET /audit_eventsaudit:readThe audit log: q, action, actor, from, to, limit, cursor.

Secrets

SecretHow it is handled
Gateway key secretReturned once, in the response to POST /gateway_keys and to POST /gateway_keys/{id}/rotate. Never readable afterwards.
Webhook endpoint secretReturned once, in the response to POST /webhook_endpoints and to POST /webhook_endpoints/{id}/rotate_secret.
Provider connection secretWrite-only. Accepted on create and on POST /provider_connections/{id}/credential, sealed, never returned.
Slack slack_url on a notification channelWrite-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?}: behavior is alert or block, alert_at is a list of fractions up to 1. null removes it.
archivedboolean
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

labelstringrequired
Up to 80 characters.
project_idstring
Create only. The project the key belongs to.
credit_limitobject | null
{amount, period}: amount from 0.01 to 1,000,000,000 USD, period is daily, weekly, monthly or never. 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 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.
environmentstring | 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_atstring | null
An ISO 8601 time after which the key stops working.
observability_accessstring
none, scoped or organization: which request history the key may read.
payload_accessboolean
Whether the key may read stored prompts and completions.
disabledboolean
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_ipsstring[]
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.

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.

Create a gateway key
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 called Production. A clash answers 400 duplicate_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. [] 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.
environmentstring | 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.
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 (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.
pool_rolestring
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.
validateboolean
Create only. Check the credential with the provider before answering.
statusstring
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 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.
strategystring
priority or weighted.
environmentsstring[] | null
The environments whose keys may call this route. [] or null is 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. On PATCH, targets replaces the whole target list.
rulesobject[]
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.
failoverobject
Failure kind (rate_limit, server_error, timeout, connection, credential, moderation) to action (retry, failover, stop). Kinds left out keep retry. Reads return every kind.
Replace a route's targets
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 null for 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.

Turn off a project's guardrail policy
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 carry auth_type and header_names only.
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}/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.
Approve a reviewed tool as read-only
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.

↑ ↓ navigate↵ open/ anywhere in docs