Webhooks

Webhooks overview and events

Tell your own tools the moment a document base is ready, a sync run finishes or a document fails, instead of polling for it. Signed, retried, and recorded in your audit log.

A pipeline that loads documents and then waits has two options, and polling is the worse one: it burns your CI minutes and our request budget to learn something we knew the instant it happened. Webhooks are the other option.

What you get

You give us an HTTPS endpoint. When a run finishes, a base starts answering, or a document cannot be read, we POST a signed JSON body to it, retry it if your endpoint is down, and record every attempt in your audit log. Three properties hold the design together:

The events

Three events, a closed set. An unknown name in a subscription is refused at the call that made it rather than surfacing months later as "why does my endpoint never fire".

EventFires whenKey fields
sync_run.completedAn ingestion run reaches a terminal state, whatever fed it: an upload, a push, an S3 sync, a Drive or SharePoint sync sync_run_id, corpus_id, source, state, counters
corpus.readyA document base finishes onboarding and is answering queriescorpus_id, name, job_id, documents_ready
document.failedOne document could not be read or built, with the reasoncorpus_id, document_id, path, lifecycle, error

Subscribe with an empty events list to get all three, which is what someone who just wants to be told things should do rather than maintaining an enumeration.

What a payload looks like

Every delivery has the same envelope: the event name, when we sent it, and the event's own data.

{
  "event": "sync_run.completed",
  "sent_at": "2026-09-13T10:06:39.411820Z",
  "data": {
    "sync_run_id": "r_5d2c...",
    "corpus_id": "c_7a1f...",
    "source": "s3",
    "source_id": "s_3e8b...",
    "state": "succeeded",
    "counters": {"added": 12, "updated": 3, "unchanged": 480, "removed": 0, "failed": 1},
    "documents_total": 15,
    "documents_done": 15,
    "error": null
  }
}
{
  "event": "corpus.ready",
  "sent_at": "2026-09-13T10:11:42.004913Z",
  "data": {
    "corpus_id": "c_7a1f...",
    "name": "Support handbook",
    "job_id": "j_4b8e...",
    "documents_ready": 495
  }
}
{
  "event": "document.failed",
  "sent_at": "2026-09-13T10:09:02.771004Z",
  "data": {
    "corpus_id": "c_7a1f...",
    "document_id": "d_92f...",
    "path": "handbook/scanned-appendix.pdf",
    "lifecycle": "failed",
    "error": "encrypted PDF: no readable text"
  }
}

source says which route the documents came in by: upload (a manifest commit, which is what the Documents tab and engram push both do), push (the API's single-document documents/upsert), s3, google_drive, sharepoint or backfill. The first two are worth reading twice, because the names are not the ones you would guess; the full table is on Keeping a source in sync.

Every timestamp we send is UTC and ends in Z, in a payload's sent_at and in the created_at and last_delivery_at on the listing alike.

A run is succeeded even when a counter says one document failed: a run fails only when nothing it tried worked. So watch counters.failed and the document.failed events for per-file problems, and state for whether the run itself got anywhere.

The headers on a delivery

HeaderValue
X-Engram-Signaturesha256=<hex>, an HMAC-SHA256 of the raw body bytes with your endpoint's secret
X-Engram-EventThe event name, so you can route before parsing
Content-Typeapplication/json
User-Agentengram-webhooks/1

Verify the signature before you trust X-Engram-Event or anything in the body. The header is convenience; the signature is the proof.

Which plan turns this on

Subscribing a new endpoint comes with the Pro plan and every tier above it. Exactly what each tier includes is on Plans and allowances, which is the single place those numbers live.

Only subscribing is gated. Listing, removing, pausing and rotating a secret stay open on every tier, and deliveries to endpoints that already exist keep running whatever plan the workspace moves to, because a pipeline going quiet without warning is a worse failure than an unenforced promise.

Next

Set one up in one call: Subscribe an endpoint. Then verify a signature and read delivery and retries before you put a receiver in production.