PlacementFlow can feed your CRM or ATS as work happens: a person engages, a recruiter shortlists them, a submission goes to a client, a placement is recorded. This page covers the three ways to connect, which events to subscribe to, what each event carries, and what you are responsible for once the data leaves PlacementFlow.
The simplest path. PlacementFlow has an invite-only Zapier app; it is not listed in Zapier's public directory. Ask PlacementFlow support for the invite link, accept it, and the app appears in your Zapier account.
The app gives you:
You connect it with a PlacementFlow API key. Create one under Settings → Integrations → API Keys and pick the Automation (Zapier/Make) preset. When you turn a Zap on, the app registers its own webhook in PlacementFlow; when you turn it off, the webhook is removed.
Step-by-step recipes:
If you would rather not use the Zapier app, point a generic webhook step at a PlacementFlow webhook:
Copy the URL the tool gives you, then create a webhook in Settings → Webhooks with that URL and the events you want. There is no PlacementFlow app for Make or n8n; this generic path is the way to use them.
One difference to know about: a webhook created in Settings → Webhooks is treated as a direct integration and receives the full payload, including the fields listed under What automation endpoints never receive. If you want the reduced payload for a Make or n8n scenario, register the webhook through the API instead, with source set to make or n8n (see the API reference, POST /api/v1/webhooks).
If you have a developer, point a webhook at your own HTTPS endpoint and call the PlacementFlow REST API for anything else. Every delivery is signed (a legacy HMAC header and the Standard Webhooks headers), and every event's payload is described in the OpenAPI document at https://placementflow.com/api/v1/openapi.json. The API reference covers signature verification, secret rotation, retries and replays.
In Settings → Webhooks, the CRM feed preset selects exactly the events below. They are the ones that record a durable fact a CRM keeps as a record or an activity. Scheduling negotiation (slot proposals, expiries), internal failures and raw message content are left out.
| Event | Label | What it means |
|---|---|---|
candidate.engaged | Candidate engaged | The first time PlacementFlow engaged a person (a cold reply, a screening, a booking, a submission). This is the "create person" moment, once per person. |
candidate.shortlisted | Candidate shortlisted | A screening response was shortlisted and the decision stands. A recruiter's shortlist is sent after its undo window closes, never after an undo. |
candidate.created | Candidate created | A recruiter created a candidate from the form or a dropped CV. For a CRM feed, prefer candidate.engaged, which also covers sourced people. |
candidate.updated | Candidate updated | A recruiter edited a candidate profile, by hand or from a dropped CV. |
candidate.erased | Candidate erased | A candidate was erased under GDPR. Carries ids and the external reference only. |
pipeline.status_changed | Pipeline status changed | A candidate moved stage on a job pipeline. |
screening.completed | Screening completed | A screening was scored, before any human decision. The recommendation is advisory: PlacementFlow never rejects on a score, and only a recruiter declines. |
screening.disqualified | Screening declined by recruiter | A recruiter declined a screening response. Scoring never fires this. |
submission.sent | Submission sent | An identified submission's first email to one client left the agency's mailbox, under the consent authority it names. One event per client. |
placement.created | Placement created | A placement was recorded (offer acceptance, manual entry, or the reconciliation re-send). |
interview.scheduled | Interview scheduled | An interview has a confirmed time for the first time. |
interview.rescheduled | Interview rescheduled | A scheduled interview moved to a new time. |
interview.completed | Interview completed | An interview took place (bot transcript, time-based auto-completion, or a manual or API completion). |
interview.cancelled | Interview cancelled | An interview was cancelled. A candidate withdrawal fires interview.rejected instead. |
interview.no_show | Interview no-show | A no-show was confirmed by a recruiter, the API or the recording bot. |
interview.rejected | Interview rejected | A party definitively declined the interview. |
interview.decision_made | Client decision made | The client gave post-interview feedback on the portal: proceed to offer, another interview, or not a fit. |
bd_prospect.qualified | BD prospect qualified | A business-development prospect became qualified (interested), once. Company and contact, never the email thread. |
meeting.scheduled | Meeting scheduled | A BD or discovery meeting has its first confirmed time. |
meeting.rescheduled | Meeting rescheduled | A meeting that already had a confirmed time got a new one. |
meeting.cancelled | Meeting cancelled | A meeting was cancelled by the host or the guest, or a booking request was declined. |
meeting.completed | Meeting completed | The host marked a meeting complete. |
The Settings page lists every other event too, grouped, if you need more than the CRM feed.
Events about a candidate (candidate.engaged, candidate.shortlisted, candidate.created, candidate.updated, screening.completed, screening.disqualified, submission.sent, pipeline.status_changed, placement.created) carry the same reference block, so a CRM can always file the event against the right record:
candidate_id, agency_id, job_id and client_id (the last two are null when the event has no job or client)candidate: id, first_name, last_name, email, phone, linkedin_url, location, headline, and:
pf_url: a link to the candidate's record in PlacementFlow (sign-in required)external_source and external_id: the CRM record this person is linked to, once you have written it back (see below), otherwise nullrepresentation_consent: one of given, not_yet, missing or withdrawn, read from the consent recordrepresentation_consent_at: when that consent state was recorded (null when missing)job and client: id, title or name, and their own external_source and external_id, or nullIn the PlacementFlow Zapier app, nested fields are flattened with double underscores, so you will see candidate__email, candidate__representation_consent and candidate__external_id, next to test, event and occurred_at. A generic webhook step receives the full envelope instead (id, event, occurred_at, test, and the event's fields under data).
Payloads never carry raw screening answers or transcripts, reviewer notes, email bodies, CV files, or business-development email threads.
Consent is about representing a candidate to employers. representation_consent tells you whether the candidate has agreed to be put forward to clients. Writing a candidate into your own CRM is your agency's own processing, but PlacementFlow cannot see where a webhook leads, so every candidate event carries the consent state and every recipe filters on it.
representation_consent in every Zap or scenario that could lead to a client seeing the candidate.representation_consent is withdrawn.candidate.engaged fires early, before a candidate has been asked for consent, so not_yet and missing are normal there. Treat them as "not cleared to represent yet".submission.sent only exists because PlacementFlow's consent check passed at send time; consent_authority says whether that was the candidate's own consent or a recorded recruiter attestation.A copy in a CRM or a spreadsheet is a new copy of personal data. You are responsible for it: its security, its retention, and answering access and erasure requests for it.
Erasure reaches your copy through candidate.erased. When a candidate is erased in PlacementFlow (on request or by the retention schedule), this event tells you so. It carries only candidate_id, agency_id, external_source, external_id, erased_at and reason (request or retention), and no other personal data. The copy in your CRM is yours to delete: find it by external_id (or by candidate_id if you stored that) and delete it, or create a task for someone to do so.
Once your CRM has a record for the candidate, write its id back to PlacementFlow:
PUT /api/v1/candidates/{id}/external-ref with a body of source and external_id. source is one of bullhorn, atlas, ezekia, hubspot or other. The key needs the candidates:write scope.What this gives you:
candidate.external_source and candidate.external_id, so your Zap can update the right CRM record directly instead of searching by email.candidate.erased carries the same reference, so deletion targets the right record.How it behaves:
409 response naming that candidate, so you can see which record is the duplicate.Webhooks registered by the Zapier app, or through the API with source set to zapier, make or n8n, never receive:
reply_text on email.replied; the short preview is still sent)These links work like keys to someone's interview, and a Zap step is not a safe place to keep them. A webhook you create in Settings → Webhooks is treated as a direct integration and still receives them.
A test delivery is marked twice: "test": true in the body, and an X-Webhook-Test: true header. The Send a test button (the test-tube icon) in Settings → Webhooks can send the sample payload of any event, so you can map fields without waiting for real activity. Sample payloads contain made-up data.
Add a filter on test (only continue when it is false) to any Zap or scenario that writes to a live system, so a test never creates a real CRM record.
source set to make instead.test equals false, and data.candidate.representation_consent is not withdrawn.source set to n8n.<X-Webhook-Timestamp header>.<raw request body>, keyed with your webhook secret, and compare it with the X-Webhook-Signature header. The HMAC must run over the raw body exactly as received, so enable the Webhook node's raw body option.standardwebhooks package with the whsec_ form of your secret to verify the webhook-id, webhook-timestamp and webhook-signature headers (self-hosted n8n has to allow the package in its Code node settings).test is false, and data.candidate.representation_consent is not withdrawn.