--- title: Webhooks | micro description: Receive, verify, and debug signed webhook events from Micro --- Webhooks let Micro push events to an HTTPS endpoint you control — for example, to notify your systems when a record changes or an automation fires. The lifecycle is: 1. **Register** an endpoint and get a signing secret. 2. Micro **verifies** the endpoint with a one-time handshake. 3. Micro **delivers** signed events to it; you verify each signature before trusting the payload. ## Creating a webhook Terminal window ``` curl https://api.micro.so/v2/webhooks/{teamId} \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "My endpoint", "url": "https://example.com/webhooks/micro" }' ``` The response includes the **signing secret** (prefix `whsec_`). It is returned only once, at creation — store it now; it is never shown again. ``` { "id": "9f8b…", "name": "My endpoint", "url": "https://example.com/webhooks/micro", "verified": false, "secret": "whsec_2bN…", "verification_token": "whtok_a1…", "verification": { "status": "pending" } } ``` `verified` starts `false`. Micro immediately runs the verification handshake (below) asynchronously; once your endpoint passes, `verified` flips to `true` and events start flowing. Poll `GET /v2/webhooks/{teamId}/{webhookId}` to observe it, or re-run it with the verify endpoint. ## Verifying your endpoint (handshake) Before delivering events, Micro confirms you control the URL with a challenge/echo handshake — a `GET` to your endpoint: ``` GET https://example.com/webhooks/micro ?micro_hook_mode=subscribe µ_hook_challenge= µ_hook_token= ``` Your endpoint must respond **`200`** with the value of `micro_hook_challenge` echoed back **verbatim** in the body. No signature is involved in the handshake — it only proves you control what the URL returns. ``` // Express app.get('/webhooks/micro', (req, res) => { res.status(200).type('text/plain').send(req.query.micro_hook_challenge || ''); }); ``` The handshake does not use the signing secret, so you can pass it before you’ve wired up signature verification. The `micro_hook_token` matches the `verification_token` from the create response if you want to assert it. ## Receiving events Deliveries are `POST` requests with a JSON body: ``` { "id": "3c1d…", "webhook_id": "9f8b…", "event": "webhook.test", "occurred_at": "2026-06-25T17:04:11.482Z", "data": {} } ``` | Field | Description | | ------------- | ------------------------------------------------------------- | | `id` | Unique delivery id. Stable across retries — use it to dedupe. | | `webhook_id` | The webhook this was sent to. | | `event` | What happened (e.g. `webhook.test`). | | `occurred_at` | ISO-8601 timestamp the event was generated. | | `data` | Event-specific payload. | Every delivery carries these headers: | Header | Description | | --------------------- | ------------------------------------------- | | `X-Micro-Signature` | `t=,v1=` — see below. | | `X-Micro-Webhook-Id` | The webhook id. | | `X-Micro-Delivery-Id` | The delivery id (matches `id` in the body). | | `X-Micro-Event` | The event name. | Respond with any `2xx` to acknowledge. Any non-2xx (or a timeout) is treated as a failure and retried — see [Deliveries & retries](#deliveries--retries). ## Verifying signatures **Always verify the signature before acting on a payload.** The `X-Micro-Signature` header looks like: ``` X-Micro-Signature: t=1750871051,v1=3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b ``` - `t` — the unix timestamp when the request was signed. - `v1` — `HMAC-SHA256(secret, "{t}.{rawBody}")`, hex-encoded, using your `whsec_` signing secret. To verify: recompute the HMAC over `"{t}.{rawBody}"` and compare to `v1` with a constant-time comparison. :::caution\[Use the raw body] Sign and compare against the **exact raw request bytes**, not a re-serialized object. Re-encoding JSON (key order, whitespace) will change the bytes and break verification. In Express, capture the raw body (e.g. `express.raw({ type: 'application/json' })` or a `verify` hook) — `express.json()` alone discards it. ::: ### Node.js ``` const crypto = require('crypto'); function verifyMicroSignature(rawBody, signatureHeader, secret) { const parts = {}; for (const kv of signatureHeader.split(',')) { const i = kv.indexOf('='); if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim(); } const { t, v1 } = parts; if (!t || !v1) return false; // Replay protection: reject timestamps older than 5 minutes. if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = crypto .createHmac('sha256', secret) .update(`${t}.${rawBody}`) .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(v1); return a.length === b.length && crypto.timingSafeEqual(a, b); } // Express handler (rawBody is the unparsed request body string) app.post('/webhooks/micro', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8'); if (!verifyMicroSignature(rawBody, req.get('X-Micro-Signature') || '', process.env.MICRO_WEBHOOK_SECRET)) { return res.status(401).send('invalid signature'); } const event = JSON.parse(rawBody); console.log('received', event.event, event.id); res.sendStatus(200); }); ``` ### Python ``` import hashlib import hmac import time def verify_micro_signature(raw_body: bytes, signature_header: str, secret: str) -> bool: parts = dict(kv.split("=", 1) for kv in signature_header.split(",") if "=" in kv) t, v1 = parts.get("t"), parts.get("v1") if not t or not v1: return False # Replay protection: reject timestamps older than 5 minutes. if abs(time.time() - int(t)) > 300: return False signed = f"{t}.".encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ### Replay protection The signed timestamp `t` lets you reject stale or replayed requests. The examples above reject anything older than 5 minutes; tune the tolerance to your needs. Combined with deduping on the delivery `id`, this makes deliveries safe to process exactly once. ## Deliveries & retries Every delivery attempt is recorded. A delivery is retried up to **2 times** on failure (non-2xx or timeout); attempts that still fail are dead-lettered. List an endpoint’s deliveries (newest first): Terminal window ``` curl "https://api.micro.so/v2/webhooks/{teamId}/{webhookId}/deliveries?status=failed&limit=25" \ -H "x-api-key: YOUR_API_KEY" ``` Inspect a single delivery and its full attempt timeline (response codes, bodies, errors): Terminal window ``` curl https://api.micro.so/v2/webhooks/{teamId}/{webhookId}/deliveries/{deliveryId} \ -H "x-api-key: YOUR_API_KEY" ``` Filters on the list endpoint: `status` (`success` | `failed`), `type` (`delivery` | `verification` | `all`), `before` / `after` (ISO-8601), `cursor`, `limit` (1–100). Use `cursor` with the response’s `next_cursor` to page. ## Managing webhooks | Method | Path | Description | | -------- | ------------------------------------------ | ----------------------------------- | | `POST` | `/v2/webhooks/{teamId}` | Create a webhook | | `GET` | `/v2/webhooks/{teamId}` | List webhooks | | `GET` | `/v2/webhooks/{teamId}/{webhookId}` | Get a webhook | | `PATCH` | `/v2/webhooks/{teamId}/{webhookId}` | Update (changing `url` re-verifies) | | `DELETE` | `/v2/webhooks/{teamId}/{webhookId}` | Delete a webhook | | `POST` | `/v2/webhooks/{teamId}/{webhookId}/verify` | Re-run the verification handshake | | `POST` | `/v2/webhooks/{teamId}/{webhookId}/ping` | Send a `webhook.test` event | Use `ping` to send a test delivery to a verified endpoint while wiring up your handler: Terminal window ``` curl -X POST https://api.micro.so/v2/webhooks/{teamId}/{webhookId}/ping \ -H "x-api-key: YOUR_API_KEY" ```