Management API
Last updated
Create, change and revoke virtual keys, read your Gateway usage, request logs, plan and provider health, manage an organization's MCP Tool Packs and member groups, and publish the rules and skills its members' agents follow, from your own scripts, CI jobs or infrastructure-as-code, with a management key. It comes with the Gateway Pro, Scale and Enterprise plans.
Management keys
A management key is a separate kind of credential from a virtual key. It can manage virtual keys but can't call models, and a virtual key can't call the management API. Management keys are prefixed tmk_, stored hashed, and shown to you once, when you create them.
Create one in the Portal:
- For your own account: Gateway → Keys → Management keys. It manages your personal virtual keys.
- For an organization: Organization → Org keys → Management keys. Only owners and admins can create one, and it manages the organization's shared virtual keys.
Choose what each key can do when you create it:
| Scope | Allows |
|---|---|
keys:read | Listing and reading virtual keys. |
keys:write | Creating, changing and revoking virtual keys. |
usage:read | Reading usage, cost and latency metrics, the plan, and provider health. |
logs:read | Reading request logs and one request's detail, including the prompt and answer Tempr captured, when you ask for them. Not given by default: tick it when you create the key. |
tool_packs:read | Reading an organization's Tool Packs, member groups, Tool Packs settings, and tool access requests and grants. Organization keys only. |
tool_packs:write | Changing which MCP tools each member and key can use: creating, changing and deleting Tool Packs and member groups, giving packs to groups, members and keys, changing the settings, and approving, declining and revoking tool access. Organization keys only. |
agent_config:read | Reading an organization's rules and skills, their content, and who gets them, and its agent settings. Organization keys only. |
agent_config:write | Changing the rules and skills every member's agent follows: adding, changing, ordering and deleting rules and skills, and giving them to groups and members; and changing its agent settings. Organization keys only. |
A management key can also have an expiry date and an IP allowlist, set the same way as a virtual key's (see what a virtual key scopes). An account can have up to 10 active management keys. Management keys are only made in the Portal; the API can't create another one.
For an organization:
- Every change a management key makes is recorded in the audit log (Organization → Compliance) under that management key's label.
- When a member is removed, or an owner or admin is made a member, the management keys they created are revoked. Virtual keys those management keys created keep working.
Making requests
The base URL is https://api.temprhq.io/v1/management. Send the management key as a bearer token:
curl https://api.temprhq.io/v1/management/keys \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx"
Request and response bodies are JSON with snake_case field names. Response timestamps are Unix seconds. expires_at in a request is an ISO 8601 date and time, for example 2027-01-31T00:00:00Z.
Each management key can make 300 requests a minute. Over that, the API answers 429 until the next minute starts.
List keys
GET /v1/management/keys (scope keys:read) returns the account's virtual keys, newest first. The Portal's Playground key and the keys Tempr creates for team members' IDE sessions aren't included.
| Query parameter | Meaning |
|---|---|
limit | Keys per page, 1 to 100. Default 20. |
after | A key id from the previous page (its last_id); the page starts after it. |
include_revoked | true to include revoked keys. By default only active and expired keys are listed. |
{
"object": "list",
"data": [ { "id": "6f1c2b0e-…", "object": "virtual_key", "label": "ci", … } ],
"first_id": "6f1c2b0e-…",
"last_id": "6f1c2b0e-…",
"has_more": false
}
Create a key
POST /v1/management/keys (scope keys:write) creates a virtual key. label is required; every other field is optional. The response is 201 with the key object, plus the raw key in key. It isn't shown again, so store it now.
curl https://api.temprhq.io/v1/management/keys \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"label": "ci",
"allowed_models": ["anthropic/claude-sonnet-4-5"],
"max_budget_usd": 25,
"rpm_limit": 60,
"expires_at": "2027-01-31T00:00:00Z"
}'
{
"id": "6f1c2b0e-…",
"object": "virtual_key",
"label": "ci",
"masked_key": "tvk_...a1b2",
"status": "active",
"created_at": 1790000000,
"expires_at": 1801353600,
"last_used_at": null,
"revoked_at": null,
"allowed_models": ["anthropic/claude-sonnet-4-5"],
"allowed_mcp_servers": null,
"fallback_chains": null,
"max_budget_usd": 25,
"hold_worst_case": false,
"rpm_limit": 60,
"tpm_limit": null,
"guardrail_mode": null,
"response_guardrail_mode": null,
"llm_guardrail_enabled": null,
"mcp_argument_mode": null,
"mcp_result_mode": null,
"mcp_injection_mode": null,
"prompt_caching_enabled": null,
"content_capture_enabled": null,
"caller_credentials_enabled": null,
"when_used_up_model": null,
"default_model": null,
"force_default_model": false,
"allowed_ips": [],
"end_user_limits": { "monthly_budget_usd": null, "rpm_limit": null },
"budget_rules": [{ "period": "day", "meter": "cost", "limit": 20, "action": "block" }],
"reasoning_policy": null,
"key": "tvk_xxxxxxxxxxxxxxxxxxxxxxxx"
}
A personal account can have up to 3 active virtual keys; an organization has no such limit.
Get a key
GET /v1/management/keys/{id} (scope keys:read) returns one key. status is active, expired or revoked.
Change a key
PATCH /v1/management/keys/{id} (scope keys:write) changes the fields you send and leaves the rest as they are. Send a field as null to clear it, which removes that limit or goes back to the account default. label can be changed but not cleared. A revoked key can't be changed.
curl -X PATCH https://api.temprhq.io/v1/management/keys/6f1c2b0e-… \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "rpm_limit": 120, "max_budget_usd": null }'
Revoke a key
POST /v1/management/keys/{id}/revoke (scope keys:write) revokes a key and returns it. Requests with the key stop authenticating at once. Revoking a key that's already revoked changes nothing.
Key fields
| Field | Type | Meaning |
|---|---|---|
label | string | A name for the key, up to 100 characters. |
allowed_models | array of strings | The provider/model values the key may call. Empty or null allows any model. |
allowed_mcp_servers | array of strings | The MCP server slugs the key may reach through the MCP proxy. Empty or null allows any. |
fallback_chains | object | Aliases mapped to their chains of 2 to 5 candidates. A chain is either a list, tried in the order written, for example {"smart": ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"]}, or an object with an order of configured, cheapest, fastest or balanced and its models, for example {"fast": {"order": "fastest", "models": ["groq/llama-3.3-70b-versatile", "openai/gpt-4o-mini"]}}. Responses return a configured chain as a list, as before orders existed, and a cheapest, fastest or balanced one as the object. See fallback chains. |
max_budget_usd | number | The most the key may spend, in US dollars. |
hold_worst_case | boolean | Makes the budget strict: each request's worst case is set aside while it runs, so requests running at once can't spend past it together. Adds about 5 milliseconds per request. False unless set; null sets it back to false. See strict budgets. |
rpm_limit, tpm_limit | integer | Requests and tokens per minute. |
guardrail_mode | string | The pre-request guardrail: off, flag_only, redact_secrets (tokens, API keys and private keys in the prompt are replaced with placeholders before the model sees them), redact_personal (secrets, and personal data such as email addresses, phone and card numbers) or block. Null uses the account's setting. |
response_guardrail_mode | string | Answer scanning: off, flag_only, redact_secrets or redact_personal (never block). Null uses the account's setting. |
budget_rules | array | Up to 5 budget rules, each { "period": "hour|day|week|month", "meter": "cost|tokens|requests", "limit": number, "action": "warn|block" }. At most one warn and one block rule per period and meter; token and request limits are whole numbers. null or [] removes them all. |
llm_guardrail_enabled | boolean | Whether the LLM guardrail check runs. Null uses the account's setting. |
mcp_argument_mode | string | What the MCP guardrails do with secrets and personal data in tool-call arguments: off, flag_only or block. Null uses the organization's or account's MCP setting. |
mcp_result_mode | string | The same for tool results: off, flag, redact or block, which withholds the whole result (the Portal calls it Withhold, and withhold is accepted too). Null uses the organization's or account's MCP setting. |
mcp_injection_mode | string | What they do with planted instructions in tool results and descriptions: off, flag_only or block. Null uses the organization's or account's MCP setting. |
prompt_caching_enabled | boolean | Whether the Gateway adds prompt-cache breakpoints. Null uses the account's setting. |
content_capture_enabled | boolean | Whether request and response bodies are kept in the logs. Null uses the account's setting. |
caller_credentials_enabled | boolean | Whether the key accepts a Claude or ChatGPT plan login, or a provider key, sent with the request beside the virtual key in x-tempr-key (see your own credentials). Null uses the account's setting: on for a personal account, off for an organization until an owner or admin turns it on. |
routing_policy | object | The key's default host preferences for its pools and fallback chains, in a request's provider shape: only, ignore, order, sort, max_price, allow_fallbacks, first_byte_timeout_ms and output_floor_tokens, for example {"only": ["groq", "cerebras"], "sort": "price"}. Provider names come back as Tempr's. A field it doesn't support is refused. A request's own provider object can narrow it but not widen it. Null clears it. |
routing_rules | array | The key's routing rules, first match wins, each {"when": {...}, "target": "..."}. Conditions in when: model (a trailing * matches a prefix), metadata and headers (objects of names to values), end_user, team, has_tools, has_images, min_prompt_tokens and max_prompt_tokens; the target is a model, an alias or a pool. For example [{"when": {"model": "fast*", "has_tools": true}, "target": "any/openai/gpt-oss-120b"}]. Up to 20. Pro and up: on Free, setting rules answers plan_required, while clearing them, or sending the saved ones unchanged, works. Null clears them. |
when_used_up_model | string | What the key's requests go to once a plan paying for them is used up, or its dollar budget is: a provider/model id or one of its fallback aliases. A plan-paid request tries the plan first and hands over when it refuses; a request refused for budget goes there instead, which the budget lets through only when a plan pays. Null: the request stops with the plan's or budget's error. |
default_model | string | The model a request on the key uses when it names none: a provider/model id or one of its fallback aliases. Null: a request must name its model. |
force_default_model | boolean | When true, default_model replaces whatever model a request names. Needs a default_model. Default false. |
group_id | string or null | The member group the key belongs to as a team, for an organization's key: the team's budget covers the key's spend, its model list and budget rules apply, and the settings it locks replace the key's own. Null: no team. |
expires_at | date and time | When the key stops working, at most five years ahead. |
allowed_ips | array of strings | Addresses or CIDR ranges the key may be used from, up to 50. Empty or null allows any address. |
end_user_limits | object | Defaults for each of the key's end users: monthly_budget_usd (number) and rpm_limit (integer), null or 0 for none, for example {"monthly_budget_usd": 5, "rpm_limit": 20}. On PATCH, a field left out of the object stays as it is, and "end_user_limits": null clears both. Set one end user's own limits with PUT …/end-users/{end_user}/limits. |
reasoning_policy | object | The key's reasoning policy: {"default": "medium", "min": "low", "max": "high"}, each a level (none, minimal, low, medium, high, xhigh, max) or null. default is used when a request names no reasoning; a request below min or above max is moved to the nearest allowed level. The object replaces the whole policy, so a level left out of it is cleared; null clears them all. The default must lie within the range. Responses return null when the key has no policy. |
The model and MCP server lists take up to 200 entries each. A field the API doesn't know is an error rather than being ignored, so a typo doesn't pass unnoticed.
End users
An app names its own customers, its end users, with the x-tempr-end-user header or the request's end-user field. These endpoints show each end user of a key and set limits for one of them, overriding the key's end_user_limits. End users aren't created: they appear as requests name them, and an id you set limits for before it's been seen is kept for when it is. Put the end-user id in the path percent-encoded, up to 256 characters.
GET /v1/management/keys/{id}/end-users(scopekeys:read) lists the end users seen on the key this calendar month (UTC), and any with limits of their own, highest spend first, as{ "object": "list", "data": [ … ], "has_more": false }. It pages like the key list, withlimitandafter(an end-user id).GET /v1/management/keys/{id}/end-users/{end_user}/limits(scopekeys:read) returns one end user, seen or not.PUT /v1/management/keys/{id}/end-users/{end_user}/limits(scopekeys:write) sets the end user's own limits:monthly_budget_usd,rpm_limitandblocked. It replaces what was set before; a limit left out or null uses the key's default, and 0 means no limit for this end user. Sending none of them, andblockedfalse, is the same asDELETE.DELETE /v1/management/keys/{id}/end-users/{end_user}/limits(scopekeys:write) drops the end user's own limits, so the key's defaults apply again, and returns the end user.
curl -X PUT https://api.temprhq.io/v1/management/keys/6f1c2b0e-…/end-users/customer-4821/limits -H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" -H "Content-Type: application/json" -d '{ "monthly_budget_usd": 20, "rpm_limit": 60, "blocked": false }'
{
"id": "customer-4821",
"object": "end_user",
"key_id": "6f1c2b0e-…",
"spend_this_month_usd": 3.4125,
"requests_this_month": 212,
"last_seen_at": 1791234567,
"limits": { "monthly_budget_usd": 20, "rpm_limit": 60, "blocked": false },
"caps": { "monthly_budget_usd": null, "rpm_limit": null },
"effective_limits": { "monthly_budget_usd": 20, "rpm_limit": 60, "blocked": false },
"budget_remaining_usd": 16.5875
}
limits is the end user's own, with null where the key's default applies. caps are limits an AI agent set through the Gateway's MCP server: a cap only ever lowers what applies, and a PUT or DELETE here replaces it. effective_limits is what the Gateway enforces, the lower of the end user's limit (or the key's default) and any cap, null for no limit. budget_remaining_usd is null when no budget applies. Spend counts the requests the Gateway priced this month; requests_this_month counts every request, refused ones too. For an organization's key, each change is in the audit log.
Test guardrail settings
POST /v1/management/guardrails/test (scope keys:read) runs the guardrail tester's checks on a text, so a CI job can check that your guardrail settings catch what they should. Nothing is stored or logged, and it doesn't run the LLM check.
| Field | Type | Meaning |
|---|---|---|
text | string | Required. The text to check, up to 100,000 characters. |
kind | string | Required. request (a request to a model, checked as the gateway scans the raw body), mcp_arguments (a tool call's arguments; JSON is checked compact, as the relay sees it), mcp_result (the text of a tool's result) or mcp_description (a tool's description). |
key_id | string | Use that key's effective settings: its overrides over the account's or organization's. Without it, the management key owner's saved settings. |
guardrail_mode | string | off, flag_only, redact_secrets, redact_personal or block, in place of the saved setting, to try one before you set it. |
mcp_argument_mode, mcp_injection_mode | string | off, flag_only or block, likewise. |
mcp_result_mode | string | off, flag, redact or block, likewise. |
{
"object": "guardrail_test",
"verdict": "blocked",
"summary": "This request would be blocked: it contains an AWS access key.",
"matches": [
{ "category": "aws-key", "start": 12, "length": 20, "masked": "A••••••••••E" }
],
"categories": ["aws-key"],
"redacted_text": "Deploy with [REDACTED-AWS-KEY] now.",
"settings": {
"guardrail_mode": "block",
"mcp_argument_mode": "flag_only",
"mcp_result_mode": "redact",
"mcp_injection_mode": "flag_only"
}
}
verdict is allowed, flagged, redacted (a request or tool result the model gets with secrets masked) or blocked (a request or tool call refused, a result withheld, or a tool left out of the list). matches lists every match with its position in the checked text and only its first and last characters showing, including matches a check that's off would make; categories are the ones the verdict counts. For MCP kinds the answer adds model_text, what the model (or, for arguments, the tool) would get, and hidden_characters; a request under a redact mode adds model_text too, the text with the covered categories masked. A key_id that isn't the owner's answers 404 key_not_found.
Usage
GET /v1/management/usage (scope usage:read) totals the account's Gateway requests over a range of days.
| Query parameter | Meaning |
|---|---|
start, end | UTC dates (yyyy-MM-dd), both included. The range can be up to 90 days. By default it's the last 30 days, ending today. |
group_by | day (the default, oldest first), key or model (both by cost, highest first). |
{
"object": "usage",
"start": "2026-09-01",
"end": "2026-09-30",
"group_by": "model",
"data": [
{
"date": null,
"key_id": null,
"model": "anthropic/claude-sonnet-4-5",
"requests": 1204,
"errors": 3,
"prompt_tokens": 5120331,
"completion_tokens": 402118,
"cost_usd": 21.3714
}
]
}
Each row fills in the field it's grouped by (date, key_id or model) and leaves the other two null. errors counts requests that got a 4xx or 5xx answer. Usage covers the days your plan keeps logs for.
Time ranges
Request logs and metrics take a time range in one of two ways:
| Query parameter | Meaning |
|---|---|
range | A preset ending now: 1h, 24h, 7d, 30d, or month (since the first of this month, UTC). |
start, end | Both UTC instants (2026-10-15T09:00Z) or both days (2026-10-15; an end day is included whole). end defaults to now; send start to use them, and it overrides range. |
A range never reaches further back than your plan keeps logs; the answer's range object gives the start and end used, as Unix seconds, and clamped_to_retention: true when the start was moved up. A range that ends before the oldest log kept answers 400 invalid_range.
Request logs
GET /v1/management/logs (scope logs:read) lists requests, newest first, by default over the last 24 hours. A personal management key reads your own traffic; an organization's reads the organization's. Rows carry metadata only: what was sent and answered is in one request's detail.
| Query parameter | Meaning |
|---|---|
range, start, end | See time ranges. |
key_id | Only this virtual key's requests. |
provider, model | Only this provider (anthropic) or model (openai/gpt-5-mini). |
status | succeeded, failed, or one error type, such as rate_limit_exceeded, upstream_error or guardrail_blocked. |
session_id, end_user | Only one session (x-tempr-session-id) or end user. |
operation | chat, embeddings or mcp. |
plan | Who paid: any plan login, none (the account), coding, claude or chatgpt. |
team_id | Only one team's requests. Organization keys only. |
metadata | A metadata key and value from x-tempr-metadata, written team=search. |
min_duration_ms | Only requests that took at least this long. |
limit, after | Up to 100 rows a page (default 20); after is the last request_id of the previous page. |
{
"object": "list",
"range": { "preset": "24h", "start": 1792044000, "end": 1792130400 },
"data": [
{
"object": "request_log",
"request_id": "0f6a3c1e-2b7d-4c55-9a51-3f0e8d2b6c41",
"created_at": 1792129812,
"key_id": "5d1f0c2a-8e47-4b1c-a0d2-7e6f9b3c1a20",
"key_label": "Support bot",
"operation": "chat",
"provider": "anthropic",
"model": "anthropic/claude-sonnet-4-5",
"http_status": 429,
"succeeded": false,
"error_type": "rate_limit_exceeded",
"error_message": "Rate limit reached for requests",
"prompt_tokens": 0,
"completion_tokens": 0,
"duration_ms": 412,
"streamed": true,
"attempts": 2,
"session_id": "chat-8812"
}
],
"first_id": "0f6a3c1e-2b7d-4c55-9a51-3f0e8d2b6c41",
"last_id": "0f6a3c1e-2b7d-4c55-9a51-3f0e8d2b6c41",
"has_more": true
}
Fields with nothing to say are left out. fell_back is true when a fallback model answered; attempts counts the models and provider keys tried when there was more than one; guardrail_categories lists what a guardrail flagged. cost_usd is the provider cost Tempr worked out for the request.
One request
GET /v1/management/logs/{request_id} (scope logs:read) returns one request, whatever its age within your retention: the row above as request, and its routing (fallback_attempts and key_rotation_attempts, each with provider, model, key label, outcome and status), guardrail outcome, cache savings, the prompt template and version, the plan that paid, and its metadata. The request id is also in every Gateway response's x-tempr-request-id header.
Add include_content=true for the prompt and answer Tempr captured, as content.request_body and content.response_body, with secrets and personal data masked (the same masks as redaction, plus your own data-loss prevention rules). Nothing is captured when content capture is off; content.captured is then false. For an organization, reading content is recorded in the audit log, as it is in the Portal.
Metrics
GET /v1/management/metrics (scope usage:read) gives the numbers on the Portal's Gateway dashboard for a range (default 7d): totals (requests, failures, error rate, cost, cache savings, tokens, median and p95 latency and time to first token), a series over time (hourly up to three days, daily beyond, UTC), and the same by model, provider and key, with guardrail and prompt-cache totals. Narrow it with key_id, provider, model or operation; add compare=true for previous_totals, the same length of time just before.
Plan
GET /v1/management/plan (scope usage:read) gives the Gateway plan and where it stands: plan and status, the billing period (period_start, period_end), requests_used of requests_included and quota_percent, the rpm_limit, overage (mode, requests and estimated cost so far, rate and cap), log_retention_days, whether content_capture and redaction are on, the account's guardrail settings (guardrail_mode, response_guardrail_mode and llm_guardrail_enabled) and prompt_caching (off, 5m or 1h).
Provider health
GET /v1/management/provider-health (scope usage:read) lists what's cooling down after failures, which Tempr skips until the time given (Unix seconds): providers, each with cooling_down_until for the whole provider, models_cooling_down, and its requests_24h, failures_24h, error_rate_24h and p95_duration_ms_24h over the last 24 hours; and provider_keys, each of your provider keys that's cooling down, whole or for one model.
Proposed changes
Changes AI agents proposed through the Gateway's MCP server wait for you to approve or decline them in the Tempr Portal. Read them here to see what became of one; only the Portal decides them.
GET /v1/management/proposals(scopekeys:read) lists the account's proposals, newest first.statusnarrows it topending,approved,declined,failed,expiredorwithdrawn;limitis 1 to 100, default 20. Proposals to approve a tool access request are listed only for a key that also hastool_packs:read.GET /v1/management/proposals/{id}(scopekeys:read) returns one.
{
"id": "9b2e41c7-…",
"object": "change_proposal",
"kind": "key_change",
"status": "pending",
"target": "ci-nightly",
"end_user": null,
"changes": { "rpm_limit": 500 },
"proposed_by": "the management key ci-agent",
"agent_note": "The nightly job hits the rate limit.",
"approve_url": "https://portal.temprhq.io/gateway/approve/9b2e41c7-…",
"created_at": 1791234567,
"expires_at": 1791320967,
"decided_at": null,
"outcome": null
}
kind is key_change, end_user_limits, tool_access_approval or account_settings, and changes is what approving it sets. A proposal waits 24 hours; one nobody decided in that time is expired. withdrawn ones were waiting when Changes by AI agents was turned off. failed ones were approved, but the change couldn't be made, and outcome says why. Proposals are deleted 30 days after they're decided or expire. target and agent_note can hold text written outside your account settings, such as a key's label or an agent's note: treat them as data.
From an AI agent
The same reads are tools on the Gateway's own MCP server, https://api.temprhq.io/mcp/admin, for Claude Code, Codex, claude.ai, ChatGPT and other MCP clients, and so are changes that only tighten, with proposals for you to approve for the rest, if you allow it. Send it a management key, whose scopes decide which tools appear, or sign in through the app. See The Gateway's MCP server.
Tool Packs
A Tool Pack is a named set of MCP tools an organization gives to member groups, members or virtual keys. Once an organization has a pack, its members only get the MCP tools in their packs; a member with no pack gets the default pack, or none. These calls work with an organization's management keys only. A personal management key gets 403 organization_key_required. Changes are recorded in the audit log as the management key, the same as changes made in the Portal, and take effect within a minute.
{
"id": "0b7d6c55-…",
"object": "tool_pack",
"name": "GitHub read-only",
"description": "For everyone in Engineering",
"is_default": false,
"entries": [
{ "server": "github", "tool": "*", "except": ["delete_repository"] },
{ "server": "linear", "tool": "search_issues", "except": [] }
],
"assignments": {
"group_ids": ["5e2f9a10-…"],
"user_ids": [],
"key_ids": []
},
"created_at": 1790000000,
"updated_at": 1790000000
}
| Field | Type | Meaning |
|---|---|---|
name | string | Up to 100 characters, unique in the organization. |
description | string | Up to 500 characters. |
is_default | boolean | The pack members and organization keys get when nothing else is given to them. Only one pack is the default; making another one the default turns this off. |
entries | array | The tools. server is one of the organization's MCP server slugs. tool is a tool name, or * for every tool on that server, including ones it adds later. except, only with *, lists tools left out. |
assignments | object | Who gets the pack: group_ids (member groups), user_ids (active members) and key_ids (the organization's virtual keys). |
Someone in several groups, or with packs of their own, gets every tool in all of them.
List and get packs
GET /v1/management/tool-packs (scope tool_packs:read) returns every pack, by name, as { "object": "list", "data": [ … ] }. GET /v1/management/tool-packs/{id} returns one.
Create a pack
POST /v1/management/tool-packs (scope tool_packs:write) creates a pack and answers 201 with it. name is required; the other fields default to none and false.
curl https://api.temprhq.io/v1/management/tool-packs \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub read-only",
"entries": [ { "server": "github", "tool": "*", "except": ["delete_repository"] } ],
"assignments": { "group_ids": ["5e2f9a10-…"] }
}'
Change or delete a pack
PATCH /v1/management/tool-packs/{id} (scope tool_packs:write) changes the fields you send and leaves the rest. entries replaces all of the pack's tools. In assignments, each list you send replaces that list, and a list you leave out stays as it is. description can be cleared with null; name can't.
DELETE /v1/management/tool-packs/{id} (scope tool_packs:write) deletes a pack, and everyone loses the tools it gave them. It answers { "id": "…", "object": "tool_pack", "deleted": true }.
Give or take away a pack
POST /v1/management/tool-packs/{id}/assignments (scope tool_packs:write) gives the pack to more groups, members or keys, and DELETE /v1/management/tool-packs/{id}/assignments takes it away. Both take a body with any of group_ids, user_ids and key_ids, change only the ids you send, and answer with the pack.
curl https://api.temprhq.io/v1/management/tool-packs/0b7d6c55-…/assignments \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "user_ids": ["c41e7a2b-…"] }'
Tool Packs settings
GET /v1/management/tool-packs/settings (scope tool_packs:read) and PATCH /v1/management/tool-packs/settings (scope tool_packs:write) read and change the organization's Tool Packs settings. only_managed_servers, when true, also refuses MCP servers the organization doesn't manage while it uses Tool Packs; by default they're allowed. uses_tool_packs says whether the organization has any pack, and can't be set.
{
"object": "tool_pack_settings",
"only_managed_servers": true,
"uses_tool_packs": true
}
Member groups
A member group is a set of an organization's members, for giving them Tool Packs, and a monthly budget and models of their own. These calls also need an organization's management key.
{
"id": "5e2f9a10-…",
"object": "member_group",
"name": "Engineering",
"description": "",
"source": "portal",
"member_user_ids": ["c41e7a2b-…", "f0a93d18-…"],
"monthly_budget_usd": 200,
"budget_enforcement": "hard_cap",
"allowed_models": ["openai/gpt-4o-mini", "anthropic/*"],
"spend_this_month_usd": 41.27,
"budget_rules": [{ "period": "week", "meter": "cost", "limit": 500, "action": "block" }],
"key_policy": { "guardrail_mode": { "value": "redact_secrets", "locked": true }, "max_budget_usd": { "value": 100, "locked": false } },
"created_at": 1790000000,
"updated_at": 1790000000
}
source is portal for a group made in the Portal or with this API, or directory for one mirrored from your company directory through directory sync. A directory group can be read, given packs and given a budget and models, but renaming, deleting or setting the members of one answers 409 directory_group_read_only: change it in the directory instead.
| Field | Type | Description |
|---|---|---|
monthly_budget_usd | number or null | The group's monthly budget in US dollars, more than 0 and at most 1,000,000. Null: no budget. |
budget_enforcement | string | soft_alert (the default: owners and admins are emailed at 80% and 100%, once a month each) or hard_cap (members' requests are refused with 402 group_budget_exceeded once it's spent). |
allowed_models | array of strings or null | The models the group's members may use: provider/model ids, or provider/* for all of a provider's models, up to 200. Null or empty: every model. |
spend_this_month_usd | number | Read-only. The group's spend this calendar month (UTC): its members' in the organization, and its team keys'. |
budget_rules | array or null | Budget rules for the group's team keys, counted together, in the key field's shape. Up to 5; null or empty for none. |
key_policy | object or null | Settings for the group's team keys. Each is { "value": …, "locked": true|false }: a new team key starts with an unlocked one when it sets none of its own, and a locked one replaces every team key's own on each request and can't be changed on a key (400 field_locked). The settings: max_budget_usd, rpm_limit, tpm_limit, guardrail_mode, response_guardrail_mode, llm_guardrail_enabled, content_capture_enabled, prompt_caching_enabled, caller_credentials_enabled, default_model (its value { "model": "…", "force": true|false }), when_used_up_model and allowed_ips. Sending it replaces the whole policy; null clears it. |
A member in several groups may use the models of all of them, unless one of their groups has no list, and never more than their own allowed models. Every budget on the way applies: theirs, each hard-capped group's and the organization's. See Group budgets and models.
GET /v1/management/groups(scopetool_packs:read) lists the groups, by name, andGET /v1/management/groups/{id}returns one.POST /v1/management/groups(scopetool_packs:write) creates a group fromname(required, up to 100 characters, unique),description,member_user_ids,monthly_budget_usd,budget_enforcement,allowed_models,budget_rulesandkey_policy, and answers201with it.PATCH /v1/management/groups/{id}(scopetool_packs:write) changesname,description,monthly_budget_usd,budget_enforcement,allowed_models,budget_rulesorkey_policy, and leaves the fields you don't send. A directory group takes all but the first two.PUT /v1/management/groups/{id}/members(scopetool_packs:write) sets the members to exactly theuser_idsyou send, each an active member of the organization:{ "user_ids": ["c41e7a2b-…"] }.DELETE /v1/management/groups/{id}(scopetool_packs:write) deletes a group, and the packs given to it no longer reach its members through it. A group with active team keys answers409 group_has_keys.
Tool access requests
When a member's agent calls a tool their Tool Packs leave out, the refusal links to a Portal page where they can ask for it, with a reason. Owners and admins decide in the Portal (Tool Packs, then Requests), or with these calls. An approval is a grant for that member and tool, beside their packs: it lasts 1 hour, 1 day, 1 week or with no limit, and can be revoked. The member is emailed either way, and every step is in the audit log, as the management key for these calls. These calls need an organization's management key.
{
"id": "7c1f4e22-…",
"object": "tool_access_request",
"user_id": "c41e7a2b-…",
"user_email": "dana@example.com",
"server": "github",
"tool": "create_pull_request",
"reason": "Opening the release PR for the 2.4 branch.",
"status": "approved",
"duration": "1d",
"decided_at": 1790003600,
"decision_note": null,
"expires_at": 1790090000,
"created_at": 1790000000,
"updated_at": 1790003600
}
status is pending, approved, declined, expired (an approval that ran out) or revoked. duration is 1h, 1d, 1w or none once approved, and null while pending or when declined. A member has at most one pending request per tool; asking again changes its reason.
GET /v1/management/tool-access-requests(scopetool_packs:read) lists the requests, newest first.?status=pending(or any other status) narrows them.POST /v1/management/tool-access-requests/{id}/approve(scopetool_packs:write) approves a pending request forduration, which is required:{ "duration": "1h" },"1d","1w"or"none"for no limit. It answers with the request; the tool works from the member's next call.POST /v1/management/tool-access-requests/{id}/decline(scopetool_packs:write) declines a pending request, with an optional note for the member of up to 500 characters:{ "note": "Use the read-only pack for this." }, or{}. It answers with the request.
Approving or declining a request that isn't pending answers 409 tool_access_request_decided.
Grants
{
"id": "e93b0a41-…",
"object": "tool_access_grant",
"user_id": "c41e7a2b-…",
"user_email": "dana@example.com",
"server": "github",
"tool": "create_pull_request",
"status": "active",
"expires_at": 1790090000,
"request_id": "7c1f4e22-…",
"created_at": 1790003600,
"revoked_at": null
}
GET /v1/management/tool-access-grants(scopetool_packs:read) lists the grants in effect, newest first.?include_ended=trueadds the expired and revoked ones;statusisactive,expiredorrevoked.DELETE /v1/management/tool-access-grants/{id}(scopetool_packs:write) revokes a grant, and answers with it. The tool is refused from the member's next call. Revoking a grant that already ended changes nothing.
Org rules and skills
An organization's rules are instructions every agent it reaches follows, added to each model call in order. Its skills are packages in the open Agent Skills format: a SKILL.md with a name and a description, plus optional text files, which an agent reads when a task calls for it. Each one is required, which a member can't switch off, or recommended. Each one goes to everyone in the organization, or only to the member groups and members it's given to.
These calls are how an organization keeps its skills in a git repository and publishes them from CI: a job that runs on every merge sends each skill folder as a .zip, or its SKILL.md and files as JSON. They work with an organization's management keys only; a personal management key gets 403 organization_key_required. Every change is kept as a version an owner or admin can restore in the Portal, is recorded in the audit log as the management key, and reaches members' agents within a minute.
Rules
{
"id": "9a4e1c70-…",
"object": "org_rule",
"title": "Spelling",
"body": "Always write in British English.",
"required": true,
"position": 0,
"assignments": {
"group_ids": [],
"user_ids": []
},
"created_at": 1790000000,
"updated_at": 1790000000
}
| Field | Type | Meaning |
|---|---|---|
title | string | Up to 120 characters. Shown in the Portal and to members; not sent to the model. |
body | string | The rule, in Markdown, sent to the model as written. Up to 16 KB; an organization's rules can be up to 128 KB together, and it can have up to 100. |
required | boolean | true: members can't switch it off, and it wins over a project's own instructions. false (the default): recommended, so members can switch it off and a project's instructions win. |
position | integer | Its place in the order the rules are given to the model, from 0. Read only; set the order with PUT …/rules/order. |
group_ids, user_ids | array of ids | In a request: who gets the rule, member groups and active members. Both empty, or never sent, gives it to everyone. A response shows them under assignments. |
GET /v1/management/agent-config/rules(scopeagent_config:read) returns the rules in the order they're given to the model, as{ "object": "list", "data": [ … ] }.GET /v1/management/agent-config/rules/{id}returns one.POST /v1/management/agent-config/rules(scopeagent_config:write) adds a rule at the end and answers201with it.titleandbodyare required.PATCH /v1/management/agent-config/rules/{id}(scopeagent_config:write) changes the fields you send and leaves the rest.group_idsoruser_ids, when sent, replaces that list.PUT /v1/management/agent-config/rules/order(scopeagent_config:write) puts the rules in the order ofidsand answers with the list. Rules you leave out keep their order, after the ones you name:{ "ids": ["9a4e1c70-…", "e1b03f42-…"] }.DELETE /v1/management/agent-config/rules/{id}(scopeagent_config:write) deletes a rule and answers{ "id": "…", "object": "org_rule", "deleted": true }. It can be restored in the Portal.
curl https://api.temprhq.io/v1/management/agent-config/rules \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "title": "Spelling", "body": "Always write in British English.", "required": true }'
Skills
A skill is named by the name in its SKILL.md frontmatter: up to 64 lowercase letters, digits and single hyphens, unique in the organization. The calls below take that name in the path.
{
"id": "3c8f20d1-…",
"object": "org_skill",
"name": "release-hotfix",
"description": "Cut a hotfix release. Use when asked to ship a fix to production outside the release train.",
"required": false,
"files": ["references/checklist.md", "scripts/tag.sh"],
"size_bytes": 6120,
"content_hash": "4f1a…",
"assignments": {
"group_ids": ["5e2f9a10-…"],
"user_ids": []
},
"created_at": 1790000000,
"updated_at": 1790000000
}
| Field | Type | Meaning |
|---|---|---|
skill_md | string | In a request: the whole SKILL.md, frontmatter included, up to 64 KB. Its name and description (up to 1,024 characters) are checked against the Agent Skills format, and the error says exactly what to fix. Other frontmatter fields, such as Claude Code's allowed-tools, stay in the file and are ignored. |
files | array | In a request: the skill's other files as { "path": "references/api.md", "content": "…" }, text only, up to 100. In a response: their paths. A skill can be up to 1 MB in all. |
required | boolean | true: every member it reaches has it, and can't switch it off. false (the default): recommended. |
group_ids, user_ids | array of ids | In a request: who gets the skill, as for a rule. A response shows them under assignments. |
content_hash | string | A SHA-256 of SKILL.md and the files, which changes whenever the content does. Read only. |
GET /v1/management/agent-config/skills(scopeagent_config:read) returns every skill, by name, without its content.GET /v1/management/agent-config/skills/{name}returns one.GET /v1/management/agent-config/skills/{name}/content(scopeagent_config:read) returns its content in the shape a request takes it:{ "name": "…", "object": "org_skill_content", "skill_md": "…", "content_hash": "…", "files": [ { "path": "…", "content": "…" } ] }.POST /v1/management/agent-config/skills(scopeagent_config:write) adds a skill and answers201with it. A name that's taken answers409 skill_exists; change that skill withPATCHinstead.PATCH /v1/management/agent-config/skills/{name}(scopeagent_config:write) changes the fields you send and leaves the rest.skill_mdreplacesSKILL.md, andfiles, when sent, replaces all of the other files; a newnamein the frontmatter renames the skill. Sending the same content again adds no version and no audit entry, so a CI job can send every skill on every run.DELETE /v1/management/agent-config/skills/{name}(scopeagent_config:write) deletes a skill and answers{ "id": "…", "name": "…", "object": "org_skill", "deleted": true }. It can be restored in the Portal.
POST and PATCH take JSON, or multipart/form-data with the skill in a file field: a .zip with SKILL.md at the top or in one folder named after the skill (the layout skills repositories use), or a lone SKILL.md. The upload can be up to 5 MB. required, group_ids and user_ids go in form fields of those names, with ids repeated or comma-separated.
curl https://api.temprhq.io/v1/management/agent-config/skills \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"skill_md": "---\nname: release-hotfix\ndescription: Cut a hotfix release. Use when asked to ship a fix outside the release train.\n---\n\n1. Read references/checklist.md first.\n",
"files": [ { "path": "references/checklist.md", "content": "- Tests green\n- Changelog entry\n" } ],
"required": false
}'
From CI, publishing a skill folder from a repository:
cd skills && zip -r release-hotfix.zip release-hotfix
curl -X PATCH https://api.temprhq.io/v1/management/agent-config/skills/release-hotfix \
-H "Authorization: Bearer $TEMPR_MANAGEMENT_KEY" \
-F "file=@release-hotfix.zip"
A 404 skill_not_found from that PATCH means the skill is new: send the same upload with POST /v1/management/agent-config/skills.
Give or take away a rule or skill
POST /v1/management/agent-config/rules/{id}/assignments and POST /v1/management/agent-config/skills/{name}/assignments (scope agent_config:write) give a rule or skill to more groups or members. DELETE /v1/management/agent-config/rules/{id}/assignments and DELETE /v1/management/agent-config/skills/{name}/assignments take it away. Both take a body with group_ids, user_ids or both, change only the ids you send, and answer with the rule or skill. Taking away the last group or member gives it back to everyone. Group ids come from member groups, which a key reads with tool_packs:read.
curl https://api.temprhq.io/v1/management/agent-config/skills/release-hotfix/assignments \
-H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "group_ids": ["5e2f9a10-…"] }'
Agent settings
An organization's settings for its members' Tempr Code and CLI, as on the Portal's Agent playbook page. local_models_allowed says whether they may use local models: Ollama, LM Studio and other OpenAI-compatible servers on a member's own machine. Those turns go to the model directly, so the Gateway's policies and logs don't reach them. It's on by default; turned off, the clients stop offering local models at their next config fetch.
GET /v1/management/agent-config/settings(scopeagent_config:read) returns{ "object": "agent_config_settings", "local_models_allowed": true }.PATCH /v1/management/agent-config/settings(scopeagent_config:write) changes the settings you send, and answers with them all. Each change is in the organization's audit log.
curl -X PATCH https://api.temprhq.io/v1/management/agent-config/settings -H "Authorization: Bearer tmk_xxxxxxxxxxxxxxxxxxxxxxxx" -H "Content-Type: application/json" -d '{ "local_models_allowed": false }'
Errors
Errors use the same shape as the rest of the Gateway API, and param names the field at fault when there is one:
{
"error": {
"message": "The RPM limit can't be negative.",
"type": "invalid_request_error",
"code": "invalid_value",
"param": "rpm_limit"
}
}
| Status | Type | Code | Meaning |
|---|---|---|---|
400 | invalid_request_error | invalid_body | The body isn't a JSON object. |
400 | invalid_request_error | invalid_value | A field or query parameter is missing, unknown, the wrong type or out of range. message says which. |
400 | invalid_request_error | field_locked | The key belongs to a team that locks this setting, so the key can't be given another value. param names the setting; change it on the team (key_policy). |
400 | invalid_request_error | invalid_cursor | after isn't the id of a key or request in the list. |
400 | invalid_request_error | invalid_range | range isn't one of the presets; start and end can't be read, mix an instant with a day, or are the wrong way round; end came without start; or the range ends before the oldest log your plan keeps. |
401 | authentication_error | missing_management_key | No Authorization: Bearer header. |
401 | authentication_error | invalid_management_key | The key doesn't exist or has been revoked. A virtual key gets this too. |
401 | authentication_error | management_key_expired | The management key is past its expiry date. |
403 | permission_error | ip_not_allowed | The request came from an address outside the management key's IP allowlist. |
403 | permission_error | plan_required | The account isn't on a paid-up Pro, Scale or Enterprise plan. |
403 | permission_error | insufficient_scope | The management key doesn't have the scope the call needs. |
403 | permission_error | organization_key_required | A Tool Packs, member groups, or rules and skills call made with a personal management key. Use one of the organization's. |
404 | invalid_request_error | key_not_found | No virtual key with that id belongs to this account. |
404 | invalid_request_error | request_not_found | No request with that id is in the account's logs: it isn't the account's, or it's older than the plan keeps logs. |
404 | invalid_request_error | plan_not_found | The account has no Gateway plan. |
404 | invalid_request_error | tool_pack_not_found | No Tool Pack with that id belongs to this organization. |
404 | invalid_request_error | group_not_found | No member group with that id belongs to this organization. |
404 | invalid_request_error | rule_not_found | No rule with that id belongs to this organization. |
404 | invalid_request_error | skill_not_found | No skill with that name belongs to this organization. |
404 | invalid_request_error | tool_access_request_not_found | No tool access request with that id belongs to this organization. |
404 | invalid_request_error | tool_access_grant_not_found | No tool access grant with that id belongs to this organization. |
404 | invalid_request_error | proposal_not_found | No proposed change with that id belongs to this account, or it's one to approve tool access and the key can't read tool access. |
409 | invalid_request_error | directory_group_read_only | The group comes from your company directory, so it can't be renamed, deleted or given members here. |
409 | invalid_request_error | group_has_keys | The group is a team with active virtual keys, so it can't be deleted. Move its keys to another team (group_id) or revoke them first. |
409 | invalid_request_error | tool_access_request_decided | The tool access request was already approved or declined, so it can't be decided again. |
409 | invalid_request_error | skill_exists | The skill's name, new or renamed, is taken by another skill in the organization. To change that one, use PATCH. |