# WS4 Spec — guides/

## Target files
- `docs/developers/guides/README.md` — update index table
- `docs/developers/guides/register-an-agent.md` — surgical update
- `docs/developers/guides/manage-credentials.md` — surgical update
- `docs/developers/guides/issue-and-revoke-tokens.md` — surgical update
- `docs/developers/guides/query-audit-logs.md` — surgical update
- `docs/developers/guides/use-analytics-dashboard.md` — create new
- `docs/developers/guides/manage-api-tiers.md` — create new
- `docs/developers/guides/a2a-delegation.md` — create new
- `docs/developers/guides/configure-webhooks.md` — create new
- `docs/developers/guides/agntcy-compliance.md` — create new

---

## Part A — Updates to existing guides

### A1. guides/README.md

**Find** (entire table):
```markdown
| Guide | What it covers |
|-------|----------------|
| [Register an Agent](register-an-agent.md) | All registration fields, validation rules, common errors and fixes |
| [Manage Credentials](manage-credentials.md) | Generate, list, rotate, and revoke credentials |
| [Issue and Revoke Tokens](issue-and-revoke-tokens.md) | OAuth 2.0 Client Credentials flow, JWT structure, introspect, revoke |
| [Query Audit Logs](query-audit-logs.md) | Filters, pagination, event structure, 90-day retention |
```

**Replace with** (adding 5 new rows):
```markdown
| Guide | What it covers |
|-------|----------------|
| [Register an Agent](register-an-agent.md) | All registration fields, organization scoping, validation rules, common errors |
| [Manage Credentials](manage-credentials.md) | Generate, list, rotate, and revoke credentials |
| [Issue and Revoke Tokens](issue-and-revoke-tokens.md) | OAuth 2.0 Client Credentials flow, JWT structure, introspect, revoke |
| [Query Audit Logs](query-audit-logs.md) | Filters, pagination, event structure, 90-day retention |
| [Use the Analytics Dashboard](use-analytics-dashboard.md) | Query token trends, agent activity heatmap, and per-agent usage |
| [Manage API Tiers](manage-api-tiers.md) | Check current tier, understand limits, trigger a Stripe upgrade |
| [A2A Delegation](a2a-delegation.md) | Create and verify agent-to-agent delegation chains |
| [Configure Webhooks](configure-webhooks.md) | Subscribe to events, understand delivery guarantees, inspect history |
| [AGNTCY Compliance](agntcy-compliance.md) | Export agent cards, generate compliance reports, verify audit chain |
```

---

### A2. guides/register-an-agent.md

#### Edit A2a — Add `organization_id` to request fields table

**Find** (last row of the request fields table):
```markdown
| `deploymentEnv` | string (enum) | Yes | Target deployment environment. See values below. |
```

**Replace with**:
```markdown
| `deploymentEnv` | string (enum) | Yes | Target deployment environment. See values below. |
| `organization_id` | string (UUID) | No | UUID of the organization to scope this agent to. Recommended on all multi-tenant instances. |
```

#### Edit A2b — Update the example curl command to include organization_id

**Find** (the curl command in the example section):
```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", "candidate:score"],
    "owner": "talent-acquisition-team",
    "deploymentEnv": "production"
  }' | jq .
```

**Replace with**:
```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", "candidate:score"],
    "owner": "talent-acquisition-team",
    "deploymentEnv": "production",
    "organization_id": "'$ORG_ID'"
  }' | jq .
```

#### Edit A2c — Add note about organization_id after the curl example

After the "The `agentId` is assigned by the system — it is immutable and never changes." line,
insert:

```markdown
> **Organization scoping**: If you include `organization_id` in the request, the agent is
> associated with that organization. Analytics, webhook events, and tier enforcement are all
> scoped by organization. To create an organization first, see the
> [Quick Start](../quick-start.md) guide.
```

---

### A3. guides/manage-credentials.md

No content changes needed to this guide. All endpoint paths are current:
- `POST /api/v1/agents/{agentId}/credentials` — correct
- `GET /api/v1/agents/{agentId}/credentials` — correct
- `POST /api/v1/agents/{agentId}/credentials/{credentialId}/rotate` — correct
- `DELETE /api/v1/agents/{agentId}/credentials/{credentialId}` — correct

#### Edit A3a — Add a note about org-scoped token issuance at the top

After the first paragraph ("A credential is a `client_id` + `client_secret` pair..."), insert:

```markdown
> **Multi-tenant note**: Credentials issued for an agent that belongs to an organization will
> produce tokens carrying an `organization_id` claim. This claim is required by analytics,
> webhooks, tier enforcement, and A2A delegation. Ensure your agent is registered with
> `organization_id` before issuing credentials for production use.
```

---

### A4. guides/issue-and-revoke-tokens.md

No endpoint path changes needed. Paths are current:
- `POST /api/v1/token` — correct
- `POST /api/v1/token/introspect` — correct
- `POST /api/v1/token/revoke` — correct

#### Edit A4a — Expand available scopes table

**Find** (the scopes table):
```markdown
| Scope | What it allows |
|-------|----------------|
| `agents:read` | Read agent records |
| `agents:write` | Create, update, decommission agents |
| `tokens:read` | Introspect tokens |
| `audit:read` | Query audit logs |
```

**Replace with** (add new scopes for Phase 6 features):
```markdown
| Scope | What it allows |
|-------|----------------|
| `agents:read` | Read agent identity records |
| `agents:write` | Create, update, and decommission agents |
| `tokens:read` | Introspect tokens |
| `audit:read` | Query audit logs and verify audit chain integrity |
| `webhooks:read` | List webhook subscriptions and delivery history |
| `webhooks:write` | Create, update, and delete webhook subscriptions |
| `admin:orgs` | Manage organizations and federation partners |
```

---

### A5. guides/query-audit-logs.md

No endpoint path changes needed. Paths are current:
- `GET /api/v1/audit` — correct
- `GET /api/v1/audit/{eventId}` — correct

#### Edit A5a — Add audit/verify to the action table

After the existing action table (the "What gets logged" section), add a note:

```markdown
> **Audit chain verification**: In addition to querying events, you can verify the cryptographic
> integrity of the entire audit hash chain via `GET /api/v1/audit/verify`. This endpoint requires
> `audit:read` scope and is rate-limited to 30 requests/min. See the
> [API Reference](../api-reference.md#get-auditverify---verify-audit-chain-integrity) for details.
```

---

## Part B — New guide files

### B1. guides/use-analytics-dashboard.md

Write a complete new file with this content:

```markdown
# Use the Analytics Dashboard

This guide explains how to query the three analytics endpoints to understand your organization's
token usage and agent activity patterns.

All analytics endpoints require Bearer token authentication and are scoped to the organization
embedded in your token.

---

## Prerequisites

- A running AgentIdP instance
- A valid Bearer token with `organization_id` in its claims
- At least one agent registered and some token issuance activity

---

## Token issuance trend

`GET /api/v1/analytics/tokens`

Returns daily token issuance counts for the past N days (default 30, max 90). Use this to
track usage growth, identify traffic spikes, and plan capacity.

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

Response:

```json
{
  "tenantId": "org-0a1b2c3d-e4f5-6789-abcd-ef0123456789",
  "days": 30,
  "data": [
    { "date": "2026-03-06", "count": 142 },
    { "date": "2026-03-07", "count": 198 },
    { "date": "2026-03-08", "count": 0 }
  ]
}
```

**Interpreting the data**: Each item in `data` is one calendar day (UTC) with the number of
tokens issued on that day. Days with zero issuance are included with `count: 0`. The array
is ordered chronologically, oldest first.

**Using it**: Compare day-over-day counts to identify growth or anomalies. A sudden spike in
`count` may indicate an agent retry loop or a credential leak. Zero-count days during expected
operation may indicate a deployment issue.

**Query parameter**: `days` — positive integer, max 90. Returns `400 VALIDATION_ERROR` if
exceeded.

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

# Last 90 days (maximum)
curl -s "http://localhost:3000/api/v1/analytics/tokens?days=90" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

---

## Agent activity heatmap

`GET /api/v1/analytics/agents/activity`

Returns request counts grouped by day-of-week (0 = Sunday, 6 = Saturday) and hour (0–23, UTC).
Use this to identify peak usage windows for capacity planning and rate limit tuning.

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

Response:

```json
{
  "tenantId": "org-0a1b2c3d-e4f5-6789-abcd-ef0123456789",
  "data": [
    { "dow": 1, "hour": 9, "count": 54 },
    { "dow": 1, "hour": 10, "count": 87 },
    { "dow": 3, "hour": 14, "count": 201 }
  ]
}
```

**Interpreting the data**: `dow` is 0 (Sunday) through 6 (Saturday). `hour` is 0–23 UTC.
Only non-zero cells are returned — missing combinations had zero activity. Sort by `count`
descending to find your peak windows.

**Using it**: If most activity is on weekday mornings UTC, ensure your rate limit headroom
covers that window. If weekend activity is unexpectedly high, investigate which agents are
active.

---

## Per-agent usage summary

`GET /api/v1/analytics/agents`

Returns token issuance counts per agent for the current calendar month (UTC). Use this to
identify your most active agents and check if any single agent is consuming a
disproportionate share of your monthly token budget.

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

Response:

```json
{
  "tenantId": "org-0a1b2c3d-e4f5-6789-abcd-ef0123456789",
  "month": "2026-04",
  "data": [
    { "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "tokenCount": 312 },
    { "agentId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "tokenCount": 87 }
  ]
}
```

**Interpreting the data**: Each item shows an agent UUID and the number of tokens it has
issued this month. The response covers the full current calendar month from day 1 to now.
It resets on the first day of each month.

**Using it**: Cross-reference `agentId` values against `GET /api/v1/agents` to identify which
agents by name. If one agent accounts for >80% of usage, investigate whether it is token
caching correctly or requesting tokens unnecessarily.
```

---

### B2. guides/manage-api-tiers.md

Write a complete new file with this content:

```markdown
# Manage API Tiers

This guide explains how to check your organization's current plan tier, understand the enforced
limits, and initiate an upgrade via Stripe.

---

## Prerequisites

- A running AgentIdP instance
- A valid Bearer token with `organization_id` in its claims

---

## Check current tier status

`GET /api/v1/tiers/status`

Returns your organization's tier, the configured limits, and live usage counters for today.

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

Response:

```json
{
  "tier": "free",
  "limits": {
    "maxAgents": 10,
    "maxCallsPerDay": 1000,
    "maxTokensPerDay": 1000
  },
  "usage": {
    "agentCount": 3,
    "callsToday": 142,
    "tokensToday": 87
  }
}
```

**Understanding the fields**:

| Field | Description |
|-------|-------------|
| `tier` | Current plan: `free`, `pro`, or `enterprise` |
| `limits.maxAgents` | Maximum active (non-decommissioned) agents allowed |
| `limits.maxCallsPerDay` | Maximum total API calls per calendar day (UTC) |
| `limits.maxTokensPerDay` | Maximum token issuances per calendar day (UTC) |
| `usage.agentCount` | Current number of active agents |
| `usage.callsToday` | API calls made so far today |
| `usage.tokensToday` | Tokens issued so far today |

**When limits are reached**: The relevant endpoint returns `403 FREE_TIER_LIMIT_EXCEEDED`.
Daily counters reset at midnight UTC. The agent count limit is a current count, not a daily
counter — decommissioning an agent immediately frees capacity.

---

## Tier comparison

| Limit | Free | Pro | Enterprise |
|-------|------|-----|------------|
| Max agents | 10 | 100 | Unlimited |
| Max API calls / day | 1,000 | 50,000 | Unlimited |
| Max token issuances / day | 1,000 | 50,000 | Unlimited |

---

## Upgrade your tier

`POST /api/v1/tiers/upgrade`

Creates a Stripe Checkout Session and returns a one-time URL. Complete the payment in the
browser to upgrade your organization's tier.

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

Response:

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

Open `checkoutUrl` in a browser to complete payment. After successful payment, Stripe sends a
webhook to AgentIdP which automatically upgrades your organization's tier.

**Constraints**:
- `target_tier` must be `pro` or `enterprise`
- `target_tier` must be higher than your current tier (you cannot downgrade via this endpoint)
- Attempting to upgrade to the current or a lower tier returns `400 VALIDATION_ERROR`

```bash
# Upgrade from free to pro
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 .

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

---

## Common errors

### `400 VALIDATION_ERROR` — target_tier missing or invalid

```json
{
  "code": "VALIDATION_ERROR",
  "message": "target_tier must be one of: free, pro, enterprise.",
  "details": { "received": "premium" }
}
```

**Fix**: Use `"pro"` or `"enterprise"`.

### `400 TIER_UPGRADE_NOT_REQUIRED` — not an upgrade

**Fix**: You are already on this tier or a higher tier. Check `GET /api/v1/tiers/status` first.

### `401 UNAUTHORIZED` — token lacks organization_id

The tier endpoints require a token with an `organization_id` claim. Use a token issued by an
agent that was registered with `organization_id`. Tokens issued via the bootstrap method
(without an org) do not carry `organization_id` and will fail.
```

---

### B3. guides/a2a-delegation.md

Write a complete new file with this content:

```markdown
# A2A Delegation

Agent-to-Agent (A2A) delegation lets one agent grant another agent a subset of its OAuth 2.0
scopes for a defined period. This is the foundation for building secure multi-agent pipelines
where an orchestrator agent coordinates specialist sub-agents.

---

## Prerequisites

- A running AgentIdP instance
- Two registered agents: the delegator (has a Bearer token) and the delegatee (knows its
  `agentId`)
- The delegator's scopes must be a superset of the scopes it wants to delegate

---

## How delegation works

```
Delegator agent                            Delegatee agent
     |                                           |
     |-- POST /oauth2/token/delegate ----------->|  (creates chain server-side)
     |<-- { delegationToken, chainId, scopes } --|
     |                                           |
     |-- passes delegationToken out-of-band ---->|
     |                                           |
     |                   POST /oauth2/token/verify-delegation
     |                   <-- { valid: true, scopes, expiresAt }
     |                                           |
     |           (optional) DELETE /oauth2/token/delegate/{chainId}
```

---

## Step 1 — Create a delegation chain

The delegator agent creates the chain by specifying the delegatee's `agentId`, the scopes to
delegate (must be a strict subset of the delegator's own scopes), and the TTL in seconds.

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

Response (`201 Created`):

```json
{
  "delegationToken": "sa_del_a1b2c3d4e5f6...",
  "chainId": "d4e5f6a7-b8c9-0123-def0-123456789abc",
  "delegatorAgentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "delegateeAgentId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "scopes": ["agents:read"],
  "expiresAt": "2026-04-04T10:00:00.000Z"
}
```

Save the `delegationToken` and `chainId`:

```bash
export DELEGATION_TOKEN="sa_del_a1b2c3d4e5f6..."
export CHAIN_ID="d4e5f6a7-b8c9-0123-def0-123456789abc"
```

**TTL constraints**: minimum 60 seconds, maximum 86400 seconds (24 hours). Choose the minimum
TTL that covers the delegatee's task.

---

## Step 2 — Pass the delegation token to the delegatee

Pass `DELEGATION_TOKEN` to the delegatee agent out-of-band. This can be via a shared queue,
a direct API call to the sub-agent, or any other channel. The token is a signed opaque string —
do not parse it; treat it as an opaque credential.

---

## Step 3 — Verify the delegation token

The delegatee (or any agent checking the delegation) calls the verify endpoint. This confirms
the chain is valid and not expired or revoked.

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

Response (`200 OK` — valid delegation):

```json
{
  "valid": true,
  "chainId": "d4e5f6a7-b8c9-0123-def0-123456789abc",
  "delegatorAgentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "delegateeAgentId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "scopes": ["agents:read"],
  "issuedAt": "2026-04-04T09:00:00.000Z",
  "expiresAt": "2026-04-04T10:00:00.000Z",
  "revokedAt": null
}
```

Response (`200 OK` — expired delegation):

```json
{
  "valid": false,
  "chainId": "d4e5f6a7-b8c9-0123-def0-123456789abc",
  "delegatorAgentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "delegateeAgentId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "scopes": ["agents:read"],
  "issuedAt": "2026-04-03T09:00:00.000Z",
  "expiresAt": "2026-04-03T10:00:00.000Z",
  "revokedAt": null
}
```

> The verify endpoint always returns `200 OK`. Check the `valid` field — it is never an error
> response for an expired or revoked token.

---

## Step 4 — (Optional) Revoke the delegation early

If the delegatee has completed its task and you want to revoke the delegation before it expires,
the delegator calls:

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

Expected response: `204` (no body).

After revocation, verify requests for this chain return `{ "valid": false, "revokedAt": "<timestamp>" }`.

---

## Scope rules

- Delegated scopes must be a strict subset of the delegator's own token scopes
- You cannot delegate scopes you do not have
- You cannot delegate to yourself (delegateeAgentId must differ from delegatorAgentId)
- Delegation is not transitive — a delegatee cannot re-delegate to a third agent

---

## Common errors

### `400 VALIDATION_ERROR` — scope not a subset

The delegator attempted to delegate a scope it does not hold. Check `GET /api/v1/token/introspect`
to confirm which scopes your token carries.

### `400 VALIDATION_ERROR` — ttlSeconds out of range

Min: 60, Max: 86400. Values outside this range return a validation error.
```

---

### B4. guides/configure-webhooks.md

Write a complete new file with this content:

```markdown
# Configure Webhooks

Webhooks let AgentIdP push real-time events to your application when agents, credentials, or
tokens change state. This guide covers creating subscriptions, the available event types,
delivery guarantees, and how to inspect delivery history.

---

## Prerequisites

- A running AgentIdP instance
- A valid Bearer token with `organization_id` in its claims
- A publicly reachable HTTPS endpoint to receive events (for local development, use a tool
  like [ngrok](https://ngrok.com))

---

## Available event types

| Event type | Triggered when |
|-----------|----------------|
| `agent.created` | A new agent is registered |
| `agent.updated` | An agent's metadata is updated |
| `agent.suspended` | An agent's status changes to `suspended` |
| `agent.reactivated` | An agent's status changes from `suspended` to `active` |
| `agent.decommissioned` | An agent is decommissioned |
| `credential.generated` | New credentials are created for an agent |
| `credential.rotated` | A credential's secret is rotated |
| `credential.revoked` | A credential is revoked |
| `token.issued` | An access token is issued |
| `token.revoked` | An access token is revoked |

---

## Create a subscription

`POST /api/v1/webhooks`

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

Response (`201 Created`):

```json
{
  "id": "wh-1a2b3c4d-e5f6-7890-abcd-ef1234567890",
  "organization_id": "org-0a1b2c3d-e4f5-6789-abcd-ef0123456789",
  "name": "prod-agent-events",
  "url": "https://my-app.example.com/hooks/sentryagent",
  "events": ["agent.created", "agent.decommissioned", "token.issued"],
  "active": true,
  "signingSecret": "whsec_a1b2c3d4e5f6789...",
  "failure_count": 0,
  "created_at": "2026-04-04T09:00:00.000Z",
  "updated_at": "2026-04-04T09:00:00.000Z"
}
```

> **Save the `signingSecret` now.** It is shown once. Use it to verify the HMAC-SHA256
> signature on incoming webhook requests. See "Verifying delivery signatures" below.

```bash
export WEBHOOK_ID="wh-1a2b3c4d-e5f6-7890-abcd-ef1234567890"
export SIGNING_SECRET="whsec_a1b2c3d4e5f6789..."
```

---

## Webhook payload format

Every delivery sends a POST to your URL with `Content-Type: application/json` and this body:

```json
{
  "id": "evt-uuid-here",
  "event": "agent.created",
  "timestamp": "2026-04-04T09:00:00.000Z",
  "organization_id": "org-0a1b2c3d-e4f5-6789-abcd-ef0123456789",
  "data": {
    "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "email": "screener-001@talent.ai",
    "agentType": "screener"
  }
}
```

The `data` object contains event-specific fields. For `agent.*` events it includes agent
metadata. For `credential.*` events it includes `credentialId` and `agentId`. For `token.*`
events it includes `agentId` and `scope`.

---

## Verifying delivery signatures

AgentIdP signs every delivery with HMAC-SHA256 using your `signingSecret`. The signature is
in the `X-SentryAgent-Signature` header as `sha256=<hex-digest>`.

Verify it in Node.js:

```javascript
const crypto = require('crypto');

function verifySignature(rawBody, signingSecret, signatureHeader) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', signingSecret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
```

Always verify the signature before processing the event. Reject requests with invalid signatures
with `401 Unauthorized`.

---

## Delivery guarantees and retry policy

- AgentIdP delivers each event **at least once** — your endpoint may receive duplicates
- Use the `id` field to deduplicate events
- Delivery is attempted immediately; on failure, retries use exponential backoff
- After repeated failures, the delivery moves to `dead_letter` status
- Subscriptions with high `failure_count` may be automatically disabled

Delivery statuses: `pending` → `delivered` (success) or `failed` (attempt failed) → `dead_letter`
(all retries exhausted)

---

## List subscriptions

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

---

## Pause or resume a subscription

To pause (disable) a subscription without deleting it:

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

To resume:

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

---

## Inspect delivery history

`GET /api/v1/webhooks/{id}/deliveries`

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

Response:

```json
{
  "deliveries": [
    {
      "id": "del-uuid",
      "subscription_id": "wh-uuid",
      "event_type": "agent.created",
      "payload": { ... },
      "status": "delivered",
      "http_status_code": 200,
      "attempt_count": 1,
      "next_retry_at": null,
      "delivered_at": "2026-04-04T09:00:01.000Z",
      "created_at": "2026-04-04T09:00:00.000Z",
      "updated_at": "2026-04-04T09:00:01.000Z"
    }
  ],
  "total": 47,
  "limit": 20,
  "offset": 0
}
```

Use `offset` to paginate through delivery history. Increase `limit` to retrieve more records
per page (the server default is 20).

---

## Delete a subscription

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

Expected response: `204`. This permanently deletes the subscription and all its delivery records.
```

---

### B5. guides/agntcy-compliance.md

Write a complete new file with this content:

```markdown
# AGNTCY Compliance

This guide explains how to use AgentIdP's AGNTCY compliance features: exporting agent cards,
generating compliance reports, verifying audit chain integrity, and checking SOC 2 control status.

---

## Prerequisites

- A running AgentIdP instance
- `COMPLIANCE_ENABLED` environment variable not set to `false` (enabled by default)
- A valid Bearer token (for authenticated endpoints)
- At least one registered agent

---

## What is AGNTCY?

AGNTCY is an open standard from the Linux Foundation for AI agent identity and governance.
AgentIdP implements AGNTCY by giving every agent a DID and an agent card. The compliance
endpoints let you export and report on that data in structured, auditable formats.

---

## Export agent cards

`GET /api/v1/compliance/agent-cards`

Exports all active agents in your organization as AGNTCY-standard agent card JSON objects.
Suitable for ingestion by external compliance tools or AGNTCY-compatible registries.

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

Response (`200 OK`): Array of agent card objects.

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

**Use cases**:
- Share with external auditors to demonstrate your agent fleet
- Import into AGNTCY-compatible discovery registries
- Baseline snapshot before and after deployments

Save the output to a file:

```bash
curl -s "http://localhost:3000/api/v1/compliance/agent-cards" \
  -H "Authorization: Bearer $TOKEN" > agent-cards-$(date +%Y%m%d).json
```

---

## Generate a compliance report

`GET /api/v1/compliance/report`

Generates an AGNTCY compliance report for your tenant. The report is cached for 5 minutes
(check the `X-Cache` header to see if the response is fresh or cached).

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

Response (`200 OK`):

```json
{
  "tenantId": "org-0a1b2c3d-e4f5-6789-abcd-ef0123456789",
  "generatedAt": "2026-04-04T09:00:00.000Z",
  "agntcyConformance": true,
  "agentCount": 12,
  "verifiedAgentCount": 12,
  "auditChainIntegrity": true,
  "from_cache": false
}
```

**Interpreting the fields**:

| Field | Description |
|-------|-------------|
| `agntcyConformance` | `true` if all agents have valid DIDs and the audit chain is intact |
| `agentCount` | Total active agents in the organization |
| `verifiedAgentCount` | Agents with a resolvable DID document |
| `auditChainIntegrity` | `true` if the audit event hash chain has not been tampered with |
| `from_cache` | `true` if served from Redis cache (up to 5 minutes old) |

**Force a fresh report**: Wait 5 minutes for the cache to expire. The `from_cache: false`
response is always freshly generated.

---

## Verify audit chain integrity

`GET /api/v1/audit/verify`

Verifies that the cryptographic hash chain of audit events is intact. Returns `verified: true`
if no tampering is detected. Rate limited to 30 requests/minute (computationally intensive).

Requires: Bearer token with `audit:read` scope.

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

Response (`200 OK`):

```json
{
  "verified": true,
  "checkedCount": 1247,
  "fromDate": null,
  "toDate": null
}
```

Verify a specific date window:

```bash
curl -s "http://localhost:3000/api/v1/audit/verify?fromDate=2026-03-01T00:00:00.000Z&toDate=2026-03-31T23:59:59.999Z" \
  -H "Authorization: Bearer $TOKEN" | jq .
```

**Interpreting the result**:
- `verified: true` — no tampering detected in the checked window
- `verified: false` — the hash chain has a broken link; contact SentryAgent.ai support
- `checkedCount` — number of audit events verified

---

## Check SOC 2 control status (public)

`GET /api/v1/compliance/controls`

Returns the live status of all SOC 2 Trust Services Criteria controls. No authentication
required. Responses are cached by CDN/proxies for 60 seconds (`Cache-Control: public, max-age=60`).

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

Response (`200 OK`):

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

Each control has a `status` of `pass`, `fail`, or `unknown`. Status is updated by background
jobs that run periodically. This endpoint is suitable for embedding in external status pages
or compliance dashboards without sharing API credentials.

---

## When compliance endpoints are disabled

If `COMPLIANCE_ENABLED=false` is set in the server environment, the AGNTCY compliance endpoints
(`/compliance/report` and `/compliance/agent-cards`) return `404 COMPLIANCE_DISABLED`. The SOC 2
endpoints (`/compliance/controls` and `/audit/verify`) are never gated and always active.
```
