Token Vault
Reference

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

Loading diagram...

Authentication

The agent's tvagent_… key, one of two ways (checked in this order):

MethodExampleNotes
Authorization headerAuthorization: Bearer tvagent_…Preferred
x-agent-key headerx-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

ParameterInRequiredDescription
servicequerynoService 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:

200 OK — served by your webhook, not Token Vault
{
  "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):

200 OK
{
  "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

HTTPcodeWhen
401MISSING_KEYNo key in header or query
403INVALID_KEYMalformed or revoked key
403AGENT_INACTIVEAgent is suspended or deleted
403POLICY_DENIEDAn ABAC rule blocked the request — body carries policy, rule, reason
429POLICY_DENIEDAn ABAC rate_limit rule blocked the request — same body as above, plus retryAfter and a Retry-After header
403GRANT_EXPIREDThe grant existed but has expired (it is deleted on this request)
400INVALID_SERVICEService name contains /
404NO_GRANTNo active grant for service
429RATE_LIMITEDRate limited — see below for which layer
423VAULT_LOCKEDThe vault owner has locked the vault
502WEBHOOK_NOT_CONFIGUREDNo webhook is bound to this vault
403 — policy denial body (non-rate_limit rules)
{
  "error": "Access denied by policy",
  "policy": "office-hours",
  "rule": "time_window",
  "reason": "Outside allowed window 09:00-18:00 Europe/London",
  "code": "POLICY_DENIED"
}
429 — policy denial body (rate_limit rule)
{
  "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:

  1. 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_LIMITED returned immediately, without even reaching the floor limiter.
  2. Floor limit — 60 requests/minute per agent (agent_credentials scope), regardless of ABAC policy. Exceeding it strikes the penalty box, and the resulting lockout is at least the floor window's own Retry-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.
  3. 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.

429 — over the no-grant miss limit
{ "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.

On this page