Custom ATS integration
Host a small HTTPS API and Jack & Jill will use it the way it uses Ashby. Jack & Jill reads jobs, stages, and applications from you. Jack & Jill posts commands when a candidate is saved, moved, archived, or hired. You can POST a wake URL when you want that read to happen now. The wake does not carry application state. The next poll does.
The base URL you enter in Jill ends in /jack-jill/v1. Every path below is relative to that prefix.
Authentication
Reads and commands use Authorization: Bearer <token>.
Commands also send Jack-Signature: sha256=<hex HMAC-SHA256 of the raw body>, keyed with the outbound signing secret you enter in Jill. Verify that signature before you create or move anything. The Idempotency-Key header is the command identity. A retry of the same key returns the original external_id and does not create a second record.
The optional wake uses the same Jack-Signature header, keyed with the signing secret Jill shows after you connect. That secret is not the bearer token.
Reads
| Method and path | What it returns |
|---|---|
GET /jobs |
Jobs a role can link to. |
GET /jobs/{external_job_id}/stages |
That job’s pipeline, in your order. |
GET /applications?external_job_id=&cursor=&limit= |
One page of applications on that job. |
GET /applications/{external_application_id} |
One application. 404 means it is gone. |
GET /candidates?email= |
Candidates with that email. |
GET /candidates/{external_candidate_id} |
One candidate, including whether a resume is already stored. |
GET /jobs returns:
{
"jobs": [
{
"external_id": "job_1",
"title": "Engineer",
"status": "open",
"is_linkable": true,
"department": "Engineering",
"location": "London"
}
]
}is_linkable is true when the job can receive candidates.
GET /jobs/{external_job_id}/stages returns:
{
"stages": [
{ "external_id": "stage_lead", "title": "New lead", "slot": "lead", "order": 0 }
]
}slot is optional. Use lead, application_review, interview, offer, hired, or archived. Jill archives a candidate only while their current stage slot is lead. A stage with no slot is left in place.
GET /applications returns a page:
{
"applications": [],
"next_cursor": null,
"complete": true
}A full read can span pages. complete: true is legal only on the last page, and it means this page plus the earlier pages are the whole job. next_cursor on a full read is a page token. Omit the cursor when you always return the full list.
A delta is optional. When you send one, complete is false and next_cursor is the cursor Jack & Jill sends next time.
Each application looks like this:
{
"external_application_id": "app_1",
"external_candidate_id": "cand_1",
"external_job_id": "job_1",
"candidate": {
"name": "Ada Lovelace",
"primary_email": "ada@example.com",
"linkedin_url": "https://www.linkedin.com/in/ada"
},
"stage": { "external_id": "stage_1", "title": "Application review", "slot": "application_review" },
"outcome": "pending"
}outcome is pending, hired, or declined. primary_email is required. linkedin_url may be empty.
GET /candidates?email= returns:
{
"candidates": [
{ "external_id": "cand_1", "primary_email": "ada@example.com", "name": "Ada Lovelace" }
]
}Zero matches means the person is new. One match is reused. Two or more matches make Jack & Jill stop and not create a duplicate.
GET /candidates/{external_candidate_id} returns:
{ "external_id": "cand_1", "has_resume": false }has_resume is required. Jack & Jill uploads a file only when it is false. A missing field, or a 404, is not treated as “no resume”.
An application missing from a full pull stays as Jill last saw it. Jill moves the candidate when a later pull includes that application with a new stage, or with outcome hired or declined. A short list is not an archive and not a deletion.
Commands
POST /commands returns:
{ "external_id": "cand_1", "adopted": false }adopted: true means you matched an existing record instead of creating one.
type |
Fields |
|---|---|
create_candidate |
name, primary_email, linkedin_url, source |
create_application |
external_candidate_id, external_job_id, stage_id |
change_application_stage |
external_application_id, stage_id |
archive_application |
external_application_id |
upload_resume |
external_candidate_id, filename, content_type, content_base64 |
create_candidate_note |
external_candidate_id, note |
create_candidate_tag |
tag_title |
add_candidate_tag |
external_candidate_id, tag_id |
remove_candidate_tag |
external_candidate_id, tag_id |
source on create_candidate is the string Jack & Jill. stage_id on create_application may be null, which means your default stage.
content_base64 is the resume file. The same Idempotency-Key must store that file once. Jack & Jill sends this command only after it creates the candidate, and only when has_resume is false.
create_candidate_note posts one note. The same Idempotency-Key posts that note once. create_candidate_tag ensures a tag title exists and returns its external_id. The same title returns the same id. A response without external_id is retried. add_candidate_tag and remove_candidate_tag change whether that tag is on the candidate. Adding a tag that is already there, and removing one that is not, changes nothing. An empty note or an empty tag title is rejected and not retried.
Errors
| Response | What Jack & Jill does |
|---|---|
401, 403 |
Stops and marks the token invalid. |
429 with Retry-After |
Waits and does not count the wait as a failed attempt. |
409, 422 |
Stops that command. |
Timeout, connection failure, 5xx |
Retries after 1 second, 5 seconds, and 30 seconds, then stops that command. |
404 on one application |
Treats that application as not found. |
Wake URL
After you connect, Jill shows a URL and a signing secret. POST that URL when an application changes. The body may be empty. Sign the raw bytes with the wake secret as Jack-Signature. Jill then reads your API. If you never call the URL, Jill still polls.