Skip to content

Agents API

The Agents API is split between two surfaces:

  • Tenant-admin (dashboard)/v1/auth/dashboard/tenants/{slug}/agents/**, session-authenticated, owner-role required for write operations.
  • End-user/v1/users/me/agents, JWT-authenticated, scoped to the caller’s user identity within the resolved tenant.

Cross-tenant access on either surface returns 404, never 403, per project convention.

The canonical machine-readable spec lives in packages/api/openapi.json (regenerated by pnpm run spec:update). What follows is a human guide; field-level shapes are authoritative in the OpenAPI document.

POST /v1/auth/dashboard/tenants/{slug}/agents
Content-Type: application/json
{
"name": "Concierge bot",
"description": "Books rooms on behalf of guests",
"agent_class": "mcp-server",
"scopes": ["read:bookings", "write:bookings"],
"grant_types": [
"urn:ietf:params:oauth:grant-type:token-exchange",
"urn:openid:params:grant-type:ciba"
],
"agent_token_max_ttl_sec": 300,
"agent_settings": {
"rate_limits": { "api_calls_per_min": 120 },
"max_concurrent_tokens": 50
}
}

Response 201: { ...AgentResponse, client_secret } — secret revealed exactly once.

Errors:

  • 400 oauth_client/use_agents_endpoint — sent if you POSTed client_type: 'agent' to the legacy /oauth-clients endpoint. Soft-redirect — use the agents endpoint instead.
  • 403 agent/tier_unauthorized — Free tier; upgrade to Pro+.
  • 422 agent/invalid_grant_type — grant outside the agent allowlist ({token-exchange, ciba, device-code, client-credentials}).
  • 422 agent/invalid_fieldredirect_uris non-empty (agents are headless).
GET /v1/auth/dashboard/tenants/{slug}/agents
GET /v1/auth/dashboard/tenants/{slug}/agents?include_revoked=true

Cursor-paginated (default limit=20, max 100). Default selector: clientType='agent' AND revoked_at IS NULL. Pass include_revoked=true to see revoked agents.

GET /v1/auth/dashboard/tenants/{slug}/agents/{agentId}
PATCH /v1/auth/dashboard/tenants/{slug}/agents/{agentId}
POST /v1/auth/dashboard/tenants/{slug}/agents/{agentId}/rotate-secret

PATCH bumps agent_metadata_version. Rotate-secret reveals the new secret exactly once.

PATCH and rotate-secret on a revoked agent → 409 agent/already_revoked.

DELETE /v1/auth/dashboard/tenants/{slug}/agents/{agentId}

Response 200: { id, name, revoked_at, agent_metadata_version }.

The revocation pipeline (atomic):

  1. UPDATE oauth_clients SET revoked_at = now() WHERE id = ? AND revoked_at IS NULL (conditional — second call is a no-op).
  2. Insert an agent_token_denylist row valid until now() + agent_token_max_ttl_sec.
  3. Emit agent.revoked audit (HIGH severity) + webhook.
  4. Notify all tenant owners by email.

Bearer-auth middleware consults the denylist after signature verification on every request; tokens whose act.sub matches an active denylist row are rejected with WWW-Authenticate: Bearer error="invalid_token", error_description="agent_revoked" 401.

Idempotent — re-revoking returns 200 with the existing revoked_at and fires no second audit / webhook.

GET /v1/auth/dashboard/tenants/{slug}/agents/{agentId}/activity
?cursor=&limit=50&since=&until=&actor_user_id=&action_type=

Returns { data: AgentActionResponse[], pagination: { next_cursor, has_more } }. Default limit=50, max 200. RBAC: only tenant.role === 'owner' may read other users’ actions.

AgentActionResponse fields: id, action, actor_user_id?, actor_user_email?, resource?, method_or_event?, jti?, scope_used?, ip_hash_prefix? (12 chars), user_agent_hash_prefix?, created_at. Raw IP / UA hashes are never returned.

GET /v1/users/me/agents
Authorization: Bearer <user-jwt>

Returns { data: UserAgentResponse[] }, sorted by last_action_at DESC.

UserAgentResponse fields: agent_client_id, agent_name, agent_logo_url?, agent_class?, last_action_at?, action_count, revoked_at? (this user revoked), agent_revoked_at? (tenant admin revoked).

DELETE /v1/users/me/agents/{agentClientId}
Authorization: Bearer <user-jwt>

Inserts a user_agent_revocations row keyed (tenant_id, user_id, agent_client_id) with reason='gdpr_art_7_withdrawal'. Token-issuance gates in token-exchange + CIBA reject any future mint where (act.sub, sub) matches.

Important: already-issued tokens remain valid until their TTL expires (≤900s ceiling). For instant per-token invalidation across all users, the tenant admin uses DELETE /v1/auth/dashboard/tenants/{slug}/agents/{agentId}.

Response 200: { agent_client_id, revoked_at, reason }. Idempotent.

  • Writes → RATE_LIMIT_AUTH_WRITE_MAX.
  • Reads + activity → RATE_LIMIT_AUTH_READ_MAX.
  • Per-agent overrides via agent_settings.rate_limits: token_exchange_per_min, ciba_initiate_per_min, device_code_per_min, client_credentials_per_min, api_calls_per_min. Hard ceiling: RATE_LIMIT_OAUTH_AGENT_CEILING = 600.
  • Hits emit agent.rate_limited audit events with the offending scope and the computed limit.

The Node SDK exposes the end-user surface only (admin endpoints stay in the dashboard’s session-auth realm):

import { RakomiClient } from '@rakomi/node';
const client = new RakomiClient({ apiKey: process.env.RAKOMI_API_KEY! });
const userToken = '<end-user-jwt>';
const list = await client.users.me.agents.list({ userToken });
if (list.ok) {
for (const agent of list.data.data) {
console.log(agent.agent_name, agent.action_count);
}
}
const revoked = await client.users.me.agents.revoke({
userToken,
agentClientId: 'agent_abc',
});

Both helpers return the SDK’s Result shape ({ ok: true, data } | { ok: false, error }) and never throw on expected 4xx responses. Typed errors are exported for instanceof checks: AgentsUnauthorizedError, AgentNotFoundError, AgentsRateLimitedError, AgentsNetworkError.