- devops docs: 8 files updated for Phase 6 state; field-trial.md added (946-line runbook) - developer docs: api-reference (50+ endpoints), quick-start, 5 existing guides updated, 5 new guides added - engineering docs: all 12 files updated (services, architecture, SDK guide, testing, overview) - OpenSpec archives: phase-7-devops-field-trial, developer-docs-phase6-update, engineering-docs-phase6-update - VALIDATOR.md + scripts/start-validator.sh: V&V Architect tooling added - .gitignore: exclude session artifacts, build artifacts, and agent workspaces Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.8 KiB
Issue and Revoke Tokens
This guide covers the complete token lifecycle: issuing, using, inspecting, and revoking JWT access tokens.
Issue a token
POST /api/v1/token
This is the OAuth 2.0 Client Credentials grant. Your agent exchanges its client_id and client_secret for a signed JWT access token.
Important
: This endpoint uses
application/x-www-form-urlencodedencoding, not JSON.
curl -s -X POST http://localhost:3000/api/v1/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=agents:read agents:write" | jq .
Response (200 OK):
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImp0aSI6InV1aWQtaGVyZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "agents:read agents:write"
}
The token expires in 3600 seconds (1 hour). Request a new one before it expires.
Request fields
| Field | Required | Description |
|---|---|---|
grant_type |
Yes | Must be client_credentials |
client_id |
Yes | Your agent's agentId (UUID) |
client_secret |
Yes | The secret from credential generation |
scope |
No | Space-separated list of requested scopes. If omitted, all scopes are granted. |
Available scopes
| Scope | What it allows |
|---|---|
agents:read |
Read agent identity records |
agents:write |
Create, update, and decommission agents |
tokens:read |
Introspect tokens |
audit:read |
Query audit logs and verify audit chain integrity |
webhooks:read |
List webhook subscriptions and delivery history |
webhooks:write |
Create, update, and delete webhook subscriptions |
admin:orgs |
Manage organizations and federation partners |
Request only the scopes your agent needs.
What's inside the JWT
A JWT has three base64-encoded parts separated by dots: header, payload, and signature. The payload contains your agent's identity claims.
Decode the payload to inspect it (for development only — never trust an unverified token in production):
# Extract the middle part (payload) of your token and decode it
TOKEN_PAYLOAD=$(echo "$TOKEN" | cut -d. -f2)
echo "$TOKEN_PAYLOAD" | base64 --decode 2>/dev/null | jq .
Claims in the payload:
| Claim | Description |
|---|---|
sub |
Subject — your agent's agentId |
client_id |
The agentId that authenticated |
scope |
Scopes granted by this token |
jti |
JWT ID — unique identifier for this token (used for revocation) |
iat |
Issued at (Unix timestamp in seconds) |
exp |
Expires at (Unix timestamp in seconds) |
Use the token
Include the token in the Authorization header of every API request:
curl -s http://localhost:3000/api/v1/agents \
-H "Authorization: Bearer $TOKEN" | jq .
Introspect a token
POST /api/v1/token/introspect
Check whether a token is currently active (valid, not expired, not revoked). Requires a Bearer token with tokens:read scope.
curl -s -X POST http://localhost:3000/api/v1/token/introspect \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=$TOKEN_TO_CHECK" | jq .
Response for an active token:
{
"active": true,
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"scope": "agents:read agents:write",
"token_type": "Bearer",
"iat": 1743151200,
"exp": 1743154800
}
Response for an inactive (expired or revoked) token:
{
"active": false
}
The introspect endpoint always returns
200 OK— even for inactive tokens. You must check theactivefield to determine token validity.
Revoke a token
POST /api/v1/token/revoke
Immediately invalidates a token, preventing it from being used for any subsequent requests. Requires a Bearer token.
curl -s -X POST http://localhost:3000/api/v1/token/revoke \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=$TOKEN_TO_REVOKE" | jq .
Response (200 OK):
{}
Notes on revocation:
- Revocation is immediate — the token is rejected on the next request
- Revoking an already-revoked or expired token is not an error (idempotent per RFC 7009)
- An agent can revoke its own tokens; revoking another agent's token requires an admin-scoped token
- Revoking a token does not affect the credential that issued it — new tokens can still be obtained using the same credentials
Token errors
401 invalid_client — wrong credentials
{
"error": "invalid_client",
"error_description": "Client authentication failed. Invalid client_id or client_secret."
}
Check that client_id matches the agent's agentId and client_secret is the current active secret.
403 unauthorized_client — agent suspended or monthly limit reached
{
"error": "unauthorized_client",
"error_description": "Agent is currently suspended and cannot obtain tokens."
}
Or:
{
"error": "unauthorized_client",
"error_description": "Free tier monthly token limit of 10,000 requests has been reached."
}
For suspension: reactivate the agent first. For the monthly limit: the counter resets on the first day of the next calendar month.
400 unsupported_grant_type
{
"error": "unsupported_grant_type",
"error_description": "Only 'client_credentials' grant type is supported."
}
Only client_credentials is supported. Do not use authorization_code, password, or other grant types.