TL;DR / Quick Summary: Polling for document updates wastes bandwidth and leaves your assistant quoting last night's prices. An API-first, event-driven design flips the relationship: your CMS pushes a signed document.updated event the moment a page changes, and CustomerGPT re-chunks, re-embeds and upserts only that page — usually in under two seconds. This guide covers the event contract, delivery reliability, signature verification, and what to monitor once it is live.

Support automation fails in a very specific, very embarrassing way: the docs are right and the bot is wrong. Someone updates the refund window from 14 days to 30, publishes it, and for the next several hours the assistant keeps confidently telling customers the old number — with a citation, which makes it worse, because the citation looks authoritative.
That gap between “published” and “retrievable” is an architecture choice, not a fact of life. Below is how to close it with webhook event triggers.
Why Polling Breaks Down at Documentation Scale
Scheduled crawling is the default because it is easy to reason about: every night, fetch everything, re-embed everything. It works fine at 50 pages. At 5,000 pages, three things go wrong at once.
- Freshness is bounded by the interval, not by the edit. A nightly crawl means the worst-case staleness is 24 hours, and the average is 12.
- Cost scales with the corpus, not with the change. Re-embedding 5,000 unchanged pages to catch the one that moved is the bulk of the bill.
- Crawl windows collide with traffic. The re-index has to run somewhere, and a full pass long enough to matter is a full pass long enough to be noticed.
- Deletions are invisible until the next pass. A retired page keeps getting cited until the crawler notices it 404s.
Scheduled polling
- Staleness bounded by the crawl interval (hours to a day)
- Re-embeds the whole corpus to catch a single diff
- Deleted pages stay citable until the next pass
- Cost grows with corpus size, every single night
Webhook event triggers
- Staleness bounded by embedding latency (seconds)
- Re-embeds exactly the page that changed
- document.deleted removes chunks immediately
- Cost grows with edit volume, which is far smaller
The Event Contract: What a Sync Webhook Should Carry
Before writing any code, agree on the payload. A sync event needs to answer four questions: what happened, to which document, when, and is this the message you think it is.
| Field | Purpose | Required |
|---|---|---|
event | document.updated, document.deleted, collection.reindexed | Yes |
eventId | Stable id used as the idempotency key on retries | Yes |
chatbotId | Which assistant's index to update | Yes |
url or sourceId | The document that changed | Yes |
occurredAt | Publish timestamp; lets the consumer drop out-of-order events | Recommended |
contentHash | Skips re-embedding when the body did not actually change | Recommended |
Sending Your First Sync Event
On the Scale and Enterprise plans the push route is a single POST. Fire it from whatever already knows a page was published — a CMS post-publish hook, a CI job on your docs repo, or a database trigger.
// Triggering an automatic chatbot sync from an external system
await fetch('https://api.customergpt.ai/v1/webhooks/sync', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.CUSTOMERGPT_TOKEN,
'Content-Type': 'application/json',
'Idempotency-Key': event.id
},
body: JSON.stringify({
chatbotId: 'bot_xyz',
event: 'document.updated',
eventId: event.id,
url: 'https://mysite.com/docs/api-guide',
occurredAt: new Date().toISOString(),
contentHash: sha256(renderedHtml)
})
});- 1Event acceptedThe endpoint validates the signature and returns 202 immediately — work happens off the request path.
- 2Fetch and diffThe page is re-fetched and compared against the stored content hash. Identical body, no work.
- 3Re-chunkContent is split along its heading hierarchy so a section stays in one retrievable piece.
- 4Upsert vectorsNew embeddings replace the old chunks in a single transaction — no window where the page is missing.
Making Delivery Reliable: Retries, Idempotency and Ordering
Webhooks are an at-least-once medium. Networks drop, receivers restart, and a well-behaved producer retries. Design for duplicates rather than hoping they do not arrive.
- Send a stable event id. Reuse the same id across every retry of the same logical event so the consumer can discard repeats.
- Retry with exponential backoff and jitter. A flat one-second retry loop from 40 publishing services is a self-inflicted thundering herd.
- Treat any 2xx as delivered. Do not parse the body for success; the status code is the contract.
- Drop stale events with
occurredAt. If a retry from 14:02 arrives after the fresh event from 14:09, the older one must lose. - Dead-letter after N attempts. Persist what you could not deliver, alert on the queue depth, and replay it after the fix.
// Producer-side retry with backoff and a dead-letter fallback
async function deliver(payload, attempt = 0) {
const res = await post(payload);
if (res.ok) return;
if (attempt >= 5) return deadLetter.push(payload);
const backoff = Math.min(2 ** attempt * 1000, 30_000);
const jitter = Math.random() * 400;
setTimeout(() => deliver(payload, attempt + 1), backoff + jitter);
}Securing the Endpoint
A sync endpoint that accepts anonymous POSTs is a content-poisoning vector: anyone who guesses a chatbot id can point your assistant at a page you do not control. Two controls close that off.
- Bearer token in the Authorization header — scoped to a single chatbot, rotatable, never committed to the docs repo that triggers it.
- Signature over the raw body — an HMAC-SHA256 of the exact bytes plus a timestamp, so a replayed or edited payload fails verification.
// Consumer-side verification — compare in constant time, and bound the clock skew
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret) {
const [ts, signature] = header.split(',');
if (Math.abs(Date.now() - Number(ts)) > 5 * 60_000) return false;
const expected = createHmac('sha256', secret)
.update(ts + '.' + rawBody)
.digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Note that the signature is computed over the raw body. Verifying a re-serialized object is a classic way to ship a check that passes in tests and fails on the one payload where key order differs.
What to Monitor After You Ship
Event-driven sync fails quietly — the pipeline does not crash, it just stops receiving events, and nobody notices until a customer quotes an old price back at you. Four signals catch that.
Rollout Plan for an Existing Docs Site
You do not have to cut over in one step. This sequence keeps the assistant answerable the entire time.
From nightly crawl to event-driven sync
- Do a full crawl once so the index has a complete baseline
- Add the publish hook in your CMS behind a feature flag
- Fire events for one low-traffic section first and watch the logs
- Compare event count against publish count for a full week
- Drop the nightly crawl to weekly reconciliation
- Alert on dead-letter depth and on publish-to-retrievable p95
- Document how to replay the dead-letter queue before you need to
The result is a support assistant whose answers move at the same speed as your documentation. Your writers publish; the index follows within seconds; nobody has to remember to press a re-sync button.