Webhooks
Receive matchwire notification events as signed HTTPS POSTs.
matchwire pushes notification events to your registered HTTPS endpoints
as signed, thin JSON POSTs (MW-443, ADR-0106). One kind is deliberately
excluded: webhook_endpoint_disabled (the auto-disable alert) is delivered
in-app only — a dead webhook cannot carry its own outage notice. Register and manage
endpoints with the MCP tools (create_webhook_endpoint,
list_webhook_endpoints, update_webhook_endpoint,
rotate_webhook_endpoint_secret, delete_webhook_endpoint — scope
notification); observe deliveries with list_webhook_deliveries.
The full event catalog (every kind, its subject, and when it fires) and an end-to-end subscribe → read → write-back recipe live on ATS integration.
The event payload is thin — re-fetch for detail
A delivery body carries identifiers only, never content:
{
"schemaVersion": "1.0.0",
"deliveryId": "6f8b…",
"kind": "candidacy_stage_changed",
"subject": { "kind": "candidacy", "ref": "9d2c…" },
"recipientToken": "b41a…",
"occurredAt": "2026-07-11T02:03:04.000Z"
}Fetch the current state through your authenticated MCP/REST credential (for
example a candidacy subject via get_pipeline_candidacy, a job via
get_job_posting, a thread via get_employer_thread). This is deliberate:
content always renders behind matchwire's disclosure gate, so a webhook can
never leak what your credential could not read — and erasure or consent
withdrawal applies retroactively because nothing is copied out.
Verifying signatures
Every request carries these headers:
| Header | Value |
|---|---|
Matchwire-Signature | t=<unix seconds>,v1=<hex HMAC> |
Matchwire-Webhook-Id | The delivery attempt id (webhook_deliveries.id) |
Matchwire-Event-Kind | The event kind, for cheap routing |
Content-Type | application/json |
To verify:
- Parse
tandv1fromMatchwire-Signature. - Compute
HMAC-SHA256(secret, "<t>." + rawBody)with themw_whsec_*secret returned when you created (or last rotated) the endpoint, and hex-encode it. - Compare with
v1using a constant-time comparison. - Reject requests whose
tis outside your replay window — 5 minutes is the recommended tolerance.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret: string, header: string, rawBody: string): boolean {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header);
if (match === null) return false;
const [, t, v1] = match;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min
const mac = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(mac), Buffer.from(v1 as string));
}The secret is shown once in the create_webhook_endpoint /
rotate_webhook_endpoint_secret response and can never be read back. Rotate
immediately if you suspect a leak — the old secret stops signing the moment
rotation completes.
Delivery contract
- At-least-once, unordered. The same event can arrive more than once and
events may arrive out of order. Deduplicate on
deliveryIdand, if you need ordering, sort byoccurredAt(tie-breakdeliveryId) yourself. - Respond fast with a 2xx. Anything in
200–299marks the delivery succeeded. Do heavy work asynchronously after acknowledging. - Retries: a
5xx,429,408, or network/timeout failure is retried with exponential backoff up to 10 attempts. Any other4xxis treated as permanent and is not retried. Each attempt times out after 10 seconds. - Auto-disable: after 10 consecutive permanent failures, the endpoint
is disabled automatically and its creator receives a
webhook_endpoint_disabledin-app notification. Fix the receiver, then re-enable withupdate_webhook_endpoint { active: true }(this also resets the failure counter). Events published while disabled are not delivered retroactively — catch up withlist_notification_deliveries. - Requirements: endpoints must be public HTTPS URLs — plain
http, private/loopback addresses, private-use hostnames (.local,.internal,.lan,home.arpa, a dotless single label, …), and URLs with embedded credentials are refused at registration and re-checked at send time.
Choosing events
create_webhook_endpoint accepts an optional kinds filter (a non-empty
subset of the notification kinds). Omit it to receive all kinds,
including ones added in future releases — unknown kind values must not
break your consumer (parse tolerantly, route on what you know).