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/searchandPOST /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
/mcpendpoint is stateless Streamable HTTP, so a singlecurlPOST 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).
| Kind | Subject | Fired when |
|---|---|---|
new_candidate | person | A new candidate becomes visible to your organization. |
profile_view | organization | An organization viewed a candidate's profile (the subject is the viewing organization, never the viewer). |
scout_received | job | Dormant — employer-initiated sending is retired (ADR-0135), so nothing fires it; existing rows stay. |
recommendation | person | Reserved (dormant) — recommendation events; the vocabulary member exists but nothing fires it yet. |
approval_pending | candidacy | An agent-proposed write was filed and awaits a human approval decision. |
message_received | thread | A new message arrived on a thread. |
interest_received | job | A job's team expressed interest in a candidate. |
mutual_interest | person | Both sides have now expressed interest — the pair became mutual. |
interview_schedule_updated | candidacy (or job) | An interview slot was proposed, confirmed, or cancelled. |
candidacy_stage_changed | candidacy | A candidacy moved to another pipeline stage. |
approval_decided | candidacy | A filed approval request was decided by a human. |
job_published | job | A job posting went live. |
webhook_endpoint_disabled | webhook_endpoint | An endpoint was auto-disabled after consecutive failures. In-app only — never delivered over webhooks. |
evaluation_requested | job | A staff member was asked to evaluate candidates against a job. |
candidacy_advance_pending | candidacy | A candidacy entered the advance-decision queue and awaits a stage decision. |
interview_slot_responded | candidacy (or job) | The candidate responded to a proposed interview slot. |
counter_request_received | job | A candidate filed a terms request (counter) on a scout thread. |
counter_request_resolved | job | A candidate's terms request was resolved by re-offer or decline. |
interview_completed | candidacy | A confirmed interview slot's start time passed — result input is now open on both sides. |
interview_result_recorded | candidacy | The counterpart recorded a positive interview result (negative results ride the stage-change events). |
interview_reminder | candidacy (or job) | A confirmed interview slot's start is approaching (fired once per reminder window per recipient). |
interview_slot_response_nudge | candidacy (or job) | A proposed slot is nearing its start with no candidate response yet (fired once per slot per window). |
interview_slot_confirmation_nudge | candidacy (or job) | A candidate-accepted slot is nearing its start and is still unconfirmed (fired once per slot per window). |
market_benchmark_update | organization | The disclosed market-rate digest changed for a subscribed organization. |
credit_expiry_upcoming | billing_purchase | Unused interview credits from a purchase are approaching expiry. |
credit_balance_low | organization | An organization's interview-credit balance reached the low-water mark. |
matching_paused | organization | New matching paused because the organization has no credits and auto-replenish is off. |
material_disclosure_opened | organization | A selection milestone auto-opened the candidate's material(s) to an organization. |
material_request_received | job | A material request was filed and awaits the candidate's answer. |
material_request_answered | job | A filed material request was already answered by the candidate's standing policy — a receipt, no decision. |
material_request_resolved | job | The 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 ofscreening_passed,interviewing,offered,accepted, ordeclined.record_interview_verdict— record the employer's verdict for one completed interview slot:slotId(fromget_pipeline_candidacy) plusverdictofpassedordeclined.convert_candidacy— convert anacceptedcandidacy into a hire record, optionally withstartDate,title, andemploymentType.
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.