> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mnemom.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Card-Change Notifications

> Subscribe to SSE or webhook notifications when a canonical card changes, sourced from the transparency log. A separate, per-agent mechanism from org-level webhooks.

Two complementary surfaces for reacting to canonical card changes without polling:

* **Server-Sent Events** at `GET /v1/agents/{id}/stream` — long-lived HTTP connection; you
  receive a `card_changed` SSE frame whenever the agent's canonical card recomposes.
* **Signed webhook subscriptions** at `POST /v1/agents/{id}/notifications/webhook` — Mnemom
  POSTs a signed payload to your URL on each canonical change.

Both surfaces read from the [transparency log](/concepts/transparency-log). This is a distinct
mechanism from the org-level [Webhook Notifications](/guides/webhooks) system — different
endpoints, its own signing convention (below), and scoped to one agent's card rather than a
whole org's event stream.

<Note>
  Both surfaces are gated per-agent by `agents.sse_enabled` / `agents.webhook_enabled`, which
  default to off. There is currently no self-service API to flip these flags — [contact
  support](https://mnemom.ai/pricing) to enable card-change notifications for an agent. Until
  enabled, both endpoints return 404 (to avoid enumerating which agents exist).
</Note>

## SSE channel

### Connect

```bash theme={null}
curl -N -H "Authorization: Bearer $TOKEN" \
  https://api.mnemom.ai/v1/agents/smolt-e2ca60ef/stream
```

The response is `Content-Type: text/event-stream`. Each canonical change emits a frame like:

```
event: card_changed
id: 4711
data: {"agent_id":"smolt-e2ca60ef","card_kind":"alignment","content_hash":"...","version":17,"composed_at":"2026-05-22T12:00:00Z","log_index":4711,"attestation_jws":"..."}
```

### Reconnect with a cursor

The `id` field on each frame is the transparency-log `log_index`. On reconnect, pass it as
`Last-Event-ID` (standard SSE reconnection header) or as the `since` query parameter —
`Last-Event-ID` takes precedence when both are present:

```bash theme={null}
curl -N -H "Authorization: Bearer $TOKEN" \
  -H "Last-Event-ID: 4711" https://api.mnemom.ai/v1/agents/smolt-e2ca60ef/stream
# OR
curl -N -H "Authorization: Bearer $TOKEN" \
  "https://api.mnemom.ai/v1/agents/smolt-e2ca60ef/stream?since=4711"
```

Frames are delivered strictly in `log_index` ascending order, so the cursor is monotone.

### Stream lifetime

Each connection caps at 5 minutes. Reconnect with the latest cursor — `EventSource`
implementations handle this automatically. A `: keepalive <timestamp>` comment frame is emitted
every 15 seconds; a final `event: close` frame fires before the connection drops.

## Webhook channel

### Subscribe

```bash theme={null}
curl -X POST https://api.mnemom.ai/v1/agents/smolt-e2ca60ef/notifications/webhook \
  -H "Authorization: Bearer $MNEMOM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-server.example.com/mnemom/card-changed",
    "consumer_id": "tenant-a"
  }'
```

Response:

```json theme={null}
{
  "subscription_id": "sub-3e8f...",
  "webhook_url": "https://your-server.example.com/mnemom/card-changed",
  "secret": "wEbHt73...JNkP",
  "expires_at": "2026-06-21T00:00:00Z"
}
```

The `secret` is shown **exactly once** — store it server-side. Mnemom holds only its hash.
Subscriptions expire after 30 days by default unless you pass an explicit `expires_at`.

### Delivery shape

On every canonical card change, Mnemom POSTs to your URL:

```json theme={null}
{
  "type": "card_changed",
  "delivered_at": "2026-05-22T12:00:01.234Z",
  "data": {
    "agent_id": "smolt-e2ca60ef",
    "card_kind": "alignment",
    "content_hash": "...",
    "version": 17,
    "composed_at": "2026-05-22T12:00:00Z",
    "log_index": 4711,
    "attestation_jws": "..."
  }
}
```

With headers:

```
X-Mnemom-Webhook-Id: sub-3e8f...
X-Mnemom-Signature: t=1716393600,v1=<hex-hmac-sha256>
User-Agent: mnemom-cards/1 (+https://mnemom.ai/v1)
Content-Type: application/json
```

<Note>
  This is a different header pair from the org-level [Webhook Notifications](/guides/webhooks#signature-verification)
  system (`X-Webhook-Id` / `X-Webhook-Signature`, no `Mnemom-` prefix) — don't share verification
  code between the two without checking the header names.
</Note>

### Verify the signature

The HMAC-SHA256 signature is computed over the string `<timestamp>.<raw-body>`:

```python theme={null}
import hmac, hashlib

def verify_mnemom_webhook(body: str, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t = parts["t"]
    expected = hmac.new(secret.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

### List active subscriptions

```bash theme={null}
curl https://api.mnemom.ai/v1/agents/smolt-e2ca60ef/notifications \
  -H "Authorization: Bearer $MNEMOM_API_KEY"
```

Returns each subscription's `subscription_id`, `channel` (`webhook` or `sse`), `consumer_id`,
`webhook_url`, and `last_sent_log_index` — never the secret.

### Idempotent delivery

Each subscription tracks `last_sent_log_index`. If the same log entry is re-processed (e.g. a
reconciler closes a gap that the normal compose path also closed), delivery is skipped for any
subscription whose `last_sent_log_index` is already at or past that entry.

### Unsubscribe

```bash theme={null}
curl -X DELETE https://api.mnemom.ai/v1/agents/smolt-e2ca60ef/notifications/sub-3e8f... \
  -H "Authorization: Bearer $MNEMOM_API_KEY"
```

Returns 204 on success, 404 if the subscription is already gone (idempotent).

## Webhook URL constraints

* `https://` only — `http://` URLs are rejected.
* No loopback, RFC 1918, link-local, multicast, or reserved address space (IPv4 or IPv6).
* No `.local` / `.internal` hostnames.

These defend against SSRF on outbound delivery. For local development, tunnel with
[ngrok](https://ngrok.com) or [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/).

## SSE vs webhook — which to use

| Use case                                                        | Channel                     |
| --------------------------------------------------------------- | --------------------------- |
| Backend service that can keep an HTTP connection open           | **SSE**                     |
| Serverless / Lambda / Cloud Functions that prefer push-on-event | **Webhook**                 |
| Browser / dashboard UI consuming card changes                   | **SSE** (via `EventSource`) |
| Compliance / SIEM / archive that wants a durable POST trail     | **Webhook**                 |

Both surfaces deliver from the same event source (the transparency log) — subscribing to both
is fine.

## See also

* [Webhook Notifications](/guides/webhooks) — the separate, org-level webhook system for
  integrity, Safe House, and billing events.
* [A2A AgentCard export](/concepts/a2a-export) — the pull-side counterpart.
* [Transparency log](/concepts/transparency-log) — the event source.
* [AAP attestation tokens](/concepts/aap-attestation) — the `attestation_jws` field.
