Agent Identity API
API reference for agent identity endpoints — Client ID Metadata, JWKS, SVID issuance, token exchange, introspection, ExtAuthZ, and TBAC policies.
All identity endpoints are mounted at /api/v1/identity (or /identity via the legacy alias).
Public Endpoints
These endpoints require no authentication. They serve the agent's public identity documents.
Get Client Metadata
Returns the IETF Client ID Metadata document for an agent, including the signed vc+jwt Badge.
GET /api/v1/identity/agents/{agentId}/client-metadata.json
Response 200 OK
{
"client_id": "https://api.meetloyd.com/api/v1/identity/agents/{agentId}/client-metadata.json",
"client_name": "MeetLoyd Agent: Sales Rep",
"client_uri": "https://api.meetloyd.com/agents/{agentId}",
"grant_types": [
"client_credentials",
"urn:ietf:params:oauth:grant-type:token-exchange"
],
"token_endpoint_auth_method": "private_key_jwt",
"token_endpoint": "https://api.meetloyd.com/api/v1/identity/oauth/token",
"jwks_uri": "https://api.meetloyd.com/api/v1/identity/agents/{agentId}/jwks.json",
"scope": "meetloyd:agents:read meetloyd:agents:execute meetloyd:conversations:read ...",
"contacts": ["agents@meetloyd.com"],
"agent_type": "ai_agent",
"spiffe_id": "spiffe://meetloyd.com/tenant/{tenantId}/agent/{agentId}",
"vc+jwt": "eyJhbGciOiJFUzI1NiIs..."
}
| Field | Description |
|---|---|
client_id | Self-referential URL (this is the agent's OAuth identity) |
grant_types | Supported OAuth grant types |
token_endpoint | Token exchange endpoint URL |
jwks_uri | Agent's public keys |
spiffe_id | SPIFFE identity |
vc+jwt | Signed W3C Verifiable Credential Badge |
Get Agent JWKS
Returns the agent's JSON Web Key Set (public keys).
GET /api/v1/identity/agents/{agentId}/jwks.json
Response 200 OK (application/jwk-set+json)
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"kid": "agent-key-abc123",
"use": "sig",
"alg": "ES256",
"x": "...",
"y": "..."
}
]
}
Get Platform JWKS
Returns MeetLoyd's platform-level public keys for verifying Badges, SVIDs, and exchanged tokens.
GET /api/v1/identity/.well-known/jwks.json
Response 200 OK (application/jwk-set+json)
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"kid": "platform-abc123",
"use": "sig",
"alg": "ES256",
"x": "...",
"y": "..."
}
]
}
Cached for 1 hour.
Get SPIFFE Trust Bundle
Returns the SPIFFE Trust Bundle for verifying JWT-SVIDs.
GET /.well-known/spiffe/trust-bundle
Also available at:
GET /api/v1/identity/.well-known/spiffe/trust-bundle
Response 200 OK
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"kid": "platform-abc123",
"use": "jwt-svid",
"alg": "ES256",
"x": "...",
"y": "..."
}
],
"spiffe_sequence": 1740000000,
"spiffe_refresh_hint": 300
}
| Field | Description |
|---|---|
spiffe_sequence | Monotonic sequence number (derived from platform key creation timestamp — increases on key rotation) |
spiffe_refresh_hint | Recommended refresh interval in seconds (300 = 5 min) |
Token Exchange
RFC 8693 Security Token Service. Exchanges a subject token (SVID) for a scoped access token for delegation.
This endpoint is public — the subject token serves as authentication.
POST /api/v1/identity/oauth/token
Content-Type: application/x-www-form-urlencoded
Also accepts application/json.
Request Parameters
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Yes | The caller's JWT-SVID (or user JWT) |
subject_token_type | Yes | Token type (e.g., urn:ietf:params:oauth:token-type:jwt) |
audience | Yes | Target agent: SPIFFE ID, client_id URL, or bare agent ID |
scope | Yes | Space-separated tool scopes (e.g., tools:get_payments tools:list_accounts) |
resource | No | Resource URI (reserved for future use) |
requested_token_type | No | Requested token type (defaults to access_token) |
Example (form-urlencoded)
POST /api/v1/identity/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&subject_token=eyJhbGci...&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt&audience=spiffe%3A%2F%2Fmeetloyd.com%2Ftenant%2Ft1%2Fagent%2Fagent-b&scope=tools%3Aget_payments+tools%3Alist_accounts
Example (JSON)
{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "eyJhbGci...",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"audience": "spiffe://meetloyd.com/tenant/t1/agent/agent-b",
"scope": "tools:get_payments tools:list_accounts"
}
Success Response 200 OK
{
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIs...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "tools:get_payments tools:list_accounts"
}
Response headers always include Cache-Control: no-store and Pragma: no-cache.
Decoded Access Token
{
"header": {
"alg": "ES256",
"kid": "platform-abc123",
"typ": "at+jwt"
},
"payload": {
"sub": "spiffe://meetloyd.com/tenant/t1/agent/agent-a",
"aud": ["spiffe://meetloyd.com/tenant/t1/agent/agent-b"],
"azp": "https://api.meetloyd.com/api/v1/identity/agents/agent-b/client-metadata.json",
"scope": "tools:get_payments tools:list_accounts",
"act": {
"sub": "https://api.meetloyd.com/api/v1/identity/agents/agent-a/client-metadata.json"
},
"tools": ["get_payments", "list_accounts"],
"tenant_id": "t1",
"jti": "txn_abc123...",
"iss": "https://meetloyd.com",
"iat": 1771797000,
"exp": 1771800600
}
}
Error Response (RFC 6749 format)
{
"error": "insufficient_scope",
"error_description": "Caller does not have permission to delegate: delete_records"
}
| Error Code | Status | Description |
|---|---|---|
unsupported_grant_type | 400 | Wrong grant_type value |
invalid_request | 400 | Missing required parameters |
invalid_grant | 401 | Subject token is invalid or expired |
invalid_target | 400 | Target agent not found |
invalid_target | 403 | Cross-tenant delegation attempted |
invalid_scope | 400 | No valid tools:* scopes in request |
insufficient_scope | 400 | Caller has none of the requested tools |
server_error | 500 | Platform signing key unavailable |
Authenticated Endpoints
These endpoints require a JWT bearer token.
Issue JWT-SVID
Issue a short-lived SPIFFE JWT-SVID for an agent.
POST /api/v1/identity/agents/{agentId}/svid
Authorization: Bearer {jwt}
Content-Type: application/json
Required permission: agents:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
audience | string | string[] | Yes | Who can consume this SVID |
ttlSeconds | number | No | Token TTL (default: 3600, max: 86400) |
Example
{
"audience": "spiffe://meetloyd.com/tenant/t1/agent/agent-b",
"ttlSeconds": 3600
}
Response 200 OK
{
"svid": "eyJhbGciOiJFUzI1NiIs...",
"spiffeId": "spiffe://meetloyd.com/tenant/t1/agent/agent-a",
"expiresAt": "2026-02-23T09:00:00.000Z",
"audience": ["spiffe://meetloyd.com/tenant/t1/agent/agent-b"]
}
The agent must have a SPIFFE ID (keys generated) before SVIDs can be issued.
Rotate Agent Key
Rotate an agent's EC P-256 key pair. The old key is marked as rotated, and a new key is generated.
POST /api/v1/identity/agents/{agentId}/keys/rotate
Authorization: Bearer {jwt}
Required permission: agents:write
Response 200 OK
{
"message": "Key rotated successfully",
"keyId": "agent-key-xyz789",
"algorithm": "ES256"
}
Revoke Agent Key
Revoke a specific key by ID. The key must belong to the specified agent (key-agent binding is enforced).
DELETE /api/v1/identity/agents/{agentId}/keys/{keyId}
Authorization: Bearer {jwt}
Required permission: agents:write
Response 200 OK
{
"message": "Key revoked successfully"
}
Error 404 Not Found — Key not found or belongs to a different agent.
Token Introspection
Verify and inspect an exchanged access token. Per RFC 7662.
POST /api/v1/identity/oauth/introspect
Authorization: Bearer {jwt}
Content-Type: application/json
Required permission: agents:read
Request Body
{
"token": "eyJhbGciOiJFUzI1NiIs..."
}
Active Token Response 200 OK
{
"active": true,
"sub": "spiffe://meetloyd.com/tenant/t1/agent/agent-a",
"aud": ["spiffe://meetloyd.com/tenant/t1/agent/agent-b"],
"azp": "https://api.meetloyd.com/api/v1/identity/agents/agent-b/client-metadata.json",
"scope": "tools:get_payments tools:list_accounts",
"act": {
"sub": "https://api.meetloyd.com/api/v1/identity/agents/agent-a/client-metadata.json"
},
"tools": ["get_payments", "list_accounts"],
"tenant_id": "t1",
"iss": "https://meetloyd.com",
"iat": 1771797000,
"exp": 1771800600,
"jti": "txn_abc123...",
"token_type": "Bearer"
}
Inactive/Invalid Token Response 200 OK
{
"active": false
}
Per RFC 7662, introspection always returns 200 OK — an invalid token simply returns { "active": false }.
Audience Formats
The audience parameter (in SVID issuance and token exchange) accepts three formats:
| Format | Example |
|---|---|
| SPIFFE ID | spiffe://meetloyd.com/tenant/t1/agent/agent-b |
| Client ID URL | https://api.meetloyd.com/api/v1/identity/agents/agent-b/client-metadata.json |
| Bare agent ID | agent-b |
All formats resolve to the same agent via database lookup.
Verifying Tokens Externally
To verify any MeetLoyd-signed token (SVID, Badge, or access token) from an external system:
- Fetch the trust bundle —
GET https://api.meetloyd.com/.well-known/spiffe/trust-bundle - Verify the JWT signature against the keys in the trust bundle
- Check the
typheader to determine the token type:JWT— SVID (identity proof)vc+jwt— Badge (capability proof)at+jwt— Access token (delegation proof)
- Validate standard claims —
issshould behttps://meetloyd.com, checkexpfor expiry - Check
aud— verify your identity is in the audience list
All tokens use ES256 (ECDSA P-256).
ExtAuthZ (TBAC Enforcement)
Authorize a Delegated Tool Call
Verify whether a delegated tool call should be allowed. This is the main enforcement endpoint for TBAC.
This endpoint is public — the exchanged token serves as authentication.
POST /api/v1/identity/authorize
Content-Type: application/json
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The exchanged access token (from token exchange) |
tool | string | Yes | The tool being invoked |
callee | string | Yes | The callee agent ID |
Example
{
"token": "eyJhbGciOiJFUzI1NiIs...",
"tool": "get_payments",
"callee": "agent-b"
}
Allowed Response 200 OK
{
"allowed": true,
"reason": "policy_allow",
"caller": "agent-a",
"callee": "agent-b",
"tool": "get_payments",
"enforcement_mode": "enforce",
"check_duration_ms": 12
}
Denied Response 403 Forbidden
{
"allowed": false,
"reason": "policy_deny",
"caller": "agent-a",
"callee": "agent-b",
"tool": "delete_records",
"enforcement_mode": "enforce",
"check_duration_ms": 8
}
Possible reasons:
| Reason | Description |
|---|---|
token_invalid | Token signature or claims verification failed |
invalid_caller_spiffe_id | Could not parse caller identity from token |
tool_not_in_scope | Requested tool not in token's tools[] |
policy_allow | TBAC policy explicitly allows |
policy_deny | TBAC policy explicitly denies |
no_policy_audit_allow | No policy found, audit/warn mode defaults to allow |
no_policy_enforce_deny | No policy found, enforce mode defaults to deny |
internal_error | Unexpected error during check |
TBAC Policy Management
List Policies
List TBAC policies for the authenticated user's tenant.
GET /api/v1/identity/tbac/policies
Authorization: Bearer {jwt}
Required permission: settings:read
Query Parameters
| Parameter | Type | Description |
|---|---|---|
callerAgentId | string | Filter by caller agent ID |
calleeAgentId | string | Filter by callee agent ID |
toolName | string | Filter by tool name |
limit | number | Max results (default: 50) |
offset | number | Pagination offset (default: 0) |
Response 200 OK
{
"policies": [
{
"id": "tbac_abc123",
"tenantId": "t1",
"callerAgentId": "agent-a",
"calleeAgentId": "agent-b",
"toolName": "get_payments",
"effect": "allow",
"conditions": {},
"description": "Allow agent-a to call get_payments on agent-b",
"createdBy": "user-1",
"createdAt": "2026-02-23T10:00:00.000Z",
"updatedAt": "2026-02-23T10:00:00.000Z"
}
],
"total": 1
}
Create Policy
Create a new TBAC policy.
POST /api/v1/identity/tbac/policies
Authorization: Bearer {jwt}
Content-Type: application/json
Required permission: settings:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
callerAgentId | string | Yes | Caller agent ID or * for any |
calleeAgentId | string | Yes | Callee agent ID or * for any |
toolName | string | Yes | Tool name or * for any |
effect | string | No | allow (default) or deny |
conditions | object | No | JSON conditions (reserved for future use) |
description | string | No | Human-readable description |
Example
{
"callerAgentId": "agent-a",
"calleeAgentId": "agent-b",
"toolName": "get_payments",
"effect": "allow",
"description": "Allow agent-a to call get_payments on agent-b"
}
Response 201 Created
Returns the created policy object.
Error 409 Conflict — A policy already exists for this (caller, callee, tool) combination.
Update Policy
Update an existing TBAC policy.
PATCH /api/v1/identity/tbac/policies/{id}
Authorization: Bearer {jwt}
Content-Type: application/json
Required permission: settings:write
Request Body (all fields optional)
| Field | Type | Description |
|---|---|---|
effect | string | allow or deny |
conditions | object | JSON conditions |
description | string | Human-readable description |
Response 200 OK — Returns the updated policy object.
Error 404 Not Found — Policy not found or belongs to a different tenant.
Delete Policy
Delete a TBAC policy.
DELETE /api/v1/identity/tbac/policies/{id}
Authorization: Bearer {jwt}
Required permission: settings:write
Response 200 OK
{
"message": "Policy deleted successfully"
}
Error 404 Not Found — Policy not found or belongs to a different tenant.