Skip to main content

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.