Skip to main content

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..."
}
FieldDescription
client_idSelf-referential URL (this is the agent's OAuth identity)
grant_typesSupported OAuth grant types
token_endpointToken exchange endpoint URL
jwks_uriAgent's public keys
spiffe_idSPIFFE identity
vc+jwtSigned 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
}
FieldDescription
spiffe_sequenceMonotonic sequence number (derived from platform key creation timestamp — increases on key rotation)
spiffe_refresh_hintRecommended 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

ParameterRequiredDescription
grant_typeYesMust be urn:ietf:params:oauth:grant-type:token-exchange
subject_tokenYesThe caller's JWT-SVID (or user JWT)
subject_token_typeYesToken type (e.g., urn:ietf:params:oauth:token-type:jwt)
audienceYesTarget agent: SPIFFE ID, client_id URL, or bare agent ID
scopeYesSpace-separated tool scopes (e.g., tools:get_payments tools:list_accounts)
resourceNoResource URI (reserved for future use)
requested_token_typeNoRequested 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 CodeStatusDescription
unsupported_grant_type400Wrong grant_type value
invalid_request400Missing required parameters
invalid_grant401Subject token is invalid or expired
invalid_target400Target agent not found
invalid_target403Cross-tenant delegation attempted
invalid_scope400No valid tools:* scopes in request
insufficient_scope400Caller has none of the requested tools
server_error500Platform 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

FieldTypeRequiredDescription
audiencestring | string[]YesWho can consume this SVID
ttlSecondsnumberNoToken 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:

FormatExample
SPIFFE IDspiffe://meetloyd.com/tenant/t1/agent/agent-b
Client ID URLhttps://api.meetloyd.com/api/v1/identity/agents/agent-b/client-metadata.json
Bare agent IDagent-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:

  1. Fetch the trust bundleGET https://api.meetloyd.com/.well-known/spiffe/trust-bundle
  2. Verify the JWT signature against the keys in the trust bundle
  3. Check the typ header to determine the token type:
    • JWT — SVID (identity proof)
    • vc+jwt — Badge (capability proof)
    • at+jwt — Access token (delegation proof)
  4. Validate standard claimsiss should be https://meetloyd.com, check exp for expiry
  5. 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

FieldTypeRequiredDescription
tokenstringYesThe exchanged access token (from token exchange)
toolstringYesThe tool being invoked
calleestringYesThe 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:

ReasonDescription
token_invalidToken signature or claims verification failed
invalid_caller_spiffe_idCould not parse caller identity from token
tool_not_in_scopeRequested tool not in token's tools[]
policy_allowTBAC policy explicitly allows
policy_denyTBAC policy explicitly denies
no_policy_audit_allowNo policy found, audit/warn mode defaults to allow
no_policy_enforce_denyNo policy found, enforce mode defaults to deny
internal_errorUnexpected 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

ParameterTypeDescription
callerAgentIdstringFilter by caller agent ID
calleeAgentIdstringFilter by callee agent ID
toolNamestringFilter by tool name
limitnumberMax results (default: 50)
offsetnumberPagination 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

FieldTypeRequiredDescription
callerAgentIdstringYesCaller agent ID or * for any
calleeAgentIdstringYesCallee agent ID or * for any
toolNamestringYesTool name or * for any
effectstringNoallow (default) or deny
conditionsobjectNoJSON conditions (reserved for future use)
descriptionstringNoHuman-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)

FieldTypeDescription
effectstringallow or deny
conditionsobjectJSON conditions
descriptionstringHuman-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.