# 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 `Authorization` header?
* Does Meilisearch sign webhook payloads? (No: shared `Authorization` value only)
* Why is my Meilisearch webhook body unreadable / why does `JSON.parse` fail on it?
* How do I handle `documentAdditionOrUpdate` or 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.

```javascript
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/meilisearch-webhooks/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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/meilisearch-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/meilisearch-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/meilisearch-webhooks/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

```bash
MEILISEARCH_AUTHORIZATION=Bearer your-secret-token   # full header value, prefix included

```

## Local Development

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/meilisearch-webhooks/references/overview.md) - How task webhooks work, payload and task types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meilisearch-webhooks/references/setup.md) - Cloud, self-hosted flags and the /webhooks API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/meilisearch-webhooks/references/verification.md) - Header check, gunzip and ndjson in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/meilisearch-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests