Person webhooks

Receive a candidate's own notification events as signed HTTPS POSTs.

matchwire can push a candidate's own notification events to HTTPS endpoints that candidate registers — the person-axis mirror of the job-scoped Webhooks page (MW-1437). This page is for implementers of a candidate's own agent. An endpoint belongs to one person: it receives that person's events only, a URL can be registered once per person (another person may register the same URL), and the endpoint dies with the person's account.

Who may register an endpoint

A person's endpoint sends the person's own activity trail to an external URL, so the permission to manage endpoints is the one the person granted when connecting the agent: the notification:write scope on the OAuth consent screen. A connection without it is refused with insufficient_scope and nothing is written; there is no further approval step, and the person withdraws the permission by revoking the connection in the matchwire app.

Managing endpoints

Six MCP tools (scope notification, candidate realm): create_person_webhook_endpoint, list_person_webhook_endpoints, update_person_webhook_endpoint, rotate_person_webhook_endpoint_secret, delete_person_webhook_endpoint — and the attempt log via list_person_webhook_deliveries. The signing secret (mw_whsec_*) is shown once, in the create/rotate response, and can never be read back. Endpoints must be public HTTPS URLs — plain http, private/loopback addresses, private-use hostnames, and URLs with embedded credentials are refused at registration and re-checked at send time.

The payload differs from the job axis

The person-axis event payload is an independent contract, schemaVersion 1.0.0 — not the job-axis payload:

{
  "schemaVersion": "1.0.0",
  "deliveryId": "6f8b…",
  "kind": "interview_schedule_updated",
  "subject": { "kind": "candidacy", "ref": "9d2c…" },
  "recipientToken": "b41a…",
  "occurredAt": "2026-07-11T02:03:04.000Z"
}
  • There is no jobId field. A person's own event stream has no job axis — endpoints belong to the person, not to a job — so the payload deliberately carries none.
  • recipientToken is always the endpoint owner's own opaque token: only the person's own events ever reach their endpoints.
  • The payload is thin — identifiers only, never content. Fetch current state through the candidate agent's authenticated MCP reads (a selection record via get_selection, a conversation via get_conversation, a job via get_published_job_posting), so content always renders behind matchwire's disclosure gate and erasure applies retroactively.

Verifying signatures

Signing is the same module and procedure as the job axis — follow Webhooks § Verifying signatures verbatim: parse Matchwire-Signature (t=<unix seconds>,v1=<hex HMAC>), compute HMAC-SHA256(secret, "<t>." + rawBody) with your mw_whsec_* secret, compare constant-time, and reject timestamps outside your replay window (5 minutes recommended). Matchwire-Webhook-Id carries the person-axis attempt id, and Matchwire-Event-Kind the event kind.

Delivery contract

  • At-least-once, unordered. Deduplicate on deliveryId; order by occurredAt yourself if you need it.
  • Respond fast with a 2xx; do heavy work after acknowledging.
  • Retries: a 5xx, 429, 408, or network/timeout failure is retried with exponential backoff up to 10 attempts; any other 4xx is permanent and not retried. Each attempt times out after 10 seconds.
  • Auto-disable: after 10 consecutive permanent failures the endpoint is disabled automatically and the person receives a webhook_endpoint_disabled in-app alert (in-app only — a dead webhook cannot carry its own outage notice). Fix the receiver, then re-enable with update_person_webhook_endpoint { active: true }. Events published while disabled are not delivered retroactively — catch up with list_notification_deliveries.

Choosing events

create_person_webhook_endpoint accepts an optional kinds filter — a non-empty subset of the candidate-receivable notification kinds (currently 24 kinds). Omit it (or clear it with update_person_webhook_endpoint { kinds: null }) to receive all candidate-receivable kinds, including ones added in future releases — parse tolerantly and route on what you know. A kind no candidate can receive is refused at registration. Note that webhook_endpoint_disabled itself, while in the subscribable set, is delivered in-app only and never arrives over a person webhook.

On this page