Every redeployment agent starts with a boring question: who is finishing, and when? Before there is any matching or outreach, there is a pipeline that pulls placement end dates out of the ATS on a schedule, reconciles them against reality, and lands them somewhere a report can read. This walkthrough builds that pipeline against JobAdder, because its developer portal is public and its OAuth setup is conventional. The shape of the work is identical on Loxo, Crelate, Vincere or Recruiterflow; the names and limits change, so keep your vendor's API reference open. JobAdder's lives at api.jobadder.com/v2/docs.
Step one: OAuth 2.0, not API keys
JobAdder's API authenticates integrations with OAuth 2.0, not a long-lived API key you paste into a script. That is the right call for a system holding candidate data, and it means your sync is a registered application acting as a nameable user from day one.
Register an application
Sign in on the developer portal and register an application. You get a client id and a client secret, and you nominate a redirect URI. For a back-office sync job the interactive part happens exactly once: an admin at the staffing firm consents, and your redirect URI catches the authorization code.
Exchange the code for tokens
Exchange the code at the token endpoint given in the API reference, requesting offline access so a refresh token is issued alongside the short-lived access token:
curl -s -X POST "$TOKEN_ENDPOINT" \
-d grant_type=authorization_code \
-d code="$AUTH_CODE" \
-d redirect_uri="$REDIRECT_URI" \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET"
Store the refresh token in a real secrets store, not a dotfile in the repository. The nightly job then trades it for a fresh access token:
curl -s -X POST "$TOKEN_ENDPOINT" \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN" \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET"
Refresh tokens can rotate when used. Persist the newly returned one atomically before making the first API call with the new access token, or the second bug you debug will be a lockout of your own making.
Step two: page through placements
The record you want is the placement: the row that ties a candidate to a job with dates and rates. Pull placements in pages, politely:
curl -s "$API_BASE/placements?offset=0&limit=100" \
-H "Authorization: Bearer $ACCESS_TOKEN"
A trimmed response looks roughly like this; treat it as illustrative and take the exact field names for your account from the API reference:
{
"items": [
{
"placementId": 4711,
"candidate": { "candidateId": 88112 },
"job": { "jobId": 5320 },
"startDate": "2026-03-02",
"endDate": "2026-09-26",
"status": { "name": "Active" }
}
],
"totalCount": 512
}
Three rules that hold on every ATS API, not just this one:
- Respect the rate limit. Read what the reference and the response headers say, back off when you are told to, and run the sync at night. A sync that hammers the API during business hours is how your integration user gets throttled while a recruiter is mid-search.
- Filter server-side, verify client-side. Query parameters for status and date ranges save bandwidth, but re-check in your own code; filter semantics differ between vendors and change between versions.
- Keep the raw response. Land the JSON exactly as received, then transform. When an end date looks wrong in March, you will want to know what the API actually said in January.
Step three: land it in a table
Keep the sync dumb and idempotent: one row per placement, upserted on the placement id, stamped with the sync time.
create table placement_snapshots (
placement_id bigint primary key,
candidate_id bigint not null,
job_id bigint,
start_date date,
end_date date,
status text,
raw jsonb not null,
synced_at timestamptz not null default now()
);
An insert ... on conflict (placement_id) do update per item means the job can crash halfway through and rerun without ceremony, which it eventually will.
Step four: distrust the end date
The recorded end date is a claim, not a fact. Extensions get agreed on a phone call and recorded late or never. So the last step is reconciliation across three signals: the ATS end date, whether timesheets are still arriving for the placement, and anything the account manager has noted. A placement whose end date passed two weeks ago but whose timesheets keep coming is really an unrecorded extension. Flag it for a person to fix in the ATS; do not silently override it. The system of record stays the system of record, and the agent drafts and flags rather than deciding, which is the same boundary our Redeployment Agent draws in production.
With clean, reconciled end dates in one table, the interesting work can start: bucketing who finishes in 30, 60 and 90 days, checking who has actually been contacted, and putting a draft in front of the recruiter who owns the relationship. If you would rather someone else wore the OAuth scars, contact us and name your ATS.