Kognita
All posts
Tutorial2026-10-15·8 min read

Webhook-driven ingestion: keeping your KB in sync with Notion in real time

Polling for changes is slow and wasteful. Webhooks let Notion tell you the moment a page changes, so your knowledge base reflects edits in seconds. Here's how to build the pipeline.

The gap between "someone edited a Notion page" and "the knowledge base reflects the edit" is the difference between a KB people trust and one that quietly serves stale answers. Polling closes that gap slowly — you re-crawl on a schedule and hope you didn't just miss a change. Webhooks close it in real time: Notion notifies you the instant a page changes, you re-ingest just that page, and the next query sees the update seconds later. Here's how to build a webhook-driven sync pipeline, and the details that make it robust rather than flaky.

webhook-driven ingestion with Notion

Why webhooks beat polling

Polling means asking "what changed?" on a fixed interval. It has two failure modes baked in: it's late (changes wait until the next poll) and it's wasteful (most polls find nothing, but you pay for them anyway). Tighten the interval for freshness and you multiply the wasted work; loosen it to save work and you serve staler content. There's no good point on that trade-off.

Webhooks invert it. Instead of you asking, the source tells you — Notion sends an event the moment content changes. You do work only when there's real work to do, and freshness is near-instant. For a knowledge base whose whole value depends on being current, this is the right architecture.

The pipeline

A robust webhook-driven ingestion flow has five stages:

1. Receive and verify the webhook. Notion sends an HTTP request to your endpoint when a subscribed change happens. First thing you do: verify the signature. Webhook endpoints are public URLs, so you must confirm the request genuinely came from Notion (using the webhook signing secret) before trusting it. An unverified webhook endpoint is an open door for anyone to inject fake change events.

def handle_webhook(request):
    if not verify_signature(request, NOTION_WEBHOOK_SIGNING_SECRET):
        return 401
    event = request.json()
    enqueue_sync_job(event["page_id"])   # don't process inline — enqueue
    return 200   # ack fast

2. Acknowledge fast, process async. Respond to the webhook quickly (a 200) and do the actual ingestion out of band. Don't chunk and embed inside the webhook handler — that's slow work that would make you time out and cause the source to retry. Instead, enqueue a job and return immediately. This is the same queue-backed pattern Kognita uses for all ingestion: the webhook just drops a job onto the queue; a worker does the heavy lifting.

3. Fetch and re-process the changed page. A worker picks up the job, fetches the current content of the changed page from Notion's API, and runs it through the pipeline: normalize the rich Notion structure, chunk, embed, store.

4. Upsert with deterministic IDs. Write the new vectors using stable, content-derived IDs (Kognita keys vectors as {content_id}_{chunk_index}), so re-ingesting a page overwrites its existing chunks in place rather than appending duplicates. This idempotency is what makes the whole thing safe to run repeatedly — webhooks fire on no-op saves and can be delivered more than once, so re-processing the same page must land you in the same state, not pile up copies.

5. Reconcile periodically. Webhooks are not perfectly reliable — deliveries fail, your endpoint has downtime, events get missed. So run a periodic reconciliation sweep as a safety net: compare the set of pages in Notion against what's indexed, re-ingest anything that drifted, and — critically — remove deletes. A page deleted in Notion should disappear from the KB, or the bot will keep citing content that no longer exists.

The details that make it robust

  • Verify signatures, always. This is the one you cannot skip. It's the boundary between "Notion told me this changed" and "someone on the internet told me this changed."
  • Dedupe / debounce. A rapid series of edits can fire many webhooks in seconds. Debounce so you re-ingest the settled page once, not ten times mid-edit.
  • Hash to skip no-ops. Webhooks fire even when a save didn't change content. Hash the fetched content and skip re-embedding if it matches what you already have — saves compute on spurious events.
  • Handle deletes explicitly. Update events are easy; deletes are the ones that quietly rot a KB. Make sure your event handling (and your reconciliation sweep) removes content that's gone from the source.
  • Combine webhooks with reconciliation. Webhooks for immediacy, reconciliation for completeness. Neither alone is enough — webhooks miss things, and polling-only is slow. Together they give you fast and reliable.

The result

With this pipeline, the loop closes in seconds: someone fixes a typo or updates a policy in Notion, a webhook fires, a job enqueues, a worker re-embeds just that page and upserts it, and the very next question that touches that content gets the updated answer — with the stale version already overwritten. No nightly batch, no waiting for the next poll, no manual re-sync.

The broader principle applies to any connected source, not just Notion: event-driven ingestion is how a knowledge base stays genuinely live. Sync from the source of truth via change events, process async through a queue, upsert idempotently, and back it with reconciliation to catch misses and deletes. That combination is what turns a knowledge base from a snapshot that decays into a real-time reflection of your content — which, for anything people rely on for current answers, is the whole point.