LanguageENES
Integrations

The sync webhook

One signed endpoint per knowledge base. What it is, when to reach for it, and every field, header and error.

Updated 2026-09-06View .md

What a webhook is

A webhook is a URL you give to another system so it can call you when something happens. The alternative is polling -- asking "anything new?" every few minutes -- which is slower and mostly wasted. A webhook is one request, at the moment the thing actually changed.

What this one does

Every knowledge base can have one signed endpoint. Post a document to it and the document is added, updated or removed. That is the whole contract.

It exists for content that lives somewhere we do not have a connector for: an internal CMS, a Notion database routed through Zapier, a Google Sheet in Make, a script that runs after your build. If you already use WordPress or Webflow, use those connectors instead -- they know what changed and handle deletions for you.

When to use it, and when not

You want to
Use
Sync a WordPress or Webflow site
The connector for it
Crawl a public website
The website crawler
Push from your own backend, in your own code
The REST API with an API key
Let Zapier, Make or n8n push, with no code
This webhook

The difference between this and the REST API is only how you prove who you are. The REST API takes an API key in a header, which is right when you control the code. This takes a signature, which is right when a no-code tool is making the request: the URL carries a public token, so the tool needs no credential store, and the signature proves the body was not tampered with.

Turning it on

Integrations, then Generic sync webhook. Pick a knowledge base and press Enable. You get two things:

  • The endpoint, https://app.welcomeai.dev/api/v1/kb/{kbId}/sync?t=whsec_…. The token in the query string identifies which source is being fed. It is not a secret on its own -- it names, it does not authorise.
  • The signing secret, shown once. Store it where your automation keeps credentials.

Rotating the secret invalidates the previous one immediately. There is no overlap window, so change it in your automation first, or accept a few failed requests.

Authentication

Sign the raw body with HMAC-SHA256 and send the hex digest.

X-WelcomeAI-Timestamp: 1757155200
X-WelcomeAI-Signature: sha256=<hex digest of "1757155200.<raw body>">

The signed string is the timestamp, a literal dot, and the body exactly as it goes over the wire. Sign the bytes you send -- re-serialising the JSON first changes the whitespace and the signature will not match.

A timestamp older or newer than five minutes is refused, so a captured request cannot be replayed later. If your tool cannot add a header, omit the timestamp and sign the body alone; that still works and is what most Zapier setups do, but it gives up the replay protection.

The request

POST, Content-Type: application/json. One document, or a batch:

{
  "external_id": "faq-refunds",
  "title": "Refunds",
  "url": "https://acme.com/help/refunds",
  "content_md": "# Refunds\n\nWithin 14 days, email billing@acme.com.",
  "metadata": { "category": "billing" }
}
{
  "items": [
    { "external_id": "faq-refunds", "title": "Refunds", "content_md": "…" },
    { "external_id": "faq-shipping", "title": "Shipping", "content_md": "…" },
    { "external_id": "faq-old", "action": "delete" }
  ]
}

Fields

Field
Required
What it is
external_id
yes
Your identifier for this document, up to 500 characters. Posting the same one again updates it rather than making a second copy. This is the single most important field to get right.
content_md or content_html
yes, unless deleting
The content. Markdown is preferred; HTML is converted. Up to 2 MB.
title
no
What a citation shows. Worth setting -- "Untitled" in a citation makes a right answer look wrong.
url
no
Where a reader can see the original. Becomes the citation's link.
excerpt
no
A short summary, stored alongside.
metadata
no
Any JSON object of your own. Kept with the document.
content_hash
no
If you send one, an unchanged hash skips re-indexing entirely. Cheap and worth doing on a large sync.
action
no
upsert (the default) or delete.

A batch is at most 25 items and the whole body at most 4 MB.

Deleting

Send {"external_id": "…", "action": "delete"}. The document, its chunks and its embeddings go. Deleting something that was never there is not an error -- it reports missing and moves on, so a replayed delete is safe.

The response

200, always with a per-item breakdown. A partial failure is a 200 with ok: false, not a 500 -- one bad item in a batch of twenty-five must not make you resend the other twenty-four.

{
  "ok": true,
  "accepted": 2,
  "failed": 0,
  "results": [
    { "external_id": "faq-refunds",  "status": "created" },
    { "external_id": "faq-shipping", "status": "unchanged" }
  ]
}

status is one of created, updated, unchanged, deleted, missing or failed. A failed item carries an error describing why.

`unchanged` is the one to watch. It means the content hash matched what we already had, so nothing was re-indexed. On a nightly full sync most items should say unchanged; if they all say updated, something in your pipeline is rewriting the content each run -- a regenerated timestamp in the body, usually -- and you are paying to embed the same text again.

Errors

Status
Code
What happened
400
invalid_json
The body is not JSON
400
invalid_input
A field is missing or the wrong shape. The message names the field
401
invalid_signature
The signature, the secret or the timestamp is wrong
404
not_found
No webhook is enabled for that knowledge base, or the token does not match
409
source_paused
The source is paused. Resume it in Connected sources
413
payload_too_large
The body is over 4 MB

401 is the one people hit. In order of likelihood: the body was re-serialised before signing; the timestamp was included in the header but not in the signed string, or the other way round; the secret was rotated; the clock is off by more than five minutes.

Retries

We do not retry -- you are the caller. Retry a 5xx or a network failure with a backoff, and do not retry a 4xx: nothing about the request will succeed on a second attempt.

Retries are safe. external_id makes every write idempotent, so sending the same document twice produces one document, and the second call reports unchanged.

Limits

There is no per-endpoint rate limit today. Ingestion is bounded by your plan's document and storage allowances, and a batch is capped at 25 items and 4 MB. Be reasonable on a first import: a few requests a second, not a few hundred.

Keeping it safe

  • Treat the secret like a password. It is the only thing standing between your knowledge base and anyone who has the URL, and the URL travels through whichever tool you pasted it into.
  • Rotate it when someone leaves, or when an automation is decommissioned.
  • One knowledge base, one webhook. If two systems feed the same knowledge base, give them separate knowledge bases or separate external_id prefixes, so you can tell later which one sent what.
  • Do not send anything a visitor should not read. Everything you post is content the assistant may quote to whoever asks.
  • Every request is recorded in the Sync log with what it found and what it changed, which is where to look when the content is not what you expected.

Put your knowledge to work today

Free for 14 days. No card, and nothing to install on your site.