# 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 .
```
