Webhooks and change feed
Push memory changes to external systems with signed webhooks, or pull them from a cursor-based change feed.
You can send memory changes to external systems in two ways:
| Method | When? |
|---|---|
| Webhook | Your system needs to know immediately when something changes (push). |
Change feed (changesets) | You want to ask periodically "what changed since I last read?" (pull). Suited to auditing and sync. |
Neither carries content: a webhook contains a short, masked summary, and the feed contains only policy fields. For details, read the memory by its ID.
Webhooks
Subscriptions are managed on the console's Webhooks page (your plan must include the webhook feature). A subscription consists of a name, an HTTPS URL and a list of events; when it is created, a secret (whsec_…) is shown once.
Events
| Event | When? |
|---|---|
add | A new memory was written. |
update | A memory's content was updated or changed during a merge. |
delete | A memory was deleted. |
categorize | A run went through consolidation. |
test | The "Test" button in the console (memoryId is empty). |
Memories produced by consolidation don't emit individual add events; you get a single categorize for the run. Memories forgotten because they expired don't emit events.
Delivery
Each delivery is a POST request:
POST /hooks/agent-memory HTTP/1.1
Content-Type: application/json
X-AgentMemory-Signature: t=1792315107,v1=5f2c…
X-AgentMemory-Event: add
X-AgentMemory-Delivery: 3b6e…
{"version":"1","event":"add","eventId":"…","memoryId":"…","tenantId":"…","workspaceId":"…",
"applicationId":null,"summary":"Invoice numbers use a year prefix …","occurredAt":"2026-10-18T09:18:36.88+00:00"}summaryis at most 256 characters, with personal data masked; the full content is not sent.eventIdis the same across deliveries of the same event to all subscriptions;X-AgentMemory-Deliveryis specific to the delivery chain and does not change across retries.- Return
2xxwithin 10 seconds; do heavy work in the background. - Failed deliveries are retried at increasing intervals (1 min, 5 min, 30 min, 2 h); after 5 attempts a delivery is considered "dead". You can requeue dead deliveries from the console. Delivery history is kept for 30 days.
- The target URL goes through a security check: redirects are not followed, and nothing is delivered to internal network addresses.
Verifying signatures
The signature is the HMAC-SHA256 digest (lowercase hex) of the string "{t}.{raw body}", keyed with the secret.
import hmac, hashlib, time
def verify(raw_body: bytes, signature_header: str, secrets: list[str], tolerance=300) -> bool:
parts = [p.split("=", 1) for p in signature_header.split(",")]
t = next(v for k, v in parts if k == "t")
candidates = [v for k, v in parts if k == "v1"]
if abs(time.time() - int(t)) > tolerance:
return False
message = f"{t}.".encode() + raw_body
for secret in secrets:
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
if any(hmac.compare_digest(expected, c) for c in candidates):
return True
return False- Verify the raw body, before parsing it as JSON.
- When you rotate the secret, every delivery carries two
v1signatures for 24 hours (new first, then old); accept both secrets during that window. - Skip duplicates by storing processed
X-AgentMemory-DeliveryIDs for a short time.
Change feed
curl -s "$API/v1/memory/changesets?workspaceId=$WS&limit=50" -H "Authorization: Bearer $KEY"{
"rows": [
{"seq":"8149","id":"…","memoryId":"…","action":"create","actor":"apikey:…","actorRole":"agent",
"oldValues":{},"newValues":{"type":"decision","visibility":"workspace"},"reason":null,
"createdAt":"2026-10-18T09:18:36.88+00:00"}
],
"nextCursor": "8151",
"hasMore": true
}| Parameter | Description |
|---|---|
action | create, update, delete, promote, share, policy_change |
memoryId | Changes to a single memory. |
since | Changes after this time. |
cursor | The nextCursor from the previous response. Store it and pass it on the next call to get only new changes. |
limit | 1–500 (default 50). |
Rows are ordered oldest to newest. A delete row also lists dependent memories flagged with cascade. Deletions caused by expiry and retention appear with the system:retention actor.
MCP: get_changesets(action?, memoryId?, cursor?, limit).