Meili CDC Docs

Webhook (HTTP push)

A webhook task has no database to read: your application POSTs its changes to /api/v1/ingest/{task id}, and the request returns once the documents are confirmed in Meilisearch.

Create the task

  1. Create a connector of type Webhook. It has no connection settings.
  2. Create a task on it. The table is only a name for the feed (it appears in logs). Set a primary key column in the mapping step if your rows do not have an id field.
  3. Optionally add a signing secret (see signed requests). Generate creates a random whsec_… value; copy it then, because it is never shown again.
  4. Start the task. Its Overview tab shows a Push endpoint card with the URL and, for signed tasks, a ready-to-run signed curl example.

The task id is in the URL, so give the task a readable id (for example products-push) when you create it.

Payload

The body is a single change, a JSON array of changes, or newline-delimited JSON (NDJSON), up to 64 MiB:

{"op": "upsert", "row": {"id": 1, "name": "Desk lamp", "price": 39.9}}
[
  {"op": "upsert", "row": {"id": 1, "name": "Desk lamp"}},
  {"op": "delete", "row": {"id": 2}},
  {"row": {"id": 3, "name": "Chair"}}
]
{"op":"upsert","row":{"id":4,"name":"Shelf"}}
{"op":"delete","row":{"id":5}}
FieldMeaning
opupsert (default; insert, update and replace are aliases), delete or truncate.
rowThe row as a JSON object. Required and non-empty for upserts and deletes; a delete needs at least the key columns. Integral numbers become integers, others floats.

Changes in one request are applied in order, and requests to one task are applied in the order they arrive. A truncate holds the task unless allowTruncate is on.

Responses

StatusMeaning
200 {"accepted":n,"position":"…"}Every change is durably in Meilisearch.
400Invalid payload (the error says why), or the task is not a webhook task.
401Not authenticated, or the signature is missing, wrong, or stale.
403Authenticated, but the role is below moderator.
503the task is not running; start it and retry.
504changes were not confirmed in time; retry (writes are idempotent) after 60 seconds.

Retry on 503, 504 and network errors: the same change applied twice gives the same document.

Authentication

How a push authenticates depends on whether the task has a signing secret (source.webhookSecret):

TaskWhat the request needs
No secretA bearer token or session with at least the moderator role. The static CDC_ADMIN_TOKEN works: Authorization: Bearer ….
Secret setA valid signature. No token is needed, and a token alone is refused with 401.

Anonymous callers get 401 both for unknown task ids and for tasks without a secret, so task ids cannot be probed.

# Task without a secret: a token with the moderator role or higher
curl -X POST "https://cdc.example.com/api/v1/ingest/products-push" \
  -H "Authorization: Bearer $CDC_ADMIN_TOKEN" -H 'Content-Type: application/json' \
  -d '{"op":"upsert","row":{"id":1,"name":"Desk lamp"}}'

Signed requests

With a signing secret (at least 16 characters), each request carries two headers:

X-CDC-Timestamp: 1767225600
X-CDC-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + raw body)>

The timestamp is in Unix seconds and must be within 5 minutes of the server clock. The HMAC covers the timestamp, a dot, and the exact bytes of the body, so sign what you send. A request is refused with 401 when it is unsigned (the request is not signed: send the X-CDC-Timestamp and X-CDC-Signature headers), signed with another secret or over a changed body (the signature does not match …), or too old or too far in the future (the signature timestamp is outside the accepted 5 minute window; check the sender's clock).

With curl and openssl

CDC=https://cdc.example.com
TASK=products-push
SECRET='whsec_…'                       # the task's signing secret
BODY='{"op":"upsert","row":{"id":1,"name":"Desk lamp"}}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST "$CDC/api/v1/ingest/$TASK" -H 'Content-Type: application/json' \
  -H "X-CDC-Timestamp: $TS" -H "X-CDC-Signature: sha256=$SIG" -d "$BODY"

From Node.js

import { createHmac } from 'node:crypto';

async function push(changes) {
  const body = JSON.stringify(changes);
  const ts = Math.floor(Date.now() / 1000).toString();
  const sig = createHmac('sha256', process.env.CDC_WEBHOOK_SECRET).update(`${ts}.${body}`).digest('hex');
  const res = await fetch('https://cdc.example.com/api/v1/ingest/products-push', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-CDC-Timestamp': ts, 'X-CDC-Signature': `sha256=${sig}` },
    body,
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json(); // {accepted, position}
}

Rotating the secret

The secret is encrypted at rest and returned masked (••••••). In the task's Edit page, Rotate generates a new one and Remove clears it (the API takes "" to remove it; sending the mask back keeps the stored one). The X-CDC-Signature header may hold several comma-separated signatures, and any one that matches is accepted, so a sender can sign with both the old and the new secret while you switch.

Positions and limits

Positions are a per-task sequence number (0, 1, 2…), one per pushed change. Pushes are queued in memory (64 requests wait at most; further pushers block) and are not kept across restarts: a push that was not confirmed when the service stopped fails, and the sender must retry. The schema is a single id column (the primary key unless the mapping names another).

Do not resync a webhook task

The webhook source has nothing to copy: its snapshot is empty. A Resync (or a Reset checkpoint while zero-downtime resync is on) rebuilds the index from nothing: an in-place resync clears it, and a zero-downtime rebuild swaps in an empty shadow index. Your application then has to push every document again. Use the stream sync mode for webhook tasks: it refuses resyncs (409, resync needs a snapshot). Never use snapshot mode.

Meili CDC user guide · last updated 2026-10-26 · Edit this page on GitHub

meilisearch-cdc is open source. Meilisearch is a trademark of Meili SAS; this project is not affiliated with it.