Partner Assessment API
Quickstart
Two ways to run a validated JobCannon assessment from your own system. Both use the same key, the same 48 hire-eligible tests and the same result envelope — they differ only in who shows the questions to the candidate.
Hosted invites
POST /api/v1/invites
We host the assessment page, email the candidate, score the submission and hand back a number. You send one request and read one score. This is what an ATS bridge wants.
Embed
POST /api/v1/assessments
Headless. We hand you the raw questions and score the answers you send back; the candidate never leaves your product. You build the test UI.
Authentication
Every request carries your organisation key in an X-API-Key header. Keys look like jck_live_… and are issued per organisation. Keep the key server-side — never ship it in browser code.
X-API-Key: jck_live_your_key_here
Content-Type: application/jsonHosted invites
Send an assessment, get a score back
One call sends the invitation. JobCannon emails the candidate, hosts the test page, scores the submission and returns the number — either pushed to your callback_url or polled. Nothing candidate-facing has to exist on your side.
curl -X POST https://jobcannon.io/api/v1/invites \
-H "X-API-Key: jck_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"test_slug": "big-five",
"candidate_email": "alex@example.com",
"candidate_first_name": "Alex",
"external_id": "bullhorn-candidate-4242",
"callback_url": "https://your-app.example.com/hooks/jobcannon"
}'Response — 201
{
"session_id": "3f9a1c2e-…",
"assessment_url": "https://jobcannon.io/assessments/big-five?session=3f9a1c2e-…",
"test": "big-five",
"test_name": "Big Five Personality Test",
"status": "sent",
"locale": "en",
"candidate_email": "alex@example.com",
"external_id": "bullhorn-candidate-4242",
"callback_url": "https://your-app.example.com/hooks/jobcannon",
"sandbox": false,
"email_delivered": true,
"question_count": 50,
"duration_minutes": 15
}email_delivered: false means the link is valid but the mail bounced — assessment_url still works, so put it in front of the candidate yourself rather than resending.
Sending in the candidate's language
Add locale and the invitation email, the assessment page and the result page all arrive in that language — assessment_url comes back locale-prefixed, and the candidate never sees an English screen. 25 languages are available.
Omit it and the invite goes out in your organisation's own language. Set it per invite when your candidates don't share your dashboard language — an English-speaking recruiting team sending Spanish-speaking applicants is exactly the case it exists for. Region subtags are accepted and ignored (es-MX → es). A language we don't have is refused with 400 unsupported_locale rather than quietly sent in English, so a typo can't reach a candidate.
{
"test_slug": "culture-fit",
"candidate_email": "ana@example.com",
"locale": "es"
}
// → 201
{
"assessment_url": "https://jobcannon.io/es/assessments/culture-fit?session=…",
"test_name": "Encaje cultural",
"locale": "es"
}locale is echoed on the response because it may have come from your organisation setting rather than your request — it is the record of what the candidate was actually sent, and the language test_name is written in.
Rehearse it first: sandbox
Add "sandbox": true to any invite. No email is sent and the invite does not count toward your included volume, but assessment_url is real: open it, complete the test yourself, and watch the score arrive on your callback. That is the whole send → complete → score loop, on the same key you will use in production — no separate credentials to swap later, and no test candidate sitting in your live pipeline.
{ "test_slug": "eq", "candidate_email": "qa@your-app.example.com", "sandbox": true }
// → 201, "sandbox": true, "email_delivered": null (null = no mail was attempted)It has to be a JSON boolean. The string "true" — what a templating layer or a form-encoded bridge tends to produce — comes back as a 400 invalid_sandbox rather than quietly running for real. A rehearsal that mails a live candidate is worse than a request that fails loudly.
Getting the result
Push. If you supplied a callback_url, we POST this body to it once the candidate finishes, with an X-JobCannon-Event: assessment.completed header. Delivery retries with backoff; a non-2xx from you is treated as undelivered. The callback is an unsigned notification — treat it as a nudge and confirm through the read endpoint below, which is scoped to your own key.
{
"event": "assessment.completed",
"session_id": "3f9a1c2e-…",
"external_id": "bullhorn-candidate-4242",
"test": "big-five",
"status": "completed",
"candidate": { "email": "alex@example.com", "first_name": "Alex", "last_name": null },
"completed_at": "2026-07-29T14:02:11.480Z",
"score": 71,
"result_url": "https://jobcannon.io/results/…",
"result": { /* standardised result envelope */ }
}Poll. Or skip callback_url entirely and read the invite whenever you like. Needs the assessments:read scope.
curl https://jobcannon.io/api/v1/invites/3f9a1c2e-… \
-H "X-API-Key: jck_live_your_key_here"
{
"session_id": "3f9a1c2e-…",
"test": "big-five",
"status": "complete", // sent | started | complete | error
"candidate": { "email": "alex@example.com", "first_name": "Alex", "last_name": null },
"external_id": "bullhorn-candidate-4242",
"sandbox": false,
"sent_at": "2026-07-29T13:40:02.118Z",
"started_at": null, // often null — see note below
"completed_at": "2026-07-29T14:02:11.480Z",
"score": 71,
"result_url": "https://jobcannon.io/results/…",
"result": { /* standardised result envelope */ }
}score is null for instruments with no numeric dimensions — typologies such as MBTI place a candidate in a type rather than on a scale. Read result for those. Do not write a null score into a numeric field as 0: in a ranked column that is not a missing value, it is last place.
started_at is frequently null even on a finished invite — a candidate who opens the link and completes in one sitting produces one write, not two. Branch on status and completed_at, never on the presence of started_at.
Embed
Or run the test inside your own product
Three calls: start a session, collect the answers in your UI, submit them for server-side scoring. Results are attributed to your organisation. No redirect, no iframe — and no invitation email, because the candidate is already in front of you.
1. Start a session
Pick a test by slug (big-five, riasec, eq, …). Pass your own external_user_id to tie the result back to your applicant. You get a signed session_id and the full question set.
curl -X POST https://jobcannon.io/api/v1/assessments \
-H "X-API-Key: jck_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "test": "big-five", "locale": "en", "external_user_id": "candidate-4821" }'Response
{
"session_id": "eyJ0ZXN0Ijoi…", // signed token — pass to step 3
"test": "big-five",
"locale": "en",
"question_pool": null,
"total_questions": 50,
"questions": [
{
"index": 0,
"text": "I am the life of the party.",
"options": [
{ "value": 0, "label": "Very Inaccurate", "description": "Not at all like me" },
{ "value": 1, "label": "Moderately Inaccurate", "description": "Somewhat unlike me" },
{ "value": 2, "label": "Neither", "description": "Neither accurate nor inaccurate" },
{ "value": 3, "label": "Moderately Accurate", "description": "Somewhat like me" },
{ "value": 4, "label": "Very Accurate", "description": "Definitely like me" }
]
}
// … one object per question, up to total_questions
]
}2. Collect answers in your UI
Render the questions however you like inside your own flow. Record the option value the candidate picks for each question, in the order the questions were served. The result is a single array of integer option indices, one per question.
// answers[i] is the chosen option value for questions[i]
const answers = [3, 1, 4, 2, 0, 4, 3, /* … 50 values total … */]Need the questions again mid-session? GET /api/v1/assessments/{session_id}/questions re-serves the same set.
3. Submit for scoring
Post the answers back with the session_id from step 1. The array length must match total_questions. Scoring runs server-side and the result is persisted under your organisation.
curl -X POST https://jobcannon.io/api/v1/assessments/{session_id}/complete \
-H "X-API-Key: jck_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "answers": [3, 1, 4, 2, 0, 4, 3 /* …50… */], "external_user_id": "candidate-4821" }'Response (example)
{
"result_id": "b7f1c2a4-…",
"test": "big-five",
"top_result": "extraversion",
"scores": {
"openness": 72,
"conscientiousness": 64,
"extraversion": 81,
"agreeableness": 58,
"neuroticism": 39
},
"question_pool": null,
"external_user_id": "candidate-4821"
}Score keys and top_result vary by instrument (Big Five returns the five OCEAN dimensions; RIASEC returns the six Holland codes, and so on). The values above are illustrative.
Errors worth handling
401 invalid_api_key— key missing, malformed, or revoked.403 session_org_mismatch— the session belongs to a different key.422 answer_count_mismatch— the answers array length ≠ total_questions.429 rate_limited— back off and retry after the returned reset time.
Hosted invites add
400 test_not_sendable— that slug is not one of the hire-eligible assessments.400 invalid_callback_url— callback_url must be absolute https on a public host; loopback and private ranges are refused.400 unsupported_locale— we don't have that language. The response lists the ones we do; nothing is sent, so a typo never reaches a candidate in the wrong language.402 quota_exceeded— the included assessments for this period are spent and there is no active paid plan to invoice the extra against. One assessment is one unit, so an invite carrying three tests spends three. On a paid plan in good standing the invite is created instead, and assessments above the allowance are $2 each, billed after the period. Sandbox invites are exempt.404 invite_not_found— unknown id, or an invite belonging to another organisation. The two are deliberately indistinguishable.
Full reference & a key
The complete machine-readable spec — every endpoint, parameter, and schema — lives in the OpenAPI document. Import it straight into Postman or Swagger.