GET /v1/memories/jobs/{job_id} to find out when it’s done. Webhooks flip that around — register one URL and XTrace POSTs to it the moment learning completes or fails for a conversation.
The motivating case is a “memory is ready” gate: an app that wants to tell its user “we’ve learned from that conversation” before letting them continue. Polling makes that laggy and chatty; a webhook makes it a push.
The model
- One endpoint per org. You register a single subscriber URL — there are no per-conversation subscriptions to manage. Every terminal ingest job for the org fires to that one URL.
- Two events.
memory.learning.completedandmemory.learning.failed(see Events). - Signed. Every request carries an
X-Webhook-SignatureHMAC so you can verify it really came from XTrace (see Verifying the signature). - Correlated by your own ids. The payload echoes back the
conv_idanduser_idyou sent at ingest, so you can match a delivery to the originating conversation. - Best-effort. Delivery is at-most-once with a few quick retries. Treat the job-polling endpoint as your fallback (see Delivery semantics).
1. Register your endpoint
PUT /v1/webhooks with the URL XTrace should call. It must be HTTPS and resolve to a public address.
Response
PUT is create-or-replace: call it again to change the URL or events. By default it keeps the existing secret (so an edit doesn’t break verification); pass ?rotate_secret=true to mint a fresh one.
To subscribe to only one event, pass events:
Events
| Event | When it fires | Carries |
|---|---|---|
memory.learning.completed | The ingest job succeeded. Fires even when nothing was extracted (memories: []). | memories, memories_updated |
memory.learning.failed | Extraction errored. | error |
completed is a “this conversation has been processed” signal, not a “we found something” one. A conversation that yields no new memory still fires completed with an empty memories array — so an app gating on “memory is ready” always gets unblocked. Don’t treat memories: [] as an error.Payload
memory.learning.completed
memory.learning.failed
conv_id/user_idare the exact values you passed toPOST /v1/memories— use them to correlate the delivery back to the originating conversation.memoriesare thin refs ({ id, type }). Fetch the full row withGET /v1/memories/{id}when you need the content.
Verifying the signature
Every delivery includes:sha256= + an HMAC-SHA256 of the raw request body bytes, keyed by your webhook secret. Recompute it over the bytes you received (before any JSON parsing) and constant-time compare.
Node / Express
Python / Flask
2xx to acknowledge. A non-2xx (or a timeout) is treated as a failed delivery and retried (see below).
Delivery semantics
Delivery is best-effort, at-most-once:- On a non-
2xxor a network error, XTrace retries a few times with a short backoff, then gives up. A4xx(other than408/429) is treated as a permanent rejection and not retried. - Deliveries are independent of the ingest job. A dropped webhook never changes the job’s outcome — the memories are already stored.
- Your fallback is the job endpoint. If a delivery is ever lost,
GET /v1/memories/jobs/{job_id}still returns the terminal result. For anything you can’t afford to miss, reconcile against it rather than relying on the webhook alone.
timestamp if you need to reason about ordering.
Managing the webhook
Best practices
Always verify the signature. Your endpoint is public; the signature is what proves a request is from XTrace and wasn’t tampered with. Reject anything that doesn’t verify. Verify against the raw body. Capture the bytes before parsing (express.raw, request.get_data()), or you’ll re-serialize and break the comparison.
Treat completed as “processed,” not “found.” An empty memories array is a normal success — it means the conversation produced no new memory, and your “ready” gate should still release.
Acknowledge fast, work async. Return 2xx quickly and do any heavy work after. A slow handler looks like a failed delivery and gets retried.
Make your handler idempotent. Retries (and at-most-once semantics) mean you should key on job_id so a re-delivery doesn’t double-process.
Reconcile critical flows against the job endpoint. Webhooks are best-effort; if a step truly cannot be missed, fall back to GET /v1/memories/jobs/{job_id}.
Keep the secret server-side. It’s an org-wide signing key — never ship it to a browser. Rotate it (?rotate_secret=true) if it leaks.
See also
- Ingesting memories — the async job lifecycle webhooks notify on
- Authentication — the
Authorization+X-Org-Idheaders used to manage the webhook - API Reference —
PUT/GET/DELETE /v1/webhooksrequest and response schemas