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). An endpoint belongs to one job (ADR-0187, the GitHub repository-webhook shape): it receives that job's events only, every payload names the jobId, and events that belong to no job — the organization and endpoint-alert kinds, among them webhook_endpoint_disabled (the auto-disable alert, delivered in-app only: a dead webhook cannot carry its own outage notice), and in the commercial edition the billing kinds — never reach a webhook. Register and manage endpoints on the job's settings tab (求人 → 設定 → Webhook) or with the MCP tools (create_webhook_endpoint with the jobId, 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. This page is the organization/job axis; a candidate's own endpoints — receiving the person's own events, managed under the person's own OAuth scopes — are the separate Person webhooks page.

The event payload is thin — re-fetch for detail

A delivery body carries identifiers only, never content:

{
  "schemaVersion": "1.1.0",
  "deliveryId": "6f8b…",
  "kind": "candidacy_stage_changed",
  "subject": { "kind": "candidacy", "ref": "9d2c…" },
  "recipientToken": "b41a…",
  "jobId": "1c7e…",
  "occurredAt": "2026-07-11T02:03:04.000Z"
}

jobId (added in 1.1.0) is the endpoint's own job, so a receiver that serves several jobs routes on it without a re-fetch.

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:

HeaderValue
Matchwire-Signaturet=<unix seconds>,v1=<hex HMAC>
Matchwire-Webhook-IdThe delivery attempt id (webhook_deliveries.id)
Matchwire-Event-KindThe event kind, for cheap routing
Content-Typeapplication/json

To verify:

  1. Parse t and v1 from Matchwire-Signature.
  2. Compute HMAC-SHA256(secret, "<t>." + rawBody) with the mw_whsec_* secret returned when you created (or last rotated) the endpoint, and hex-encode it.
  3. Compare with v1 using a constant-time comparison.
  4. Reject requests whose t is 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, or on the settings tab right after 「Webhookを追加」/「シークレットを再発行」 — 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 deliveryId and, if you need ordering, sort by occurredAt (tie-break deliveryId) yourself.
  • Respond fast with a 2xx. Anything in 200–299 marks 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 other 4xx is 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_disabled in-app notification. Fix the receiver, then re-enable with update_webhook_endpoint { active: true } (this also resets the failure counter). Events published while disabled are not delivered retroactively — catch up with list_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 (and the settings tab's 「選んだ動きだけ」) accepts an optional kinds filter — a non-empty subset of the job-scoped notification kinds (the event catalog lists them, and the kinds that never reach a webhook). Omit it to receive all job-scoped kinds, including ones added in future releases — unknown kind values must not break your consumer (parse tolerantly, route on what you know). A kind that belongs to no job is refused at registration.

On this page