Wiring interviews into your ATS: the API, the events, and the mistakes that get expensive
Written for whoever implements it. Who is in charge, why the request that creates an interview has to be repeatable, how a signature is verified, and the mistake nobody sees coming: the same word with two meanings.
Integration · API and events
Published on · 7 min read
You integrate by letting the client system remain the source of the role and the candidate, creating interviews with an idempotency key so a retry duplicates nothing, and receiving signed events instead of polling. The signature is verified with a constant-time comparison and with the timestamp inside the signed message. Before any code is written, both sides agree in writing on what each word means.
An interview integration looks very little like the one you imagine at the start. Almost nobody trips over the shape of the JSON. They trip over the network that dropped mid-request, the event that arrived twice, the receiver that was down for forty minutes, and the word both teams had been using for months believing it meant the same thing.
What follows is the list of decisions to make before writing the first line, ordered by what they cost to fix later. The specific operations that exist today are on the integrations page; this issue is about using them without getting hurt.
The ATS is still in charge, and it is worth saying out loud
The first decision is about ownership, not architecture: who owns what. The role is born in the client system, and so is the candidate. The interview platform does not create them: it receives them, mirrors them, and returns what it produced about them.
That means syncing a role is an operation you can repeat without consequences — you send the current state and it lands — and that the identifier in charge is the one from the ATS. When each side has its own identifier and neither is authoritative, the first reconciliation is done by hand, and so is every one after it.
It is worth writing this sentence into the scope document: “the source of truth for the role and the candidate is the ATS; the source of truth for the interview, its transcript and its report is the platform”. Half an hour of argument there saves weeks afterwards.
The request that creates has to be repeatable
This is the most common problem and the one that looks worst from outside. The ATS requests an interview for a person. The request goes out, the platform processes it, and the response is lost on the way back: a timeout, a load balancer that cut the connection, a corporate network doing what corporate networks do. The ATS does not know whether the interview exists, so it retries. And the person receives two links.
The fix is not retrying more carefully: it is making the retry incapable of duplicating anything. The request that creates carries an idempotency key, generated by the ATS and stable for that combination of candidate and role. If the key has been seen before, the platform returns the interview that already exists instead of creating a second one.
- The key is generated by the caller, not the receiver. Generated by the receiver it is useless: the side that retries does not have it.
- The key has to be stable across a retry and different across a legitimately new request. An identifier derived from the candidate and the role does both; a timestamp does neither.
- Store the key you used next to the ATS record. The day somebody has to audit why two interviews exist, that column is the answer.
Events instead of polling
The temptation is to ask every minute whether the interview has finished. It works in the demo and degrades on its own: with a hundred active candidates that is a hundred questions a minute, and ninety-nine per cent of the answers say “not yet”.
Worse than the cost is the latency. Polling introduces, by definition, a delay equal to the interval, and that delay shows up exactly where it matters: the recruiter refreshes the ATS, sees a stale state, and calls support. An event arrives when the thing that had to happen happened.
The practical rule: events are the primary mechanism and direct queries are the safety net. One reconciliation query a day over interviews that have sat too long in the same state covers whatever a lost event might leave behind, without turning the integration into a permanent survey.
Verifying the signature, and why the timestamp goes inside
An event receiver is a public address. If it does not verify who is talking to it, anyone who finds it can push false states into the ATS. Verification is short to write, and there are three details where it breaks.
- Sign the EXACT body, byte for byte, as it arrived. If your framework deserialises the JSON and you re-serialise it to verify, the signature will not match even when the content is identical: key order and whitespace change.
- Compare in constant time. An ordinary string comparison returns early when the first character differs, and that microsecond difference, measured enough times, lets someone guess the signature character by character.
- Reject a distant timestamp. A window of a few minutes against your own clock is enough, and it is what stops someone replaying a legitimate event captured weeks ago.
That third point has a design consequence people skip: the timestamp has to travel INSIDE the signed message, not merely alongside it. If it travels outside, whoever captured an old event replays it with the hour changed, and the signature — computed over the body alone — is still valid. Signing the concatenation of the timestamp and the body closes that hole.
The receiver has to be idempotent too
A serious event system retries when it gets no acknowledgement. That means your receiver will see the same event twice, sooner or later, and not because anyone made a mistake: because your response was lost after you had already processed it.
Processing “the interview finished” twice is usually harmless. Processing “the report is ready” twice can fire two emails at the same recruiter, or move a stage in the ATS twice. The defence is storing the identifier of every event already processed and discarding the repeat, with a table and a uniqueness constraint. Nothing more sophisticated is needed.
Answer fast and work afterwards. A receiver that acknowledges as soon as it has stored the event, and does the heavy work in the background, does not provoke retries through slowness. A receiver that renders a PDF before answering does.
When the other side does not answer
It will happen. Maintenance, an expired certificate, a deployment that took longer than planned. What separates a grown-up integration from a fragile one is having decided beforehand what happens then.
| Situation | What the sender should do | What the receiver should do |
|---|---|---|
| The receiver returns a temporary error | Retry with growing backoff, not in a tight loop | Return the real error, not an empty success |
| The receiver has been down for hours | Keep retrying inside a bounded window, then stop | Reconcile on return, querying whatever was left pending |
| A repeated event arrived | Nothing: the retry is correct | Discard it by its identifier and acknowledge anyway |
| The signature does not validate | Nothing | Reject, log the attempt, and do not process it |
And one request almost nobody makes and everybody appreciates: a test event, fired by hand at the receiver to confirm the address answers and the signature validates, before a single real candidate is involved.
The most expensive mistake: one word, two meanings
Everything above is fixable with code. This is not. In an integration meeting, somebody from the ATS side says something like this:
For us “rejected” means the recruiter dropped the person. From what you are describing, for you “rejected” means the person declined the interview. Those are not the same, and we have spent three weeks mapping that field.
That discovery is cheap in week one and ruinous in week twelve, when dashboards have been built on top of it. The list of words to define in writing is short and always the same: role, candidate, application, interview, rejected, finished, active.
| Word | What it usually means in an ATS | What it usually means in the interview |
|---|---|---|
| Role | The approved requisition, with budget | The script and competencies it will interview with |
| Candidate | The person, unique across the whole database | The person in the context of one specific role |
| Rejected | The recruiter dropped the person | The person declined the interview |
| Finished | The selection process closed | The conversation ended, even if the report is still coming |
What this does not solve
A well-built integration moves data between two systems without losing or duplicating it. It does not fix a selection process that was never clear, it does not make comparable a set of interviews run with different criteria — that lives earlier, in the script, and the August issue deals with it — and it does not replace the scoping conversation where you decide which fields travel and in which direction.
Nor does it eliminate the reconciliation work of the first month. It makes it small and explainable, which is the most you can ask of an integration between two systems that had never met.
Questions about this issue
Should we start with the integration or with a few roles by hand?
A few by hand, almost always. The first weeks surface vocabulary and process mismatches that no document reveals, and it is far cheaper to find them before writing the code that maps them. The buying checklist includes that question for the same reason.
What if our ATS cannot receive incoming events?
Common in older or closed installations. The alternative is a periodic reconciliation query over active interviews, accepting the latency it introduces. It works; you simply have to choose the interval with your eyes open rather than discover it when somebody complains about a stale state.
How much information should be returned to the ATS?
Less than is asked for at the start. The interview state and a link to the report usually suffice; pulling the full transcript into the ATS duplicates sensitive material into a system that may have a different retention policy. The May issue covers that.