Adds the full bedroom-developer-docs OpenSpec change implementation: - docs/developers/README.md — index page - docs/developers/quick-start.md — bootstrap to working token in 7 steps - docs/developers/concepts.md — AgentIdP, AGNTCY, lifecycle, OAuth 2.0, free tier - docs/developers/guides/README.md — guide index - docs/developers/guides/register-an-agent.md — all fields, validation, common errors - docs/developers/guides/manage-credentials.md — generate, list, rotate, revoke - docs/developers/guides/issue-and-revoke-tokens.md — OAuth 2.0 flow, introspect, revoke - docs/developers/guides/query-audit-logs.md — filters, pagination, 90-day retention - docs/developers/api-reference.md — all 14 endpoints, all error codes, curl examples Also commits deferred OpenSpec housekeeping from previous session: - Archives phase-1-mvp-implementation change to openspec/changes/archive/ - Adds bedroom-developer-docs change artifacts (30/30 tasks complete) - Syncs 4 delta specs to openspec/specs/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
584 lines
16 KiB
Markdown
584 lines
16 KiB
Markdown
# API Reference
|
||
|
||
Complete reference for all 14 endpoints across the four SentryAgent.ai AgentIdP services.
|
||
|
||
## Base URL
|
||
|
||
```
|
||
http://localhost:3000/api/v1
|
||
```
|
||
|
||
The port is configured via the `PORT` environment variable (default: `3000`).
|
||
|
||
All endpoints are currently unversioned within the path prefix `/api/v1`. API versioning will be introduced in Phase 2.
|
||
|
||
## Authentication
|
||
|
||
All endpoints require a JWT Bearer token in the `Authorization` header:
|
||
|
||
```
|
||
Authorization: Bearer <access_token>
|
||
```
|
||
|
||
Obtain a token via `POST /token` using your agent's `client_id` and `client_secret`.
|
||
|
||
## Table of Contents
|
||
|
||
- [Errors](#errors)
|
||
- [Agent Registry](#agent-registry) — 5 endpoints
|
||
- [OAuth 2.0 Tokens](#oauth-20-tokens) — 3 endpoints
|
||
- [Credential Management](#credential-management) — 4 endpoints
|
||
- [Audit Log](#audit-log) — 2 endpoints
|
||
|
||
---
|
||
|
||
## Errors
|
||
|
||
All error responses use this envelope:
|
||
|
||
```json
|
||
{
|
||
"code": "ERROR_CODE",
|
||
"message": "Human-readable description.",
|
||
"details": {}
|
||
}
|
||
```
|
||
|
||
The `details` field is optional and provides additional context (e.g. which field failed validation).
|
||
|
||
### Error codes
|
||
|
||
| Code | HTTP Status | Description |
|
||
|------|-------------|-------------|
|
||
| `VALIDATION_ERROR` | 400 | Request body or query parameter failed validation |
|
||
| `UNAUTHORIZED` | 401 | Missing, expired, or invalid Bearer token |
|
||
| `FORBIDDEN` | 403 | Valid token but insufficient scope |
|
||
| `AGENT_NOT_FOUND` | 404 | Agent with the given `agentId` does not exist |
|
||
| `CREDENTIAL_NOT_FOUND` | 404 | Credential with the given `credentialId` does not exist |
|
||
| `AUDIT_EVENT_NOT_FOUND` | 404 | Audit event with the given `eventId` does not exist (or outside retention window) |
|
||
| `AGENT_ALREADY_EXISTS` | 409 | An agent with this email is already registered |
|
||
| `AGENT_ALREADY_DECOMMISSIONED` | 409 | Agent has already been decommissioned |
|
||
| `CREDENTIAL_ALREADY_REVOKED` | 409 | Credential has already been revoked |
|
||
| `RATE_LIMIT_EXCEEDED` | 429 | 100 req/min limit exceeded |
|
||
| `FREE_TIER_LIMIT_EXCEEDED` | 403 | Free tier resource limit reached |
|
||
| `INSUFFICIENT_SCOPE` | 403 | Token is missing a required scope |
|
||
| `IMMUTABLE_FIELD` | 400 | Attempt to modify a field that cannot be changed |
|
||
| `AGENT_NOT_ACTIVE` | 403 | Operation requires agent to be in `active` status |
|
||
| `AGENT_DECOMMISSIONED` | 403 | Cannot modify a decommissioned agent |
|
||
| `RETENTION_WINDOW_EXCEEDED` | 400 | Requested audit date is outside the 90-day retention window |
|
||
| `INTERNAL_SERVER_ERROR` | 500 | Unexpected server error |
|
||
|
||
### Rate limit headers
|
||
|
||
Every response includes rate limit headers:
|
||
|
||
| Header | Description |
|
||
|--------|-------------|
|
||
| `X-RateLimit-Limit` | Maximum requests per minute (100) |
|
||
| `X-RateLimit-Remaining` | Requests remaining in current window |
|
||
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
|
||
|
||
On `429` responses, wait until `X-RateLimit-Reset` before retrying.
|
||
|
||
---
|
||
|
||
## Agent Registry
|
||
|
||
### POST /agents — Register a new agent
|
||
|
||
Creates a new AI agent identity. The `agentId` is system-assigned.
|
||
|
||
**Auth**: Bearer token with `agents:write` scope.
|
||
|
||
**Request body** (`application/json`):
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `email` | string | Yes | Unique email-format identifier |
|
||
| `agentType` | enum | Yes | `screener` \| `classifier` \| `orchestrator` \| `extractor` \| `summarizer` \| `router` \| `monitor` \| `custom` |
|
||
| `version` | string | Yes | Semantic version (e.g. `1.0.0`) |
|
||
| `capabilities` | string[] | Yes | `resource:action` strings, min 1 |
|
||
| `owner` | string | Yes | Owning team/org, 1–128 chars |
|
||
| `deploymentEnv` | enum | Yes | `development` \| `staging` \| `production` |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `201` | Agent registered successfully |
|
||
| `400` | Validation error |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope or free tier limit reached |
|
||
| `409` | Email already registered |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:3000/api/v1/agents \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"email": "screener-001@talent.ai",
|
||
"agentType": "screener",
|
||
"version": "1.0.0",
|
||
"capabilities": ["resume:read", "email:send"],
|
||
"owner": "talent-team",
|
||
"deploymentEnv": "production"
|
||
}' | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### GET /agents — List agents
|
||
|
||
Returns a paginated list of registered agents.
|
||
|
||
**Auth**: Bearer token with `agents:read` scope.
|
||
|
||
**Query parameters**:
|
||
|
||
| Parameter | Type | Default | Description |
|
||
|-----------|------|---------|-------------|
|
||
| `page` | integer | 1 | Page number (1-based) |
|
||
| `limit` | integer | 20 | Results per page (max 100) |
|
||
| `owner` | string | — | Filter by owner (exact match) |
|
||
| `agentType` | enum | — | Filter by agent type |
|
||
| `status` | enum | — | Filter by status |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | List returned |
|
||
| `400` | Invalid query parameters |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s "http://localhost:3000/api/v1/agents?page=1&limit=20&status=active" \
|
||
-H "Authorization: Bearer $TOKEN" | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### GET /agents/{agentId} — Get agent by ID
|
||
|
||
Returns the full identity record for a single agent.
|
||
|
||
**Auth**: Bearer token with `agents:read` scope.
|
||
|
||
**Path parameters**:
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|------|-------------|
|
||
| `agentId` | UUID | The agent's immutable identifier |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Agent record returned |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope |
|
||
| `404` | Agent not found |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID" \
|
||
-H "Authorization: Bearer $TOKEN" | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### PATCH /agents/{agentId} — Update agent metadata
|
||
|
||
Partially updates agent metadata. Only provided fields are changed. Immutable fields (`agentId`, `email`, `createdAt`) cannot be updated.
|
||
|
||
**Auth**: Bearer token with `agents:write` scope.
|
||
|
||
**Request body** (`application/json`) — all fields optional:
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `agentType` | enum | Updated agent type |
|
||
| `version` | string | Updated semantic version |
|
||
| `capabilities` | string[] | Updated capabilities (replaces the full list) |
|
||
| `owner` | string | Updated owner |
|
||
| `deploymentEnv` | enum | Updated deployment environment |
|
||
| `status` | enum | Updated status (`active` \| `suspended` \| `decommissioned`) |
|
||
|
||
> Setting `status` to `decommissioned` is **irreversible**. The agent cannot be reactivated.
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Agent updated, full record returned |
|
||
| `400` | Validation error or attempt to modify immutable field |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope or agent is decommissioned |
|
||
| `404` | Agent not found |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s -X PATCH "http://localhost:3000/api/v1/agents/$AGENT_ID" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{ "version": "1.5.0", "status": "suspended" }' | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### DELETE /agents/{agentId} — Decommission an agent
|
||
|
||
Permanently decommissions an agent (soft delete). All active credentials are immediately revoked. This operation is **irreversible**.
|
||
|
||
**Auth**: Bearer token with `agents:write` scope.
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `204` | Agent decommissioned (no body) |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope |
|
||
| `404` | Agent not found |
|
||
| `409` | Agent already decommissioned |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s -X DELETE "http://localhost:3000/api/v1/agents/$AGENT_ID" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-o /dev/null -w "%{http_code}\n"
|
||
```
|
||
|
||
---
|
||
|
||
## OAuth 2.0 Tokens
|
||
|
||
### POST /token — Issue an access token
|
||
|
||
Issues a signed RS256 JWT via the OAuth 2.0 Client Credentials grant.
|
||
|
||
**Auth**: Client credentials in the request body (no Bearer token required for this endpoint).
|
||
|
||
> **Content-Type**: This endpoint uses `application/x-www-form-urlencoded`, not JSON.
|
||
|
||
**Request fields** (form-encoded):
|
||
|
||
| Field | Required | Description |
|
||
|-------|----------|-------------|
|
||
| `grant_type` | Yes | Must be `client_credentials` |
|
||
| `client_id` | Yes | Your agent's `agentId` (UUID) |
|
||
| `client_secret` | Yes | The credential secret |
|
||
| `scope` | No | Space-separated scopes. If omitted, all scopes are granted. |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Token issued |
|
||
| `400` | Malformed request, invalid scope, or unsupported grant type |
|
||
| `401` | Invalid `client_id` or `client_secret` |
|
||
| `403` | Agent suspended or monthly token limit reached |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Note on 429**: The `X-RateLimit-*` headers are returned on all responses, including `429`.
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
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 .
|
||
```
|
||
|
||
---
|
||
|
||
### POST /token/introspect — Introspect a token
|
||
|
||
Checks whether a token is active. Returns `{ "active": false }` for expired or revoked tokens — always `200 OK`.
|
||
|
||
**Auth**: Bearer token with `tokens:read` scope.
|
||
|
||
> **Content-Type**: `application/x-www-form-urlencoded`
|
||
|
||
**Request fields**:
|
||
|
||
| Field | Required | Description |
|
||
|-------|----------|-------------|
|
||
| `token` | Yes | The JWT to introspect |
|
||
| `token_type_hint` | No | Optional hint — `access_token` |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Result returned (check `active` field) |
|
||
| `400` | Missing `token` parameter |
|
||
| `401` | Caller's Bearer token is invalid |
|
||
| `403` | Caller's token lacks `tokens:read` scope |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
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 .
|
||
```
|
||
|
||
---
|
||
|
||
### POST /token/revoke — Revoke a token
|
||
|
||
Immediately invalidates a token. Idempotent — revoking an already-revoked token returns `200`.
|
||
|
||
**Auth**: Bearer token (agent can revoke its own tokens).
|
||
|
||
> **Content-Type**: `application/x-www-form-urlencoded`
|
||
|
||
**Request fields**:
|
||
|
||
| Field | Required | Description |
|
||
|-------|----------|-------------|
|
||
| `token` | Yes | The JWT to revoke |
|
||
| `token_type_hint` | No | Optional hint — `access_token` |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Token revoked (or was already inactive) |
|
||
| `400` | Missing `token` parameter |
|
||
| `401` | Caller's Bearer token is invalid |
|
||
| `403` | Insufficient permissions to revoke this token |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
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 .
|
||
```
|
||
|
||
---
|
||
|
||
## Credential Management
|
||
|
||
### POST /agents/{agentId}/credentials — Generate credentials
|
||
|
||
Creates a new `client_id` + `client_secret` pair. The `clientSecret` is returned **once only**.
|
||
|
||
**Auth**: Bearer token with `agents:write` scope.
|
||
|
||
**Request body** (`application/json`) — optional:
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `expiresAt` | ISO 8601 | No | Optional expiry date. Must be a future date. If omitted, credential does not expire. |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `201` | Credential created — save `clientSecret` now |
|
||
| `400` | Invalid `expiresAt` |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope or agent not active |
|
||
| `404` | Agent not found |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s -X POST "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{ "expiresAt": "2027-01-01T00:00:00.000Z" }' | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### GET /agents/{agentId}/credentials — List credentials
|
||
|
||
Returns all credentials (active and revoked). The `clientSecret` is never returned.
|
||
|
||
**Auth**: Bearer token with `agents:read` scope.
|
||
|
||
**Query parameters**:
|
||
|
||
| Parameter | Type | Default | Description |
|
||
|-----------|------|---------|-------------|
|
||
| `page` | integer | 1 | Page number |
|
||
| `limit` | integer | 20 | Results per page (max 100) |
|
||
| `status` | enum | — | Filter by `active` or `revoked` |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | List returned |
|
||
| `400` | Invalid query parameters |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope |
|
||
| `404` | Agent not found |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials?status=active" \
|
||
-H "Authorization: Bearer $TOKEN" | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### POST /agents/{agentId}/credentials/{credentialId}/rotate — Rotate a credential
|
||
|
||
Replaces the `clientSecret` for the same `credentialId`. The old secret is immediately invalidated.
|
||
|
||
**Auth**: Bearer token with `agents:write` scope.
|
||
|
||
**Request body** (`application/json`) — optional:
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `expiresAt` | ISO 8601 | No | New expiry for the rotated credential |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Credential rotated — save new `clientSecret` now |
|
||
| `400` | Invalid `expiresAt` |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope |
|
||
| `404` | Agent or credential not found |
|
||
| `409` | Credential is already revoked |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s -X POST \
|
||
"http://localhost:3000/api/v1/agents/$AGENT_ID/credentials/$CREDENTIAL_ID/rotate" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{}' | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### DELETE /agents/{agentId}/credentials/{credentialId} — Revoke a credential
|
||
|
||
Permanently revokes a credential. The credential can no longer obtain tokens. Irreversible.
|
||
|
||
**Auth**: Bearer token with `agents:write` scope.
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `204` | Credential revoked (no body) |
|
||
| `401` | Invalid token |
|
||
| `403` | Insufficient scope |
|
||
| `404` | Agent or credential not found |
|
||
| `409` | Credential already revoked |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s -X DELETE \
|
||
"http://localhost:3000/api/v1/agents/$AGENT_ID/credentials/$CREDENTIAL_ID" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-o /dev/null -w "%{http_code}\n"
|
||
```
|
||
|
||
---
|
||
|
||
## Audit Log
|
||
|
||
### GET /audit — Query audit log
|
||
|
||
Returns a paginated, filtered list of audit events (most recent first).
|
||
|
||
**Auth**: Bearer token with `audit:read` scope.
|
||
|
||
**Query parameters**:
|
||
|
||
| Parameter | Type | Default | Description |
|
||
|-----------|------|---------|-------------|
|
||
| `page` | integer | 1 | Page number |
|
||
| `limit` | integer | 50 | Results per page (max 200) |
|
||
| `agentId` | UUID | — | Filter by agent |
|
||
| `action` | enum | — | Filter by action type (see [Audit Log guide](guides/query-audit-logs.md)) |
|
||
| `outcome` | enum | — | `success` or `failure` |
|
||
| `fromDate` | ISO 8601 | — | Events at or after this timestamp (max 90 days ago) |
|
||
| `toDate` | ISO 8601 | — | Events at or before this timestamp |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Events returned |
|
||
| `400` | Invalid parameters or date outside retention window |
|
||
| `401` | Invalid token |
|
||
| `403` | Token lacks `audit:read` scope |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s "http://localhost:3000/api/v1/audit?agentId=$AGENT_ID&action=token.issued&limit=50" \
|
||
-H "Authorization: Bearer $TOKEN" | jq .
|
||
```
|
||
|
||
---
|
||
|
||
### GET /audit/{eventId} — Get audit event by ID
|
||
|
||
Returns a single audit event by its immutable `eventId`.
|
||
|
||
**Auth**: Bearer token with `audit:read` scope.
|
||
|
||
**Path parameters**:
|
||
|
||
| Parameter | Type | Description |
|
||
|-----------|------|-------------|
|
||
| `eventId` | UUID | The audit event's identifier |
|
||
|
||
**Response codes**:
|
||
|
||
| Code | Meaning |
|
||
|------|---------|
|
||
| `200` | Audit event returned |
|
||
| `401` | Invalid token |
|
||
| `403` | Token lacks `audit:read` scope |
|
||
| `404` | Event not found or outside 90-day retention window |
|
||
| `429` | Rate limit exceeded |
|
||
|
||
**Example**:
|
||
|
||
```bash
|
||
curl -s "http://localhost:3000/api/v1/audit/$EVENT_ID" \
|
||
-H "Authorization: Bearer $TOKEN" | jq .
|
||
```
|