Events
Webhooks
Get a signed POST the moment a memory is created, a fact is superseded, or an end user is about to hit a quota, so your product reacts in real time instead of polling.
Register an endpoint and Korely delivers an HTTP POST for the events
you subscribe to. Each delivery is signed (HMAC-SHA256, Standard Webhooks scheme),
retried with exponential backoff, and carries a stable id so you can dedupe.
Webhooks are best-effort and fully decoupled, they never block or slow down the
add /
search calls that trigger them.
Why fact.invalidated is the one that matters. When a new
memory contradicts something Korely already knew, the old typed fact is
superseded (invalidated, never silently overwritten) and you get pushed
the event, so any cache, search index, or downstream prompt you built stays honest.
A plain vector store has no typed facts to invalidate, so it has nothing to push.
This is the moat, delivered as an event.
Events
| Event | Fires when | Data fields |
|---|---|---|
memory.created | A memory is added, via POST /v1/memories or a batch import. | id, user_id, agent_id |
fact.invalidated | A typed fact is superseded because a newer memory contradicted it. | fact_id, invalidated_by, at |
quota.warning | A monthly write quota crosses 80% (fired once at the crossing). | used, limit, percent |
Subscribe to specific events, or use * to receive all of them. The body
of every delivery is a JSON envelope: a top-level event string and a
data object whose shape depends on the event.
Payloads
{ "event": "memory.created", "data": { "id": "mem_8f2c1a", "user_id": "u_42", "agent_id": "support-bot" }}Register an endpoint
Webhook endpoints are managed from your Korely dashboard. Add the URL that should
receive deliveries (must be http:// or https://), pick the
events (or *), and Korely returns a signing secret of the form
whsec_.... Store that secret, you need it to verify every delivery, and
it is shown so you can rotate it if it ever leaks.
You can register more than one endpoint (for example, one for staging and one for
production), and each gets its own secret. Use the dashboard's Send test
action to enqueue a sample memory.created delivery and confirm your receiver
and signature check work before you depend on real traffic.
Verify the signature
Every delivery carries three headers. Always verify before you trust the body:
| Header | Value |
|---|---|
webhook-id | Unique id for this delivery. Use it to dedupe replays. |
webhook-timestamp | Unix seconds when Korely signed the delivery. |
webhook-signature | The signature, formatted v1,<base64>. |
The signature is an HMAC-SHA256 over the exact string
{webhook-id}.{webhook-timestamp}.{raw-body}, keyed by your
whsec_... secret (used verbatim as the UTF-8 key, do not
base64-decode it), base64-encoded and prefixed with v1,. Compute it over the
raw request body, byte-for-byte, before any JSON parsing or
re-serialization, re-encoding the JSON will change the bytes and break the check.
import hmac, hashlib, base64
WEBHOOK_SECRET = "whsec_..." # from your dashboard
def verify(headers, raw_body: bytes) -> bool: msg_id = headers["webhook-id"] ts = headers["webhook-timestamp"] sig = headers["webhook-signature"] # "v1,<base64>"
signed_content = f"{msg_id}.{ts}.{raw_body.decode()}".encode("utf-8") digest = hmac.new(WEBHOOK_SECRET.encode("utf-8"), signed_content, hashlib.sha256).digest() expected = "v1," + base64.b64encode(digest).decode("ascii")
# constant-time compare return hmac.compare_digest(expected, sig)
As an extra guard against replay, reject deliveries whose webhook-timestamp
is more than a few minutes old.
Delivery & retries
- Success is any 2xx. Return
200,299quickly. Any other status, a timeout, or a connection error counts as a failure and is retried. - At-least-once. A delivery can arrive more than once (after a retry or a network hiccup). Dedupe on
webhook-idand make your handler idempotent. - Exponential backoff. Failed deliveries are retried with a growing delay (doubling, capped at one hour) until the attempt limit is reached, then marked failed.
- No strict ordering. Retries mean a later event can arrive before an earlier one. Don't assume order, use the data in each event, or re-read from the API if you need the current state.
- Decoupled by design. Events are queued to an outbox and delivered by a background worker, so a slow or down endpoint never affects the
add/searchrequest that produced the event. - Respond fast, work async. Acknowledge with a 2xx immediately and do heavy processing in your own queue. The delivery times out after ~10s of read time.
Notes
- Keep the secret server-side. Anyone with the
whsec_secret can forge a valid signature. Never ship it to a browser or mobile client. - One secret per endpoint. Register separate endpoints for staging and production so a leaked staging secret can't sign production traffic.
- Verify, then parse. Compute the signature over the raw body first; only parse the JSON once the signature matches.
- Disabled on repeated failure. Endpoints that keep failing accumulate a failure count and stop receiving deliveries until you fix and re-enable them.
Related
- Add a memory, the call that fires
memory.created - Temporal facts, how supersession produces
fact.invalidated - Update a memory, correcting a memory and the invalidation it triggers
- Data & governance, quotas, residency, and the audit trail