ATS integration

Wire an ATS or any external system to matchwire — subscribe to events, read state, write back outcomes.

matchwire integrates with applicant tracking systems through one generic recipe, not per-vendor connectors: subscribe to boundary events over webhooks, read current state through the authenticated MCP/REST surface, and write outcomes back with three MCP verbs. Anything that can receive an HTTPS POST and send an HTTPS request can run this loop — an ATS, a BI pipeline, or a homegrown script.

Two deliberate design points shape the recipe:

  • Reads have both an MCP surface (tool reference) and a REST mirror — but the REST mirror is intentionally limited to POST /v1/search and POST /v1/matches (see the OpenAPI reference in this section). There is no REST CRUD for pipeline state.
  • Write-back is MCP-only, by design. The three write verbs exist solely as MCP tools behind matchwire's human-approval gate. That is not a gap to wait out: the /mcp endpoint is stateless Streamable HTTP, so a single curl POST with a bearer credential calls any tool — no MCP SDK, no session handshake.

Event catalog

Webhook deliveries carry a kind from the notification vocabulary below — currently 27 kinds. One is deliberately excluded from webhook delivery: webhook_endpoint_disabled is in-app only, because a dead webhook cannot carry its own outage notice. Kinds are added over time; parse tolerantly and route on the kinds you know (see Choosing events).

KindSubjectFired when
new_candidatepersonA new candidate becomes visible to your organization.
profile_vieworganizationAn organization viewed a candidate's profile (the subject is the viewing organization, never the viewer).
scout_receivedjobDormant — employer-initiated sending is retired (ADR-0135), so nothing fires it; existing rows stay.
recommendationpersonReserved (dormant) — recommendation events; the vocabulary member exists but nothing fires it yet.
approval_pendingcandidacyAn agent-proposed write was filed and awaits a human approval decision.
message_receivedthreadA new message arrived on a thread.
interest_receivedjobA job's team expressed interest in a candidate.
mutual_interestpersonBoth sides have now expressed interest — the pair became mutual.
interview_schedule_updatedcandidacy (or job)An interview slot was proposed, confirmed, or cancelled.
candidacy_stage_changedcandidacyA candidacy moved to another pipeline stage.
approval_decidedcandidacyA filed approval request was decided by a human.
job_publishedjobA job posting went live.
webhook_endpoint_disabledwebhook_endpointAn endpoint was auto-disabled after consecutive failures. In-app only — never delivered over webhooks.
evaluation_requestedjobA staff member was asked to evaluate candidates against a job.
candidacy_advance_pendingcandidacyA candidacy entered the advance-decision queue and awaits a stage decision.
interview_slot_respondedcandidacy (or job)The candidate responded to a proposed interview slot.
counter_request_receivedjobA candidate filed a terms request (counter) on a scout thread.
counter_request_resolvedjobA candidate's terms request was resolved by re-offer or decline.
interview_completedcandidacyA confirmed interview slot's start time passed — result input is now open on both sides.
interview_result_recordedcandidacyThe counterpart recorded a positive interview result (negative results ride the stage-change events).
interview_remindercandidacy (or job)A confirmed interview slot's start is approaching (fired once per reminder window per recipient).
interview_slot_response_nudgecandidacy (or job)A proposed slot is nearing its start with no candidate response yet (fired once per slot per window).
interview_slot_confirmation_nudgecandidacy (or job)A candidate-accepted slot is nearing its start and is still unconfirmed (fired once per slot per window).
market_benchmark_updateorganizationThe disclosed market-rate digest changed for a subscribed organization.
credit_expiry_upcomingbilling_purchaseUnused interview credits from a purchase are approaching expiry.
credit_balance_loworganizationAn organization's interview-credit balance reached the low-water mark.
matching_pausedorganizationNew matching paused because the organization has no credits and auto-replenish is off.
material_disclosure_openedorganizationA selection milestone auto-opened the candidate's material(s) to an organization.
material_request_receivedjobA material request was filed and awaits the candidate's answer.
material_request_answeredjobA filed material request was already answered by the candidate's standing policy — a receipt, no decision.
material_request_resolvedjobThe candidate shared or declined a material request.

Kinds whose subject says "(or job)" use the job reference on the pre-application lane, where no candidacy exists yet.

The recipe

1. Get a credential

An organization admin mints an API credential in the product UI under Org settings → Connections (/org/connections) — pick the scopes, and copy the mw_sk_* secret when it is shown (once; it cannot be read back). This recipe needs notification:read, notification:write, pipeline:read, and pipeline:write.

2. Calling MCP tools with curl

/mcp is stateless: every call is a self-contained JSON-RPC POST — no session to establish, no state between calls. The response is a one-event SSE stream (Content-Type: text/event-stream) whose data: line is the JSON-RPC response; both Accept values below are required.

curl -s https://your-matchwire-host/mcp \
  -X POST \
  -H "Authorization: Bearer $MW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": { "name": "<tool name>", "arguments": {} }
  }'

3. Subscribe to events

Register an HTTPS endpoint with create_webhook_endpoint, optionally filtered to the kinds you handle. The signing secret (mw_whsec_*) is returned once in this response — store it now.

curl -s https://your-matchwire-host/mcp \
  -X POST \
  -H "Authorization: Bearer $MW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "create_webhook_endpoint",
      "arguments": {
        "name": "ats-sync",
        "url": "https://ats.example.com/hooks/matchwire",
        "kinds": ["candidacy_stage_changed", "interview_completed", "candidacy_advance_pending"]
      }
    }
  }'

Signature verification, the delivery contract (retries, auto-disable, at-least-once semantics), and endpoint management are covered on the webhooks page — implement its verification steps before going live.

4. Read state on each event

Delivery payloads are thin — identifiers only, never content. Re-fetch the current state through your credential using the matching read tool for the subject.kind: a candidacy via get_pipeline_candidacy, a job via get_job_posting, a thread via get_employer_thread. For example, on a candidacy_stage_changed delivery:

curl -s https://your-matchwire-host/mcp \
  -X POST \
  -H "Authorization: Bearer $MW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_pipeline_candidacy",
      "arguments": { "candidacyId": "<subject.ref from the delivery>" }
    }
  }'

The same disclosure gate that governs the product UI applies here, so a webhook can never leak what your credential could not read.

5. Write outcomes back

Three verbs cover the ATS write-back surface (scope pipeline:write):

  • transition_candidacy — move a candidacy along the employer edges: "to" is one of screening_passed, interviewing, offered, accepted, or declined.
  • record_interview_verdict — record the employer's verdict for one completed interview slot: slotId (from get_pipeline_candidacy) plus verdict of passed or declined.
  • convert_candidacy — convert an accepted candidacy into a hire record, optionally with startDate, title, and employmentType.
curl -s https://your-matchwire-host/mcp \
  -X POST \
  -H "Authorization: Bearer $MW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "transition_candidacy",
      "arguments": { "candidacyId": "<candidacy id>", "to": "interviewing" }
    }
  }'

Writes pass through matchwire's human-approval gate. A write succeeds only inside the job's standing human approval; outside it, the call is refused and the approval request is filed automatically — the refusal is the proposal, and a human decides in matchwire. Moving to accepted additionally requires a per-candidate hire approval, which a miss files the same way. Treat a refusal as "pending human decision", subscribe to approval_decided, and retry after the decision arrives.

On this page