Skip to content
Go to Micro
Using the API

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:

  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.
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.

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
&micro_hook_challenge=<random-string>
&micro_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.

// 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.

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": {}
}
FieldDescription
idUnique delivery id. Stable across retries — use it to dedupe.
webhook_idThe webhook this was sent to.
eventWhat happened (e.g. webhook.test).
occurred_atISO-8601 timestamp the event was generated.
dataEvent-specific payload.

Every delivery carries these headers:

HeaderDescription
X-Micro-Signaturet=<unix>,v1=<hmac> — see below.
X-Micro-Webhook-IdThe webhook id.
X-Micro-Delivery-IdThe delivery id (matches id in the body).
X-Micro-EventThe event name.

Respond with any 2xx to acknowledge. Any non-2xx (or a timeout) is treated as a failure and retried — see Deliveries & retries.

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.
  • v1HMAC-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. :::

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);
});
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)

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.

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.

MethodPathDescription
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}/verifyRe-run the verification handshake
POST/v2/webhooks/{teamId}/{webhookId}/pingSend 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"