# WS1 Spec — api-reference.md

## Target file
`docs/developers/api-reference.md`

## Objective
Replace the current 14-endpoint Phase 1 API reference with a complete reference covering all current
endpoints. The Developer must rewrite the file in full. Every endpoint must include: HTTP method +
path, one-line description, auth requirement, required scopes, request fields, response schema,
error responses, and a working curl example.

---

## Document structure

The updated file must open with the same Base URL and Authentication preamble (no changes). The
table of contents must list all sections below. The Errors section must be expanded with all new
error codes. Endpoint groups must appear in this order:

1. Agent Registry
2. Credentials
3. OAuth 2.0 / Tokens
4. Audit Log
5. Organizations
6. Analytics
7. API Tiers
8. Compliance
9. Webhooks
10. Federation
11. DID / OIDC
12. A2A Delegation
13. Marketplace

---

## Updated preamble (verbatim)

```
# API Reference

Complete reference for all SentryAgent.ai AgentIdP endpoints.

## Base URL

    http://localhost:3000/api/v1

The port is configured via the PORT environment variable (default: 3000).

## Authentication

All endpoints require a JWT Bearer token in the Authorization header unless noted otherwise:

    Authorization: Bearer <access_token>

Obtain a token via POST /api/v1/token using your agent's client_id and client_secret.

Endpoints marked **No auth** do not require a Bearer token. Endpoints marked **Unauthenticated** are
intentionally public.
```

---

## Errors section — updated error codes table

Preserve all existing error codes and add the following new ones:

| Code | HTTP Status | Description |
|------|-------------|-------------|
| `ORG_NOT_FOUND` | 404 | Organization with the given `orgId` does not exist |
| `ORG_ALREADY_EXISTS` | 409 | An organization with this slug already exists |
| `ORG_SUSPENDED` | 403 | Organization is suspended — operations are blocked |
| `MEMBER_ALREADY_EXISTS` | 409 | Agent is already a member of this organization |
| `DELEGATION_NOT_FOUND` | 404 | Delegation chain with the given `chainId` does not exist |
| `DELEGATION_EXPIRED` | 403 | Delegation token has expired |
| `DELEGATION_REVOKED` | 403 | Delegation token has been revoked |
| `DELEGATION_SCOPE_EXCEEDED` | 403 | Requested scopes exceed the delegator's own scopes |
| `TIER_UPGRADE_NOT_REQUIRED` | 400 | Target tier is not higher than the current tier |
| `WEBHOOK_NOT_FOUND` | 404 | Webhook subscription with the given `id` does not exist |
| `PARTNER_NOT_FOUND` | 404 | Federation partner with the given `id` does not exist |
| `COMPLIANCE_DISABLED` | 404 | AGNTCY compliance endpoints are disabled on this instance |

---

## Section 1 — Agent Registry

### POST /agents — Register a new agent

**Description**: 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 for the agent |
| `agentType` | enum | Yes | `screener` \| `classifier` \| `orchestrator` \| `extractor` \| `summarizer` \| `router` \| `monitor` \| `custom` |
| `version` | string | Yes | Semantic version string, e.g. `1.0.0` |
| `capabilities` | string[] | Yes | One or more `resource:action` strings (min 1) |
| `owner` | string | Yes | Owning team or organisation, 1–128 characters |
| `deploymentEnv` | enum | Yes | `development` \| `staging` \| `production` |
| `organization_id` | string | No | UUID of the org to scope the agent to. Required on multi-tenant instances. |

**Response** `201 Created`:

| Field | Type | Description |
|-------|------|-------------|
| `agentId` | UUID | System-assigned immutable identifier |
| `email` | string | Unique email identifier |
| `agentType` | string | Agent type |
| `version` | string | Semantic version |
| `capabilities` | string[] | Capability list |
| `owner` | string | Owning team |
| `deploymentEnv` | string | Deployment environment |
| `status` | string | Always `active` on creation |
| `createdAt` | ISO 8601 | Registration timestamp |
| `updatedAt` | ISO 8601 | Last update timestamp |

**Error responses**:

| Code | HTTP | Error code |
|------|------|-----------|
| Validation failure | 400 | `VALIDATION_ERROR` |
| Invalid token | 401 | `UNAUTHORIZED` |
| Missing scope | 403 | `INSUFFICIENT_SCOPE` |
| Free tier limit | 403 | `FREE_TIER_LIMIT_EXCEEDED` |
| Email taken | 409 | `AGENT_ALREADY_EXISTS` |
| Rate limit | 429 | `RATE_LIMIT_EXCEEDED` |

**curl 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

**Description**: 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 (`active` \| `suspended` \| `decommissioned`) |

**Response** `200 OK`: `{ data: Agent[], total: number, page: number, limit: number }`

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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

**Description**: Returns the full identity record for a single agent.
**Auth**: Bearer token with `agents:read` scope.

**Path parameters**: `agentId` (UUID)

**Response** `200 OK`: Full agent object (same fields as POST response).

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `AGENT_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### PATCH /agents/{agentId} — Update agent metadata

**Description**: Partially updates agent metadata. Immutable fields (`agentId`, `email`, `createdAt`) cannot be changed.
**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 full list) |
| `owner` | string | Updated owner |
| `deploymentEnv` | enum | Updated deployment environment |
| `status` | enum | `active` \| `suspended` \| `decommissioned`. Setting `decommissioned` is irreversible. |

**Response** `200 OK`: Full updated agent object.

**Error responses**: 400 `VALIDATION_ERROR` / `IMMUTABLE_FIELD`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE` / `AGENT_DECOMMISSIONED`, 404 `AGENT_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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

**Description**: Permanently decommissions an agent (soft delete). All active credentials are immediately revoked. Irreversible.
**Auth**: Bearer token with `agents:write` scope.

**Response** `204 No Content` (empty body).

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `AGENT_NOT_FOUND`, 409 `AGENT_ALREADY_DECOMMISSIONED`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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"
```

---

## Section 2 — Credentials

### POST /agents/{agentId}/credentials — Generate credentials

**Description**: 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. Omit for non-expiring credential. |

**Response** `201 Created`:

| Field | Type | Description |
|-------|------|-------------|
| `credentialId` | UUID | Unique credential identifier |
| `clientId` | UUID | Same as `agentId` |
| `clientSecret` | string | Plaintext secret (shown once only — store immediately) |
| `status` | string | `active` |
| `createdAt` | ISO 8601 | Creation timestamp |
| `expiresAt` | ISO 8601 \| null | Expiry date or null |
| `revokedAt` | ISO 8601 \| null | Always null on creation |

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE` / `AGENT_NOT_ACTIVE`, 404 `AGENT_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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

**Description**: Returns all credentials for an agent (active and revoked). The `clientSecret` is never returned.
**Auth**: Bearer token with `agents:read` scope.

**Query parameters**: `page` (default 1), `limit` (default 20, max 100), `status` (`active` \| `revoked`)

**Response** `200 OK`: `{ data: Credential[], total: number, page: number, limit: number }`

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `AGENT_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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

**Description**: Generates a new `clientSecret` for the same `credentialId`. The old secret is immediately invalidated.
**Auth**: Bearer token with `agents:write` scope.

**Request body** (`application/json`) — optional: `{ "expiresAt": "ISO 8601" }`

**Response** `200 OK`: Full credential object with new `clientSecret` (shown once only).

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `AGENT_NOT_FOUND` / `CREDENTIAL_NOT_FOUND`, 409 `CREDENTIAL_ALREADY_REVOKED`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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

**Description**: Permanently revokes a credential. Irreversible.
**Auth**: Bearer token with `agents:write` scope.

**Response** `204 No Content`.

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `AGENT_NOT_FOUND` / `CREDENTIAL_NOT_FOUND`, 409 `CREDENTIAL_ALREADY_REVOKED`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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"
```

---

## Section 3 — OAuth 2.0 / Tokens

### POST /token — Issue an access token

**Description**: Issues a signed RS256 JWT via the OAuth 2.0 Client Credentials grant.
**Auth**: No Bearer token — credentials are in the request body.
**Content-Type**: `application/x-www-form-urlencoded`

**Request fields** (form-encoded):

| Field | Required | Description |
|-------|----------|-------------|
| `grant_type` | Yes | Must be `client_credentials` |
| `client_id` | Yes | Agent's `agentId` (UUID) |
| `client_secret` | Yes | Credential secret |
| `scope` | No | Space-separated scopes. If omitted, all scopes are granted. |

**Response** `200 OK`:

| Field | Type | Description |
|-------|------|-------------|
| `access_token` | string | Signed RS256 JWT |
| `token_type` | string | Always `Bearer` |
| `expires_in` | integer | Lifetime in seconds (3600) |
| `scope` | string | Granted scopes (space-separated) |

**Error responses**:

| Code | HTTP | Error |
|------|------|-------|
| Bad request / bad grant | 400 | `{ "error": "unsupported_grant_type" }` |
| Bad credentials | 401 | `{ "error": "invalid_client" }` |
| Agent suspended or monthly limit | 403 | `{ "error": "unauthorized_client" }` |
| Rate limit | 429 | `RATE_LIMIT_EXCEEDED` |

**curl 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

**Description**: Checks whether a token is active. Always returns `200 OK` — check the `active` field.
**Auth**: Bearer token with `tokens:read` scope.
**Content-Type**: `application/x-www-form-urlencoded`

**Request fields**: `token` (required), `token_type_hint` (optional, `access_token`)

**Response** `200 OK` (active): `{ "active": true, "sub": "...", "client_id": "...", "scope": "...", "token_type": "Bearer", "iat": 0, "exp": 0 }`
**Response** `200 OK` (inactive): `{ "active": false }`

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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

**Description**: Immediately invalidates a token. Idempotent.
**Auth**: Bearer token.
**Content-Type**: `application/x-www-form-urlencoded`

**Request fields**: `token` (required), `token_type_hint` (optional)

**Response** `200 OK`: `{}` (empty object)

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `FORBIDDEN`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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 .
```

---

## Section 4 — Audit Log

### GET /audit — Query audit log

**Description**: 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` | string | — | Filter by action type (e.g. `token.issued`, `agent.created`) |
| `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** `200 OK`: `{ data: AuditEvent[], total: number, page: number, limit: number }`

**AuditEvent fields**: `eventId` (UUID), `agentId` (UUID), `action` (string), `outcome` (string), `ipAddress` (string), `userAgent` (string), `metadata` (object), `timestamp` (ISO 8601)

**Error responses**: 400 `VALIDATION_ERROR` / `RETENTION_WINDOW_EXCEEDED`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 429 `RATE_LIMIT_EXCEEDED`

**curl 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

**Description**: Returns a single audit event by its immutable `eventId`.
**Auth**: Bearer token with `audit:read` scope.

**Path parameters**: `eventId` (UUID)

**Response** `200 OK`: Single AuditEvent object.

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `AUDIT_EVENT_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/audit/$EVENT_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /audit/verify — Verify audit chain integrity

**Description**: Verifies the cryptographic hash chain of all audit events. Returns `verified: true` if the chain is intact. Rate limited to 30 req/min (computationally intensive).
**Auth**: Bearer token with `audit:read` scope.

**Query parameters**: `fromDate` (ISO 8601, optional), `toDate` (ISO 8601, optional)

**Response** `200 OK`:

| Field | Type | Description |
|-------|------|-------------|
| `verified` | boolean | `true` if chain is intact, `false` if tampering detected |
| `checkedCount` | integer | Number of events checked |
| `fromDate` | ISO 8601 \| null | Verification window start |
| `toDate` | ISO 8601 \| null | Verification window end |

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/audit/verify" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

## Section 5 — Organizations

### POST /organizations — Create an organization

**Description**: Creates a new tenant organization. Agents can be scoped to an organization via `organization_id`.
**Auth**: Bearer token (OPA scope enforcement — `admin:orgs` or equivalent policy).

**Request body** (`application/json`):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Display name, 1–255 characters |
| `slug` | string | Yes | URL-safe identifier — lowercase letters, digits, hyphens only |
| `planTier` | enum | No | `free` (default) \| `pro` \| `enterprise` |
| `maxAgents` | integer | No | Override the plan default agent limit |
| `maxTokensPerMonth` | integer | No | Override the plan default monthly token limit |

**Response** `201 Created`:

| Field | Type | Description |
|-------|------|-------------|
| `organizationId` | UUID | System-assigned organization identifier |
| `name` | string | Display name |
| `slug` | string | URL-safe slug |
| `planTier` | string | Current plan tier |
| `maxAgents` | integer | Agent limit |
| `maxTokensPerMonth` | integer | Monthly token limit |
| `status` | string | `active` |
| `createdAt` | ISO 8601 | Creation timestamp |
| `updatedAt` | ISO 8601 | Last update timestamp |

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 409 `ORG_ALREADY_EXISTS`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/organizations \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme AI",
    "slug": "acme-ai",
    "planTier": "pro"
  }' | jq .
```

---

### GET /organizations — List organizations

**Description**: Returns a paginated list of organizations.
**Auth**: Bearer token (OPA scope enforcement).

**Query parameters**: `page` (default 1), `limit` (default 20, max 100), `status` (`active` \| `suspended` \| `deleted`)

**Response** `200 OK`: `{ data: Organization[], total: number, page: number, limit: number }`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/organizations?status=active" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /organizations/{orgId} — Get organization by ID

**Description**: Returns the full record for a single organization.
**Auth**: Bearer token (OPA scope enforcement).

**Path parameters**: `orgId` (UUID)

**Response** `200 OK`: Full organization object.

**Error responses**: 401 `UNAUTHORIZED`, 404 `ORG_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/organizations/$ORG_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### PATCH /organizations/{orgId} — Update organization

**Description**: Partially updates an organization. The `slug` is immutable after creation.
**Auth**: Bearer token (OPA scope enforcement).

**Request body** (`application/json`) — all fields optional:

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | New display name |
| `planTier` | enum | `free` \| `pro` \| `enterprise` |
| `maxAgents` | integer | New agent limit |
| `maxTokensPerMonth` | integer | New token limit |
| `status` | enum | `active` \| `suspended` (use DELETE to set `deleted`) |

**Response** `200 OK`: Full updated organization object.

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 404 `ORG_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X PATCH "http://localhost:3000/api/v1/organizations/$ORG_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "planTier": "enterprise" }' | jq .
```

---

### DELETE /organizations/{orgId} — Delete organization

**Description**: Soft-deletes an organization. Sets status to `deleted`.
**Auth**: Bearer token (OPA scope enforcement).

**Response** `204 No Content`.

**Error responses**: 401 `UNAUTHORIZED`, 404 `ORG_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X DELETE "http://localhost:3000/api/v1/organizations/$ORG_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -o /dev/null -w "%{http_code}\n"
```

---

### POST /organizations/{orgId}/members — Add a member

**Description**: Adds an existing agent to an organization with a specified role.
**Auth**: Bearer token (OPA scope enforcement).

**Request body** (`application/json`):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `agentId` | UUID | Yes | The agent to add |
| `role` | enum | Yes | `member` \| `admin` |

**Response** `201 Created`:

| Field | Type | Description |
|-------|------|-------------|
| `memberId` | UUID | Membership record identifier |
| `organizationId` | UUID | Organization |
| `agentId` | UUID | Agent |
| `role` | string | `member` or `admin` |
| `joinedAt` | ISO 8601 | Membership creation timestamp |

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 404 `ORG_NOT_FOUND` / `AGENT_NOT_FOUND`, 409 `MEMBER_ALREADY_EXISTS`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X POST "http://localhost:3000/api/v1/organizations/$ORG_ID/members" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "'$AGENT_ID'",
    "role": "member"
  }' | jq .
```

---

## Section 6 — Analytics

All analytics endpoints are scoped to the authenticated agent's `organization_id`.

### GET /analytics/tokens — Token issuance trend

**Description**: Returns daily token issuance counts for the past N days, scoped to the current organization.
**Auth**: Bearer token.

**Query parameters**:

| Parameter | Type | Default | Max | Description |
|-----------|------|---------|-----|-------------|
| `days` | integer | 30 | 90 | Number of days to return |

**Response** `200 OK`:

```json
{
  "tenantId": "org-uuid",
  "days": 30,
  "data": [
    { "date": "2026-03-01", "count": 142 },
    { "date": "2026-03-02", "count": 198 }
  ]
}
```

**Error responses**: 400 `VALIDATION_ERROR` (days > 90), 401 `UNAUTHORIZED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/analytics/tokens?days=30" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /analytics/agents/activity — Agent activity heatmap

**Description**: Returns agent request counts grouped by day-of-week and hour (UTC), for the current organization.
**Auth**: Bearer token.

**Response** `200 OK`:

```json
{
  "tenantId": "org-uuid",
  "data": [
    { "dow": 1, "hour": 9, "count": 54 },
    { "dow": 1, "hour": 10, "count": 87 }
  ]
}
```

`dow` is 0 (Sunday) through 6 (Saturday). `hour` is 0–23 UTC.

**Error responses**: 401 `UNAUTHORIZED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/analytics/agents/activity" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /analytics/agents — Per-agent usage summary

**Description**: Returns token issuance counts per agent for the current calendar month, for the current organization.
**Auth**: Bearer token.

**Response** `200 OK`:

```json
{
  "tenantId": "org-uuid",
  "month": "2026-03",
  "data": [
    { "agentId": "uuid", "tokenCount": 312 },
    { "agentId": "uuid2", "tokenCount": 87 }
  ]
}
```

**Error responses**: 401 `UNAUTHORIZED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/analytics/agents" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

## Section 7 — API Tiers

### GET /tiers/status — Get current tier status

**Description**: Returns the organization's current plan tier, configured limits, and live usage counters.
**Auth**: Bearer token with a valid `organization_id` claim.

**Response** `200 OK`:

| Field | Type | Description |
|-------|------|-------------|
| `tier` | string | `free` \| `pro` \| `enterprise` |
| `limits.maxAgents` | integer | Maximum agents allowed |
| `limits.maxCallsPerDay` | integer | Maximum API calls per day |
| `limits.maxTokensPerDay` | integer | Maximum token issuances per day |
| `usage.agentCount` | integer | Current active agent count |
| `usage.callsToday` | integer | API calls made today |
| `usage.tokensToday` | integer | Tokens issued today |

**Error responses**: 401 `UNAUTHORIZED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/tiers/status" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### POST /tiers/upgrade — Initiate tier upgrade

**Description**: Creates a Stripe Checkout Session to upgrade the organization to a higher plan tier. Returns a one-time checkout URL to redirect the user to.
**Auth**: Bearer token with a valid `organization_id` claim.

**Request body** (`application/json`):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `target_tier` | enum | Yes | `pro` \| `enterprise` — must be higher than current tier |

**Response** `200 OK`:

```json
{ "checkoutUrl": "https://checkout.stripe.com/pay/cs_live_..." }
```

**Error responses**: 400 `VALIDATION_ERROR` / `TIER_UPGRADE_NOT_REQUIRED`, 401 `UNAUTHORIZED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/tiers/upgrade \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "target_tier": "pro" }' | jq .
```

---

## Section 8 — Compliance

### GET /compliance/controls — SOC 2 control status (public)

**Description**: Returns the live status of all SOC 2 Trust Services Criteria controls. No authentication required.
**Auth**: None.

**Response** `200 OK` (`Cache-Control: public, max-age=60`):

```json
{
  "controls": [
    { "id": "CC6.1", "name": "Logical Access Controls", "status": "pass", "lastChecked": "2026-04-04T00:00:00.000Z" },
    { "id": "CC7.2", "name": "System Monitoring", "status": "pass", "lastChecked": "2026-04-04T00:00:00.000Z" }
  ]
}
```

Each control: `id` (string), `name` (string), `status` (`pass` \| `fail` \| `unknown`), `lastChecked` (ISO 8601)

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/compliance/controls" | jq .
```

---

### GET /compliance/report — AGNTCY compliance report

**Description**: Generates an AGNTCY compliance report for the authenticated tenant. Cached in Redis for 5 minutes. Sets `X-Cache: HIT` when served from cache.
**Auth**: Bearer token.

**Response** `200 OK`:

```json
{
  "tenantId": "org-uuid",
  "generatedAt": "2026-04-04T00:00:00.000Z",
  "agntcyConformance": true,
  "agentCount": 12,
  "verifiedAgentCount": 12,
  "auditChainIntegrity": true,
  "from_cache": false
}
```

**Error responses**: 401 `UNAUTHORIZED`, 404 `COMPLIANCE_DISABLED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/compliance/report" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /compliance/agent-cards — Export AGNTCY agent cards

**Description**: Exports all active agents for the authenticated tenant as AGNTCY-standard agent card JSON objects.
**Auth**: Bearer token.

**Response** `200 OK`: Array of agent card objects.

Each card: `did` (string), `name` (string), `agentType` (string), `capabilities` (string[]), `owner` (string), `version` (string), `deploymentEnv` (string), `identityProvider` (string), `issuedAt` (ISO 8601)

**Error responses**: 401 `UNAUTHORIZED`, 404 `COMPLIANCE_DISABLED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/compliance/agent-cards" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

## Section 9 — Webhooks

All webhook endpoints require Bearer token authentication and are scoped to the authenticated agent's `organization_id`. Required scopes are enforced via OPA policy.

### POST /webhooks — Create a subscription

**Description**: Creates a new webhook subscription for the organization. The `signingSecret` is returned once only.
**Auth**: Bearer token. OPA enforces `webhooks:write`.

**Request body** (`application/json`):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Human-readable subscription name |
| `url` | string | Yes | HTTPS endpoint that will receive events |
| `events` | string[] | Yes | One or more event types to subscribe to (see event type list below) |

**Available event types**: `agent.created`, `agent.updated`, `agent.suspended`, `agent.reactivated`, `agent.decommissioned`, `credential.generated`, `credential.rotated`, `credential.revoked`, `token.issued`, `token.revoked`

**Response** `201 Created`:

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Subscription identifier |
| `organization_id` | UUID | Owning organization |
| `name` | string | Subscription name |
| `url` | string | Target endpoint URL |
| `events` | string[] | Subscribed event types |
| `active` | boolean | `true` |
| `signingSecret` | string | HMAC-SHA256 signing secret (shown once) |
| `failure_count` | integer | `0` |
| `created_at` | ISO 8601 | Creation timestamp |
| `updated_at` | ISO 8601 | Last update timestamp |

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod-events",
    "url": "https://my-app.example.com/hooks/sentryagent",
    "events": ["agent.created", "token.issued"]
  }' | jq .
```

---

### GET /webhooks — List subscriptions

**Description**: Returns all webhook subscriptions for the organization. Signing secrets are never returned.
**Auth**: Bearer token. OPA enforces `webhooks:read`.

**Response** `200 OK`: Array of subscription objects (without `signingSecret`).

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/webhooks" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /webhooks/{id} — Get subscription by ID

**Description**: Returns a single subscription by its UUID.
**Auth**: Bearer token. OPA enforces `webhooks:read`.

**Path parameters**: `id` (UUID)

**Response** `200 OK`: Single subscription object (without `signingSecret`).

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `WEBHOOK_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### PATCH /webhooks/{id} — Update subscription

**Description**: Partially updates a webhook subscription.
**Auth**: Bearer token. OPA enforces `webhooks:write`.

**Request body** (`application/json`) — all fields optional: `name` (string), `url` (string), `events` (string[]), `active` (boolean)

**Response** `200 OK`: Updated subscription object.

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `WEBHOOK_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X PATCH "http://localhost:3000/api/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }' | jq .
```

---

### DELETE /webhooks/{id} — Delete subscription

**Description**: Permanently deletes a webhook subscription and all its delivery records.
**Auth**: Bearer token. OPA enforces `webhooks:write`.

**Response** `204 No Content`.

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `WEBHOOK_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X DELETE "http://localhost:3000/api/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -o /dev/null -w "%{http_code}\n"
```

---

### GET /webhooks/{id}/deliveries — List delivery history

**Description**: Returns a paginated list of delivery attempts for a subscription.
**Auth**: Bearer token. OPA enforces `webhooks:read`.

**Query parameters**: `limit` (default 20), `offset` (default 0)

**Response** `200 OK`:

```json
{
  "deliveries": [...],
  "total": 47,
  "limit": 20,
  "offset": 0
}
```

Each delivery: `id` (UUID), `subscription_id` (UUID), `event_type` (string), `payload` (object), `status` (`pending` \| `delivered` \| `failed` \| `dead_letter`), `http_status_code` (integer \| null), `attempt_count` (integer), `next_retry_at` (ISO 8601 \| null), `delivered_at` (ISO 8601 \| null), `created_at` (ISO 8601), `updated_at` (ISO 8601)

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `WEBHOOK_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/webhooks/$WEBHOOK_ID/deliveries?limit=20&offset=0" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

## Section 10 — Federation

All partner management endpoints require the `admin:orgs` scope (enforced via OPA). The verify
endpoint requires any authenticated agent.

### POST /federation/trust — Register a trusted partner

**Description**: Registers a new trusted federation partner (a remote IdP whose tokens this instance will accept).
**Auth**: Bearer token. OPA enforces `admin:orgs`.

**Request body** (`application/json`): Implementation-defined fields for partner registration including `name` (string), `issuer` (string — partner's token issuer URL), `jwksUri` (string — partner's JWKS endpoint).

**Response** `201 Created`: Partner record with `id`, `name`, `issuer`, `jwksUri`, `createdAt`.

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/federation/trust \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "PartnerCorp IdP",
    "issuer": "https://idp.partnercorp.com",
    "jwksUri": "https://idp.partnercorp.com/.well-known/jwks.json"
  }' | jq .
```

---

### GET /federation/partners — List partners

**Description**: Returns all registered federation partners.
**Auth**: Bearer token. OPA enforces `admin:orgs`.

**Response** `200 OK`: Array of partner records.

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/federation/partners" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /federation/partners/{id} — Get partner by ID

**Description**: Returns a single federation partner record.
**Auth**: Bearer token. OPA enforces `admin:orgs`.

**Path parameters**: `id` (UUID)

**Error responses**: 401 `UNAUTHORIZED`, 403 `INSUFFICIENT_SCOPE`, 404 `PARTNER_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/federation/partners/$PARTNER_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### PATCH /federation/partners/{id} — Update partner

**Description**: Partially updates a federation partner record.
**Auth**: Bearer token. OPA enforces `admin:orgs`.

**curl example**:

```bash
curl -s -X PATCH "http://localhost:3000/api/v1/federation/partners/$PARTNER_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Updated Partner Name" }' | jq .
```

---

### DELETE /federation/partners/{id} — Delete partner

**Description**: Removes a federation partner. This instance will no longer accept tokens from the partner's issuer.
**Auth**: Bearer token. OPA enforces `admin:orgs`.

**Response** `204 No Content`.

**curl example**:

```bash
curl -s -X DELETE "http://localhost:3000/api/v1/federation/partners/$PARTNER_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -o /dev/null -w "%{http_code}\n"
```

---

### POST /federation/verify — Verify a federated token

**Description**: Verifies a token issued by a trusted federation partner. Returns the decoded claims if the token is valid and the issuer is trusted.
**Auth**: Bearer token (any authenticated agent — no `admin:orgs` required).

**Request body** (`application/json`): `{ "token": "<partner-issued-jwt>" }`

**Response** `200 OK`: `{ "valid": true, "claims": { ... } }` or `{ "valid": false, "reason": "..." }`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/federation/verify \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "token": "'$PARTNER_TOKEN'" }' | jq .
```

---

## Section 11 — DID / OIDC

### GET /agents/{agentId}/did — Get agent DID document

**Description**: Returns the W3C DID Core 1.0 document for an agent. Unauthenticated — publicly accessible.
**Auth**: None.

**Response** `200 OK`: W3C DID Document.

```json
{
  "@context": ["https://www.w3.org/ns/did/v1"],
  "id": "did:web:localhost%3A3000:agents:a1b2c3d4",
  "controller": "did:web:localhost%3A3000:agents:a1b2c3d4",
  "verificationMethod": [{
    "id": "did:web:localhost%3A3000:agents:a1b2c3d4#key-1",
    "type": "JsonWebKey2020",
    "controller": "did:web:localhost%3A3000:agents:a1b2c3d4",
    "publicKeyJwk": { "kty": "RSA", "n": "...", "e": "AQAB" }
  }],
  "authentication": ["did:web:localhost%3A3000:agents:a1b2c3d4#key-1"],
  "agntcy": {
    "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "agentType": "screener",
    "capabilities": ["resume:read"],
    "deploymentEnv": "production",
    "owner": "talent-team",
    "version": "1.0.0"
  }
}
```

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/did" | jq .
```

---

### GET /agents/{agentId}/did/resolve — Resolve agent DID

**Description**: Returns the full W3C DID Resolution Result format including metadata.
**Auth**: Bearer token + OPA policy.

**Response** `200 OK`:

```json
{
  "didDocument": { ... },
  "didDocumentMetadata": {
    "created": "2026-03-28T09:00:00.000Z",
    "updated": "2026-03-28T09:00:00.000Z",
    "deactivated": false
  },
  "didResolutionMetadata": {
    "contentType": "application/did+ld+json",
    "retrieved": "2026-04-04T00:00:00.000Z"
  }
}
```

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/did/resolve" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### GET /agents/{agentId}/did/card — Get AGNTCY agent card

**Description**: Returns the AGNTCY-format agent card for an agent. Unauthenticated.
**Auth**: None.

**Response** `200 OK`:

```json
{
  "did": "did:web:localhost%3A3000:agents:a1b2c3d4",
  "name": "screener-001@talent.ai",
  "agentType": "screener",
  "capabilities": ["resume:read"],
  "owner": "talent-team",
  "version": "1.0.0",
  "deploymentEnv": "production",
  "identityProvider": "https://sentryagent.ai",
  "issuedAt": "2026-04-04T00:00:00.000Z"
}
```

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/did/card" | jq .
```

---

### GET /.well-known/openid-configuration — OIDC discovery document

**Description**: Returns the OIDC Provider discovery document. Unauthenticated. Mounted at the server root (not under `/api/v1`).
**Auth**: None.

**curl example**:

```bash
curl -s "http://localhost:3000/.well-known/openid-configuration" | jq .
```

---

### GET /.well-known/jwks.json — JWKS endpoint

**Description**: Returns the JSON Web Key Set (public keys used to verify ID tokens). Unauthenticated.
**Auth**: None.

**curl example**:

```bash
curl -s "http://localhost:3000/.well-known/jwks.json" | jq .
```

---

### GET /agent-info — Agent identity claims

**Description**: Returns identity claims for the authenticated agent (equivalent to UserInfo in OIDC). Mounted at the server root.
**Auth**: Bearer token.

**curl example**:

```bash
curl -s "http://localhost:3000/agent-info" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### POST /api/v1/oidc/token — OIDC token exchange (GitHub Actions)

**Description**: Exchanges a GitHub OIDC JWT for a SentryAgent.ai access token. Unauthenticated — the GitHub OIDC token is the credential. Trust-policy enforcement happens inside the controller.
**Auth**: None (GitHub OIDC JWT in body).

**Request body** (`application/json`): `{ "github_token": "<github-oidc-jwt>", "agentId": "<uuid>" }`

**Response** `200 OK`: `{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/oidc/token \
  -H "Content-Type: application/json" \
  -d '{
    "github_token": "'$GITHUB_OIDC_TOKEN'",
    "agentId": "'$AGENT_ID'"
  }' | jq .
```

---

### POST /api/v1/oidc/trust-policies — Create trust policy

**Description**: Registers a trust policy that allows GitHub Actions workflows matching specific claims to exchange tokens.
**Auth**: Bearer token with `agents:write` scope.

**Request body** (`application/json`): Repository, branch, and claim constraints (implementation-defined fields).

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/oidc/trust-policies \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "'$AGENT_ID'",
    "repository": "my-org/my-repo",
    "branch": "main"
  }' | jq .
```

---

### GET /api/v1/oidc/trust-policies — List trust policies

**Description**: Returns all trust policies for an agent.
**Auth**: Bearer token with `agents:write` scope.

**Query parameters**: `agentId` (UUID, required)

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/oidc/trust-policies?agentId=$AGENT_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

### DELETE /api/v1/oidc/trust-policies/{id} — Delete trust policy

**Description**: Deletes a trust policy by its UUID.
**Auth**: Bearer token with `agents:write` scope.

**Response** `204 No Content`.

**curl example**:

```bash
curl -s -X DELETE "http://localhost:3000/api/v1/oidc/trust-policies/$POLICY_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -o /dev/null -w "%{http_code}\n"
```

---

## Section 12 — A2A Delegation

### POST /oauth2/token/delegate — Create a delegation chain

**Description**: Creates a delegation chain that grants a delegatee agent a subset of the delegator's scopes for a limited time.
**Auth**: Bearer token.

**Request body** (`application/json`):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `delegateeAgentId` | UUID | Yes | The agent that receives delegated authority |
| `scopes` | string[] | Yes | Scopes to delegate — must be a strict subset of the caller's own scopes |
| `ttlSeconds` | integer | Yes | Delegation lifetime in seconds. Min: 60, Max: 86400 |

**Response** `201 Created`:

| Field | Type | Description |
|-------|------|-------------|
| `delegationToken` | string | Signed delegation token (HMAC-SHA256) |
| `chainId` | UUID | Delegation chain identifier |
| `delegatorAgentId` | UUID | Agent granting the delegation |
| `delegateeAgentId` | UUID | Agent receiving the delegation |
| `scopes` | string[] | Delegated scopes |
| `expiresAt` | ISO 8601 | Expiry timestamp |

**Error responses**: 400 `VALIDATION_ERROR` / `DELEGATION_SCOPE_EXCEEDED`, 401 `UNAUTHORIZED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/oauth2/token/delegate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "delegateeAgentId": "'$DELEGATEE_AGENT_ID'",
    "scopes": ["agents:read"],
    "ttlSeconds": 3600
  }' | jq .
```

---

### POST /oauth2/token/verify-delegation — Verify a delegation token

**Description**: Verifies a delegation token and returns the chain details. Returns `valid: false` (not an error) for expired or revoked tokens.
**Auth**: Bearer token.

**Request body** (`application/json`): `{ "delegationToken": "<delegation-jwt>" }`

**Response** `200 OK`:

| Field | Type | Description |
|-------|------|-------------|
| `valid` | boolean | Whether the delegation is currently valid |
| `chainId` | UUID | Chain identifier |
| `delegatorAgentId` | UUID | Delegating agent |
| `delegateeAgentId` | UUID | Receiving agent |
| `scopes` | string[] | Delegated scopes |
| `issuedAt` | ISO 8601 | Issue timestamp |
| `expiresAt` | ISO 8601 | Expiry timestamp |
| `revokedAt` | ISO 8601 \| null | Revocation timestamp, or null |

**Error responses**: 400 `VALIDATION_ERROR`, 401 `UNAUTHORIZED`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X POST http://localhost:3000/api/v1/oauth2/token/verify-delegation \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "delegationToken": "'$DELEGATION_TOKEN'" }' | jq .
```

---

### DELETE /oauth2/token/delegate/{chainId} — Revoke a delegation chain

**Description**: Immediately revokes a delegation chain. The delegatee can no longer use the delegation token. Only the delegator can revoke their own chains.
**Auth**: Bearer token.

**Path parameters**: `chainId` (UUID)

**Response** `204 No Content`.

**Error responses**: 401 `UNAUTHORIZED`, 403 `FORBIDDEN`, 404 `DELEGATION_NOT_FOUND`, 429 `RATE_LIMIT_EXCEEDED`

**curl example**:

```bash
curl -s -X DELETE "http://localhost:3000/api/v1/oauth2/token/delegate/$CHAIN_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -o /dev/null -w "%{http_code}\n"
```

---

## Section 13 — Marketplace

The Marketplace is feature-flagged via `MARKETPLACE_ENABLED` env var. When disabled, all endpoints
return `404 NOT_FOUND`. All marketplace endpoints are **unauthenticated** — no Bearer token required.

### GET /marketplace/agents — List public agents

**Description**: Returns a paginated list of publicly-listed agents.
**Auth**: None.

**Query parameters**: `page` (default 1), `limit` (default 20, max 100), `q` (text search), `capability` (filter by capability string), `publisher` (filter by owner)

**Response** `200 OK`: `{ data: PublicAgent[], total: number, page: number, limit: number }`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/marketplace/agents?q=screener&limit=20" | jq .
```

---

### GET /marketplace/agents/{agentId} — Get public agent

**Description**: Returns a single public agent with its DID document included. Returns 404 if the agent is private or inactive.
**Auth**: None.

**Path parameters**: `agentId` (UUID)

**Response** `200 OK`: Public agent object including `didDocument` field.

**Error responses**: 404 `AGENT_NOT_FOUND`

**curl example**:

```bash
curl -s "http://localhost:3000/api/v1/marketplace/agents/$AGENT_ID" | jq .
```
