Skip to main content
Ingest is asynchronous: you send conversation turns and the server extracts memories in the background. Normally you’d poll 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.completed and memory.learning.failed (see Events).
  • Signed. Every request carries an X-Webhook-Signature HMAC so you can verify it really came from XTrace (see Verifying the signature).
  • Correlated by your own ids. The payload echoes back the conv_id and user_id you 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
The full secret is returned only here — on first create, or when you rotate it. Store it now. Every later GET returns it masked (whsec_••••66b0). You need the full value to verify signatures.
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

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_id are the exact values you passed to POST /v1/memories — use them to correlate the delivery back to the originating conversation.
  • memories are thin refs ({ id, type }). Fetch the full row with GET /v1/memories/{id} when you need the content.

Verifying the signature

Every delivery includes:
The signature is 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.
Verify against the raw body, not a re-serialized object. Re-encoding the JSON can reorder keys or change whitespace and the signature won’t match.
Node / Express
Python / Flask
Respond with any 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-2xx or a network error, XTrace retries a few times with a short backoff, then gives up. A 4xx (other than 408/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.
Deliveries are not strictly ordered — use 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-Id headers used to manage the webhook
  • API ReferencePUT / GET / DELETE /v1/webhooks request and response schemas