Skip to content
Guides
API essentials

Webhooks

Register, verify, secure, test, and recover webhook deliveries.

Webhook endpoints live under /v2/webhooks/{teamId}. Register a public HTTPS receiver:

curl --request POST \
  "https://developers.micro.so/v2/webhooks/${MICRO_TEAM_ID}" \
  --header "content-type: application/json" \
  --header "x-api-key: ${MICRO_API_KEY}" \
  --header "idempotency-key: $(uuidgen)" \
  --data '{"name":"Production events","url":"https://example.com/micro/webhooks","enabled":true}'

The response shows the whsec_… signing secret once. Store it immediately. Creation starts an asynchronous verification handshake and can succeed before verified becomes true.

Complete the handshake

Micro sends a GET request with micro_hook_mode=subscribe, a one-time micro_hook_challenge, and micro_hook_token. Verify the token created for this webhook, then respond 200 with the challenge verbatim as plain text. Poll the webhook until verified is true or call its verify endpoint to retry.

Verify every delivery

Deliveries use x-micro-signature: t=<unix-seconds>,v1=<hex-hmac>. The digest is HMAC-SHA256 over <timestamp>.<raw-body> using the whsec_… secret. Preserve the raw request bytes; parsing and re-serializing JSON can change the digest.

import crypto from "node:crypto";

export function verifyMicroSignature(rawBody, header, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof header !== "string" || typeof secret !== "string") {
    return false;
  }
  const values = new Map(header.split(",").map((part) => {
    const index = part.indexOf("=");
    if (index < 1) return ["", ""];
    return [part.slice(0, index).trim(), part.slice(index + 1).trim()];
  }));
  const timestamp = values.get("t");
  const supplied = values.get("v1");
  if (!timestamp || !/^[0-9]+$/.test(timestamp) || !supplied || !/^[0-9a-f]{64}$/i.test(supplied)) {
    return false;
  }

  const timestampSeconds = Number(timestamp);
  const age = Math.abs(Date.now() / 1000 - timestampSeconds);
  if (!Number.isFinite(age) || age > 300) return false;

  const expected = crypto.createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBody]))
    .digest();
  const received = Buffer.from(supplied, "hex");
  return received.length === expected.length && crypto.timingSafeEqual(expected, received);
}

Reject invalid or stale signatures before parsing the event. Deduplicate work by x-micro-delivery-id, because a delivery can be attempted more than once. x-micro-webhook-id identifies the subscription and x-micro-event identifies the event name.

Acknowledge and recover

Return a 2xx response quickly, then process asynchronously. Non-2xx responses and timeouts are retried, and every attempt appears in the delivery log. Use the team or per-webhook delivery endpoints to find failures and inspect their attempt history. A webhook must be enabled and verified before the ping endpoint can send webhook.test.

The event catalog is not yet published. Accept unknown fields and event names without failing the receiver; route only events your integration understands.

micro.so