Agent skill
Meilisearch Webhooks Skill
Receive and authenticate Meilisearch task webhooks. Use when setting up a Meilisearch webhook endpoint (Cloud project webhooks, self-hosted --task-webhook-url, or the /webhooks API), checking the Authorization header, decoding the gzipped ndjson body, debugging 401s, or handling finished tasks such as documentAdditionOrUpdate, settingsUpdate and indexDeletion.
Install this skill
npx skills add hookdeck/webhook-skills --skill meilisearch-webhooks
Meilisearch (open-source search engine) sends an HTTP POST to your endpoint whenever tasks finish. Payloads are not signed: the only credential is the Authorization header you configure. The body is gzip-compressed ndjson with one finished task object per line.
When to Use This Skill
- How do I receive Meilisearch webhooks?
- How do I verify the Meilisearch webhook
Authorizationheader? - Does Meilisearch sign webhook payloads? (No: shared
Authorizationvalue only) - Why is my Meilisearch webhook body unreadable / why does
JSON.parsefail on it? - How do I handle
documentAdditionOrUpdateor failed tasks from a webhook? - Why does my Meilisearch webhook endpoint return 401?
Verification (core)
Set the Authorization header when you create the webhook (Meilisearch Cloud: Project Settings > Webhooks; self-hosted: --task-webhook-authorization-header). Meilisearch sends it verbatim, so store the whole value exactly as set in Meilisearch, including any Bearer or Basic prefix, and compare the whole header to it. Don't strip, parse or base64-decode it.
const crypto = require('crypto');
const zlib = require('zlib');
function safeEqual(a, b) { // hash both sides: equal-length buffers, never throws
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
const expected = process.env.MEILISEARCH_AUTHORIZATION; // e.g. "Bearer your-secret-token"
const received = req.headers['authorization'];
if (!expected || !received || !safeEqual(received, expected)) return res.status(401).end();
let body = req.body; // raw Buffer
if (/gzip/i.test(req.headers['content-encoding'] || '')) body = zlib.gunzipSync(body); // 400 on error
const tasks = body.toString('utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
Check the credential first, then gunzip, then JSON.parse each line, never the whole body. Return 401 for a missing or wrong header, 400 for a body that can't be gunzipped or parsed, and 2xx once handled. Meilisearch Cloud documents retries with exponential backoff for non-2xx responses; the self-hosted docs don't mention retries, and the open-source engine sends each request once with a 30-second timeout, so a failure there is lost. Python and Next.js equivalents are in references/verification.md.
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Task Types and Statuses
Each ndjson line is a finished task object (same shape as the Tasks API: uid, indexUid, status, type, details, error, ...). Route on type and status:
type | Meaning |
|---|---|
documentAdditionOrUpdate | Documents added or updated (details.indexedDocuments) |
documentDeletion | Documents deleted |
settingsUpdate | Index settings changed |
indexCreation / indexUpdate / indexDeletion | Index lifecycle |
indexSwap | Indexes swapped |
dumpCreation / snapshotCreation | Backups |
taskCancelation / taskDeletion | Task queue maintenance |
status is succeeded, failed or canceled (only finished tasks are sent).
Environment Variables
MEILISEARCH_AUTHORIZATION=Bearer your-secret-token # full header value, prefix included
Local Development
npx hookdeck-cli listen 3000 meilisearch --path /webhooks/meilisearch
No account required. Use the printed URL as the webhook URL. Hookdeck stores gzip bodies as binary, so events show a binary body in the dashboard; it forwards the original bytes and Content-Encoding header. In Hookdeck's Meilisearch source (type MEILISEARCH): paste the Authorization header value exactly as set in Meilisearch, including any "Bearer " or "Basic " prefix.
Resources
- references/overview.md - How task webhooks work, payload and task types
- references/setup.md - Cloud, self-hosted flags and the /webhooks API
- references/verification.md - Header check, gunzip and ndjson in Node, Python and Next.js, gotchas
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests