Webhooks
Receive signed HTTP notifications when memories are added, updated, deleted or consolidated — and verify, de-duplicate and retry them correctly.
Webhooks notify your service when memory in a workspace changes, so you can sync a search index, audit changes or trigger downstream work without polling.
Events
| Event | Fires when |
|---|---|
add | A memory was appended. |
update | A memory's content was updated. |
delete | A memory was deleted. |
categorize | A run was consolidated. |
test | You pressed Test delivery in the console. memoryId is null. |
A subscription chooses any subset of add, update, delete and categorize. Internal derivations — facts promoted during consolidation, for example — do not emit one event each; consolidation emits a single categorize event per run.
Managing subscriptions
Subscriptions are per workspace and are managed in the web console under Webhooks:
- Create — give it a name, an HTTPS URL and the events. The signing secret (
whsec_…) is shown once. - Rotate secret — issues a new secret, shown once. For 24 hours afterwards every delivery is signed with both the new and the previous secret.
- Disable / enable — disabling pauses queued deliveries (they wait and are re-offered); enabling resumes them. Deleting a subscription discards its queued deliveries.
- Deliveries — every attempt with its status, HTTP code, response snippet and next retry.
- Requeue dead — re-queues deliveries that exhausted their retries (up to 100 per action), with the attempt counter reset.
The target URL must be HTTPS and resolve only to public IP addresses; loopback, private, link-local, CGNAT and cloud-metadata addresses are rejected. Creating webhooks requires a plan that includes them.
Delivery format
Each delivery is an HTTP POST with a JSON body and three headers:
| Header | Contents |
|---|---|
X-AgentMemory-Signature | t=<unix-seconds>,v1=<hex>[,v1=<hex>] — see verification. |
X-AgentMemory-Event | The event type. |
X-AgentMemory-Delivery | The delivery ID (GUID). The same across retries of one delivery. |
{
"version": "1",
"event": "add",
"eventId": "0d9c2a4e-7b31-4f8a-9c65-2e1d4b8a6f30",
"memoryId": "8b0c1c0e-6c7d-4a51-9d2e-1f3a5b7c9d0e",
"tenantId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"workspaceId": "a1b2c3d4-e5f6-4a51-9d2e-0e02b2c3d479",
"applicationId": null,
"summary": "…",
"occurredAt": "2026-08-31T12:34:56.789+00:00"
}| Field | Notes |
|---|---|
version | Payload schema version. Branch on it; it changes only if the shape changes incompatibly. New fields may be added without a version bump. |
event | Same as the X-AgentMemory-Event header. |
eventId | Unique per source event and shared by every subscription's delivery of it — use it to de-duplicate across several endpoints. |
memoryId | The memory concerned; null for test and categorize. |
applicationId | The application scope of the change; null for workspace-wide changes. |
summary | A short, redacted description. The memory content itself is never sent — fetch it with GET /v1/memory/{id} if you need it. |
occurredAt | When the change happened. |
Verifying signatures
The signature is HMAC-SHA256(secret, "{t}.{rawBody}"), hex-encoded in lowercase, where t is the timestamp from the header. Use the entire secret string, including the whsec_ prefix, as the HMAC key.
Read the raw request body bytes, before any JSON parsing.
Parse t and every v1 entry from X-AgentMemory-Signature. There is one v1 normally and two during the 24 hours after a secret rotation (new secret first).
Compute the expected signature and compare it with each v1 using a constant-time comparison. Accept if any matches.
Reject the delivery if t is more than 300 seconds away from your clock. The timestamp is inside the MAC, so it cannot be forged.
Skip deliveries whose X-AgentMemory-Delivery ID you have already processed, and answer 2xx quickly — the sender times out after 10 seconds. Do heavy work asynchronously.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRET = process.env.AGENTMEMORY_WEBHOOK_SECRET // whsec_…
const TOLERANCE_SECONDS = 300
const seen = new Set() // use Redis or a database with a 24h TTL in production
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-AgentMemory-Signature') ?? ''
const match = /^t=(\d+)((?:,v1=[0-9a-f]{64})+)$/.exec(header)
if (!match) return res.status(400).end()
const t = match[1]
const signatures = [...match[2].matchAll(/v1=([0-9a-f]{64})/g)].map(m => m[1])
if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return res.status(400).end()
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body.toString('utf8')}`).digest('hex')
if (!signatures.some(s => crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(s))))
return res.status(401).end()
const deliveryId = req.get('X-AgentMemory-Delivery')
if (seen.has(deliveryId)) return res.status(200).end()
seen.add(deliveryId)
const event = JSON.parse(req.body)
if (event.version !== '1') return res.status(200).end() // unknown schema: acknowledge and log
// handle event.event / event.memoryId
res.status(200).end()
})Retries
Any response other than 2xx — or a timeout or connection error — is a failed attempt. Deliveries are retried with backoff of roughly 1 minute, 5 minutes, 30 minutes and 2 hours (each delay varies by ±20%). After 5 attempts a delivery is marked dead and is not retried automatically; requeue it from the console once your endpoint is fixed.
Retries re-send the identical body with the same X-AgentMemory-Delivery ID and a fresh signature timestamp, so de-duplicating on the delivery ID (and on eventId across endpoints) makes processing exactly-once from your side.
The sender never follows redirects — answer the final URL directly — and connects only to the IP address it validated when resolving your hostname.
Self-hosting settings
| Key | Default | Effect |
|---|---|---|
Webhooks:MaxAttempts | 5 | Attempts before a delivery is marked dead. |
Webhooks:TimeoutSeconds | 10 | Per-attempt timeout. |
Webhooks:DispatchIntervalSeconds | 5 | How often the dispatcher looks for due deliveries. |
Webhooks:SecretRotationWindowHours | 24 | How long the previous secret keeps signing after a rotation. |
retention.webhookDeliveryDays (workspace setting) | 30 | How long delivery attempts are kept. |
Security
The controls that protect memory in Agent Memory — credentials, isolation, least privilege, content screening, secrets handling, erasure and webhook delivery.
Workspace settings
Tune retrieval, consolidation, screening, retention, learning and promotion per workspace or per organization, and how setting values are resolved.