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

## Table of Contents

- [Errors](#errors)
- [Agent Registry](#section-1--agent-registry)
- [Credentials](#section-2--credentials)
- [OAuth 2.0 / Tokens](#section-3--oauth-20--tokens)
- [Audit Log](#section-4--audit-log)
- [Organizations](#section-5--organizations)
- [Analytics](#section-6--analytics)
- [API Tiers](#section-7--api-tiers)
- [Compliance](#section-8--compliance)
- [Webhooks](#section-9--webhooks)
- [Federation](#section-10--federation)
- [DID / OIDC](#section-11--did--oidc)
- [A2A Delegation](#section-12--a2a-delegation)
- [Marketplace](#section-13--marketplace)

---

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

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

---

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