Mapping JobCannon Scores to Lever Fields
The JobCannon result envelope: every field, what the numbers mean, why there is no overall score, and how to get it alongside Lever while there is no native adapter.
This is the field-by-field reference for a JobCannon result. It is the same envelope on every delivery path — read it first, then see how to get it next to Lever below.
The result envelope
Every JobCannon result — every test, every delivery path — arrives in one shape. Here is a completed Big Five, rendered by the same normalizer that produces live results:
{
"test_type": "big-five",
"test_name": "Big Five Personality",
"completed_at": "2026-05-16T10:34:21.000Z",
"candidate": {
"email": "candidate@example.com",
"name": "Alex Morgan"
},
"primary_outcome": {
"code": "high-openness",
"label": "High Openness to Experience",
"summary": ""
},
"dimensions": [
{
"code": "openness",
"label": "Openness to Experience",
"score": 64,
"raw_score": 78,
"interpretation": "moderate"
},
{
"code": "conscientiousness",
"label": "Conscientiousness",
"score": 58,
"raw_score": 64,
"interpretation": "moderate"
},
{
"code": "extraversion",
"label": "Extraversion",
"score": 63,
"raw_score": 55,
"interpretation": "moderate"
},
{
"code": "agreeableness",
"label": "Agreeableness",
"score": 53,
"raw_score": 71,
"interpretation": "moderate"
},
{
"code": "neuroticism",
"label": "Neuroticism",
"score": 18,
"raw_score": 33,
"interpretation": "low"
}
],
"metadata": {
"top_dim": "openness",
"top_dim_key": "O",
"score_metric": "percentile",
"raw_score_metric": "percent_of_max",
"norms_n": 3472,
"norms_pull_date": "2026-08-07"
}
}| Field | Type | Meaning |
|---|---|---|
status | string | Session lifecycle: sent, started, complete, expired, error. |
score | null | Always null. There is no honest single number for these instruments — see "Why there is no overall score" below. The key stays on the wire so an integration that reads it does not break on a missing field. |
result_url | string | Permalink to the candidate's full report. This is what you put in front of a hiring manager. |
result | object | The standardised result envelope, below. |
result.test_type | string | Canonical assessment slug, dashed (big-five, disc, riasec). |
result.test_name | string | Human-readable assessment name. |
result.completed_at | string | ISO-8601 completion timestamp. |
result.candidate.email | string | null | Null for an anonymous result. |
result.candidate.name | string | null | Null when unknown. Never invented. |
result.primary_outcome.code | string | Machine-friendly dominant result (INTJ, Type-5, realistic). |
result.primary_outcome.label | string | The recruiter-facing result, in words. This is the field to render. |
result.primary_outcome.summary | string | 1–2 sentence narrative, or empty string when the test has no published copy. Never fabricated to fill the gap. |
result.dimensions[].code | string | Stable dimension identifier (openness, self_awareness). |
result.dimensions[].label | string | Column heading for a recruiter table. |
result.dimensions[].score | number | 0–100 within this test's own scale. See "What the number means". |
result.dimensions[].raw_score | number? | The original score when it differs from score — RIASEC raw sum, MBTI signed axis, Big Five percent-of-maximum. Omitted when it would just duplicate score. |
result.dimensions[].interpretation | string? | low / moderate / high, cut at 39 and 70. Omitted where a low/high spectrum is meaningless (an MBTI letter, an Enneagram type). |
result.metadata | object | Everything mode-specific. Safe to ignore entirely; safe to read by key when you know the test. |
What the number means
dimensions[].score is 0–100 within that test's own scale. Three consequences, all of them load-bearing:
- It is not a percentile unless
metadata.score_metricsayspercentile. Today Big Five is the only mode that publishes percentiles, and it says so in the envelope alongsidemetadata.norms_n(the sample the norms were drawn from) andmetadata.norms_pull_date. Every other test publishes a percentage of that test's maximum. A 78 on one is not the same claim as a 78 on the other. - Scores from two different tests are not comparable. Ranking candidate A's Big Five Openness against candidate B's DISC Dominance is arithmetic on two different rulers.
- There is no total. Ranking on one named dimension of one test is a defensible thing to do. Averaging the dimensions is not — the traits are not commensurate, and for Big Five a high Neuroticism and a high Conscientiousness do not add up to anything.
Why there is no overall score
score is null on every result JobCannon returns, for every test. This is a decision, not a gap.
An assessment like Big Five has no correct total. A candidate scoring O 78 / C 64 / E 55 / A 71 / N 33 has one profile; a candidate scoring O 33 / C 78 / E 71 / A 55 / N 64 has a different one; any single number that claims to rank them has thrown away the thing being measured. For a while JobCannon did emit one — the highest of the scales — and on the first live scorecard it rendered as "60" with no unit, which was that candidate's Openness percentile and nothing more. Two candidates whose peak fell on different traits looked comparable and were not.
What you get instead is primary_outcome.label — the result in words — plus dimensions[] and result_url. If your ATS scorecard requires a numeric field, write the score of one named dimension you have decided is relevant to the role, and label the field with that dimension's name. Do not synthesise a total.
How the result reaches Lever
There is no native Lever adapter yet. JobCannon registers three delivery paths today: a Greenhouse adapter, an Ashby adapter enabled on request, and the X-API-Key partner API. A Lever adapter is on the roadmap and is not something you can switch on — if you need one, email partners@jobcannon.io and tell us your volume, because that is what decides the order they get built in.
What works today with Lever is the API path: your integration (or a workflow tool sitting beside Lever) creates the invitation, and JobCannon calls you back when the candidate finishes. You write the result into Lever with Lever's own API.
Create an invitation — scope assessments:send.
curl -X POST https://jobcannon.io/api/v1/invites \
-H "X-API-Key: $JOBCANNON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"test_slug": "big-five",
"candidate_email": "candidate@example.com",
"candidate_first_name": "Alex",
"candidate_last_name": "Morgan",
"external_id": "your-lever-record-id",
"callback_url": "https://your-app.example.com/hooks/jobcannon",
"sandbox": true
}'Answers 201 with session_id, assessment_url, test, test_name, status, locale, candidate_email, sandbox and email_delivered.
"sandbox": true runs a full send → complete → score cycle that emails nobody and consumes no invitation quota; email_delivered comes back null rather than true/false, because no mail was attempted. Drop it when you go live — and keep it while you are testing, because the live path emails a real person. It must be a JSON boolean: the string "true" is rejected with invalid_sandbox rather than quietly treated as false.
Receive the result — JobCannon POSTs the payload below to your callback_url once the candidate finishes, with header X-JobCannon-Event: assessment.completed. callback_url must be absolute https on a public host; loopback and private ranges are rejected. Omit it and the invitation is poll-only: read it with GET /api/v1/invites/{id} (scope assessments:read), no more than once a minute.
{
"event": "assessment.completed",
"session_id": "9f1c2d84-5b7e-4a30-9c11-3e6d8f0a72b5",
"external_id": "your-ats-record-id",
"test": "big-five",
"status": "complete",
"candidate": {
"email": "candidate@example.com",
"first_name": "Alex",
"last_name": "Morgan"
},
"completed_at": "2026-05-16T10:34:21.000Z",
"score": null,
"result_url": "https://jobcannon.io/assessments/big-five/result/9f1c2d84-5b7e-4a30-9c11-3e6d8f0a72b5",
"result": {
"test_type": "big-five",
"test_name": "Big Five Personality",
"completed_at": "2026-05-16T10:34:21.000Z",
"candidate": {
"email": "candidate@example.com",
"name": "Alex Morgan"
},
"primary_outcome": {
"code": "high-openness",
"label": "High Openness to Experience",
"summary": ""
},
"dimensions": [
{
"code": "openness",
"label": "Openness to Experience",
"score": 64,
"raw_score": 78,
"interpretation": "moderate"
},
{
"code": "conscientiousness",
"label": "Conscientiousness",
"score": 58,
"raw_score": 64,
"interpretation": "moderate"
},
{
"code": "extraversion",
"label": "Extraversion",
"score": 63,
"raw_score": 55,
"interpretation": "moderate"
},
{
"code": "agreeableness",
"label": "Agreeableness",
"score": 53,
"raw_score": 71,
"interpretation": "moderate"
},
{
"code": "neuroticism",
"label": "Neuroticism",
"score": 18,
"raw_score": 33,
"interpretation": "low"
}
],
"metadata": {
"top_dim": "openness",
"top_dim_key": "O",
"score_metric": "percentile",
"raw_score_metric": "percent_of_max",
"norms_n": 3472,
"norms_pull_date": "2026-08-07"
}
}
}A second worked example
DISC, to show the same envelope carrying a different instrument. The shape does not change — only test_type, the dimension codes, and what primary_outcome names.
{
"test_type": "disc",
"test_name": "DISC Profile",
"completed_at": "2026-05-16T10:34:21.000Z",
"candidate": {
"email": "candidate@example.com",
"name": "Alex Morgan"
},
"primary_outcome": {
"code": "dominance",
"label": "Dominance",
"summary": ""
},
"dimensions": [
{
"code": "dominance",
"label": "Dominance",
"score": 72,
"interpretation": "high"
},
{
"code": "influence",
"label": "Influence",
"score": 45,
"interpretation": "moderate"
},
{
"code": "steadiness",
"label": "Steadiness",
"score": 38,
"interpretation": "low"
},
{
"code": "conscientiousness",
"label": "Conscientiousness",
"score": 61,
"interpretation": "moderate"
}
],
"metadata": {
"top_dim_key": "D"
}
}Parsing it
type Dimension = {
code: string
label: string
score: number
raw_score?: number
interpretation?: 'low' | 'moderate' | 'high'
}
type JobCannonEnvelope = {
test_type: string
test_name: string
completed_at: string
candidate: { email: string | null; name: string | null }
primary_outcome: { code: string; label: string; summary: string }
dimensions: Dimension[]
metadata?: Record<string, unknown>
}
type JobCannonCallback = {
event: 'assessment.completed'
session_id: string
external_id: string | null
test: string
status: string
score: null // always null — do not build a column on it
result_url: string | null
result: JobCannonEnvelope | null
}
export function toScorecard(payload: JobCannonCallback) {
const envelope = payload.result
if (!envelope) {
// status is 'sent' | 'started' | 'expired' | 'error' — nothing to write yet.
return null
}
const metric = envelope.metadata?.score_metric === 'percentile' ? 'percentile' : 'percent of max'
return {
headline: envelope.primary_outcome.label,
// Empty string when the test publishes no summary copy. Render the headline
// alone rather than substituting text of your own.
summary: envelope.primary_outcome.summary || null,
reportUrl: payload.result_url,
rows: envelope.dimensions.map((d) => ({
label: d.label,
// Label the unit. The same 78 means different things per test.
value: `${d.score} (${metric})`,
band: d.interpretation ?? null,
})),
}
}Integrity and social-desirability signals
JobCannon does compute two validity signals, and neither one travels in the result envelope:
- Integrity flag —
clean/review/concern, derived server-side from behaviour during the session (seelib/integrity-scoring). It is computed for any test. - Social-desirability estimate — a 0–100 score with a
low/moderate/elevated/highflag, derived from Big Five responses only.
Both are stored on the result row as validity_scores and are recruiter-only: they surface in your JobCannon org dashboard, in the candidate report, and in the results CSV export. The HubSpot integration writes the integrity flag to the jc_integrity_flag contact property. They are deliberately absent from the ATS payload — a candidate-visible channel is the wrong place for a signal about that candidate's honesty, and the flag is an input to a human review, not a filter to automate.
There is no faking_detected, no faking_risk and no social_desirability_scale field on any payload JobCannon sends. If you have code branching on one of those, it has never fired.
Writing it into Lever
JobCannon does not write to Lever; your integration does, with Lever's own API. What to write:
primary_outcome.labelandresult_urlas a note on the opportunity. That pair is the whole recruiter-facing result.- If you want structure, one custom field per dimension you have decided matters for the role, each labelled with the dimension's name and its unit.
What not to write: a single "JobCannon score" field. There is no such number, and a field with that name invites everyone downstream to rank on it.
Adverse-impact analysis
If you use an assessment as a screen, US federal guidance (29 CFR 1607, the Uniform Guidelines) expects you to analyse pass rates per component. JobCannon exposes that as an endpoint rather than a spreadsheet:
curl -H "X-API-Key: $JOBCANNON_API_KEY" \
"https://jobcannon.io/api/v1/orgs/adverse-impact?test=big-five&trait=C&threshold=60"Requires scope assessments:read. Three query params matter:
- **
test** — the assessment slug, dashed. Required. - **
threshold** — the cut-off, 0-100. Required, and there is no default: the cut-off is your hiring decision, not ours, and every number below it moves when the cut-off moves. - **
trait=<O|C|E|A|N>oronet=<code>** — which screen you are analysing. Exactly one; passing both is aconflicting_metricerror, because they are different screens.
It returns, for the candidates your org invited to that assessment, the share of each self-reported group that cleared your cut-off, plus the four-fifths ratio against the highest-passing group.
Two things it will not do, on purpose:
- It will not return a group too small to be anonymous. "women: n=2" in a pool of eleven names a person. Small groups come back suppressed.
- It does not call the result a selection rate. JobCannon never sees your hire decision. This is one component's pass rate — which is what the Guidelines ask you to analyse per component anyway.
Individual demographic answers are never returned by any endpoint, and are not visible to your org admins either. This aggregate is the only route by which that data leaves.
What we publish about the instruments
Reliability figures from our own sample are at /research/reliability, and Big Five has a full technical manual at /research/big-five-technical-manual.
That coverage is not the whole catalogue. We do not publish validity documentation for every one of the 42 hire-eligible tests, so check what exists for the specific instrument before you rely on it in a selection decision. If the documentation you need is missing, ask partners@jobcannon.io rather than assuming it is somewhere on the site.
Vendor docs: https://hire.lever.co/developer/documentation Support: partners@jobcannon.io