Webhooks
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:
- Register an endpoint and get a signing secret.
- Micro verifies the endpoint with a one-time handshake.
- Micro delivers signed events to it; you verify each signature before trusting the payload.
Creating a webhook
Section titled “Creating a webhook”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)
Section titled “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=<random-string> µ_hook_token=<your verification_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.
// Expressapp.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
Section titled “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=<unix>,v1=<hmac> — 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.
Verifying signatures
Section titled “Verifying signatures”Always verify the signature before acting on a payload. The X-Micro-Signature header looks like:
X-Micro-Signature: t=1750871051,v1=3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1bt— the unix timestamp when the request was signed.v1—HMAC-SHA256(secret, "{t}.{rawBody}"), hex-encoded, using yourwhsec_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
Section titled “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
Section titled “Python”import hashlibimport hmacimport 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
Section titled “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
Section titled “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):
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):
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
Section titled “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:
curl -X POST https://api.micro.so/v2/webhooks/{teamId}/{webhookId}/ping \ -H "x-api-key: YOUR_API_KEY"