Agent Credentials API
Reference for GET /api/agents/credentials — auth forms, the 307 redirect contract, list mode, response schemas, and the full error catalogue.
GET /api/agents/credentials
Retrieve a credential the calling agent has been granted. On success Token Vault validates the grant and ABAC policies, then answers with a 307 redirect to your webhook's /v1/credential; the agent's HTTP client follows it and receives the credential directly from the webhook. Use a client that follows redirects (curl -L; Python requests follows by default).
Authentication
The agent's tvagent_… key, one of two ways (checked in this order):
| Method | Example | Notes |
|---|---|---|
Authorization header | Authorization: Bearer tvagent_… | Preferred |
x-agent-key header | x-agent-key: tvagent_… | For clients that reserve Authorization |
A ?key= query parameter is not accepted. A key in a URL is recorded by every hop that
logs a request line — Cloud Run, the Google front end, Cloudflare, browser history, and any
Referer the client sends — and none of those can be scrubbed after the fact.
A tvsess_… OAuth session token works identically on the MCP endpoint — see OAuth 2.1 for MCP Clients.
Parameters
| Parameter | In | Required | Description |
|---|---|---|---|
service | query | no | Service name, e.g. github. Omit to list all active grants instead of retrieving one. |
Response — single credential (service set)
Token Vault answers 307 Temporary Redirect with a Location pointing at your webhook, which returns:
{
"token": {
"accessToken": "ghp_…",
"refreshToken": "ghr_…",
"serviceName": "github",
"tokenType": "pat",
"createdAt": "2026-02-01T10:00:00Z"
}
}Exact fields depend on token type — a TOTP token returns a current one-time code as accessToken, a raw GCP credential returns a short-lived minted token. See the brokering guides.
Response — list mode (service omitted)
Token Vault answers directly (no redirect, no credential material):
{
"grants": [
{
"serviceName": "github",
"grantExpiresAt": null,
"refreshPolicy": "none",
"sourceGroupId": null
},
{
"serviceName": "google",
"grantExpiresAt": "2026-08-01T12:00:00Z",
"refreshPolicy": "auto",
"sourceGroupId": "grp_ab12"
}
]
}Expired grants are filtered out. sourceGroupId is set when the grant came from a Folder (token group).
Errors
| HTTP | code | When |
|---|---|---|
| 401 | MISSING_KEY | No key in header or query |
| 403 | INVALID_KEY | Malformed or revoked key |
| 403 | AGENT_INACTIVE | Agent is suspended or deleted |
| 403 | POLICY_DENIED | An ABAC rule blocked the request — body carries policy, rule, reason |
| 429 | POLICY_DENIED | An ABAC rate_limit rule blocked the request — same body as above, plus retryAfter and a Retry-After header |
| 403 | GRANT_EXPIRED | The grant existed but has expired (it is deleted on this request) |
| 400 | INVALID_SERVICE | Service name contains / |
| 404 | NO_GRANT | No active grant for service |
| 429 | RATE_LIMITED | Rate limited — see below for which layer |
| 423 | VAULT_LOCKED | The vault owner has locked the vault |
| 502 | WEBHOOK_NOT_CONFIGURED | No webhook is bound to this vault |
{
"error": "Access denied by policy",
"policy": "office-hours",
"rule": "time_window",
"reason": "Outside allowed window 09:00-18:00 Europe/London",
"code": "POLICY_DENIED"
}{
"error": "Access denied by policy",
"policy": "api-budget",
"rule": "rate_limit",
"reason": "Rate limit exceeded: 100 requests per hour",
"code": "POLICY_DENIED",
"retryAfter": 37
}A rate_limit policy rule returns HTTP 429, not 403 — every other rule type (time_window,
ip_allowlist, geo_restrict, max_usage, manual_approval) keeps the 403 shown above.
(manual_approval is currently unavailable and denies every request it's attached to — see
Policies.) Either
way code stays POLICY_DENIED; only the status line and (for rate_limit) the retryAfter
field/Retry-After header change. If you're on the tvault CLI: a policy rate limit now exits with
code 7 (the CLI's 429 mapping) instead of its 403 code — tvault's GETs already retry on 429
with backoff, so this makes rate limits recoverable there instead of a hard failure.
Rate limiting
This endpoint sits behind three layers, checked in order, before the grant/policy checks above even run:
- Penalty box — an authenticated agent that keeps tripping the floor limit gets an
exponentially escalating lockout (1s, 2s, 4s, … capped at 15 minutes). A lockout blocks ALL of
that agent's credential access — every service, not just the one that tripped it — with
429 RATE_LIMITEDreturned immediately, without even reaching the floor limiter. - Floor limit — 60 requests/minute per agent (
agent_credentialsscope), regardless of ABAC policy. Exceeding it strikes the penalty box, and the resulting lockout is at least the floor window's ownRetry-After(up to 60s) — not just the exponential base — so a client that waits out a short first-strike lockout can't land back inside the same floor window and strike again. - Account aggregate — a per-account ceiling across every agent and proxy on the account (sized to the account's own floor allowance), so no single account can scale its total credential-access traffic just by creating more agents.
GET /api/agents/credentials and POST /api/agents/mcp (see MCP Endpoint)
share the SAME per-agent floor counter and penalty-box lockout — splitting traffic across the two
transports does not double an agent's effective quota, and a lockout tripped on one transport blocks
the other too.
No-grant miss limit. Asking for a service the agent has no grant for (NO_GRANT) or whose grant
has lapsed (GRANT_EXPIRED) is never a penalty-box strike, at any volume — a typo or a stale
config is far more common than an attack, and striking here would lock the agent out of its OTHER,
actually-granted services too. It IS rate-limited on its own, tighter scope: 10 misses/minute per
agent. Past that ceiling, the response becomes 429 RATE_LIMITED (below) — the agent's granted
fetches for other services are unaffected throughout.
{ "error": "Rate limit exceeded", "code": "RATE_LIMITED", "retryAfter": 41 }If your webhook is offline, the agent's redirect fetch fails at the webhook — that is the kill switch doing its job.
Ready to try it?
Sign up free with Google — your credentials stay on your own webhook, and the quickstart gets an agent fetching its first credential in about ten minutes.
Binding Your Webhook
The one-time handshake that pairs your deployed webhook with your Token Vault account — bind page, one-time code exchange, HMAC establishment, and troubleshooting.
MCP Endpoint
Reference for POST /api/agents/mcp — the MCP server exposing list_credentials and get_credential tools, with tvagent_ key or OAuth 2.1 session auth.