Mapping JobCannon Scores to Greenhouse Fields
What Greenhouse displays from `test_status` and the full JobCannon result envelope behind it: every field, what the numbers mean, why there is no overall score, and how to write it onto a scorecard.
When a candidate completes a JobCannon assessment, JobCannon marks the session complete and Greenhouse fetches the result from test_status. This page is the field-by-field reference for what comes back.
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 Greenhouse
JobCannon ships a native Greenhouse adapter. When the candidate finishes, JobCannon scores the result, stores the envelope, and calls back on the callback_url Greenhouse supplied when the assessment was sent. A failed callback retries with exponential backoff (10 min → 30 min → 2 h → 6 h, five attempts) and alerts our team.
The same result is readable at any time over the Assessment API — GET /api/v1/invites/{id} with your X-API-Key — and in your JobCannon org dashboard.
{
"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.
What Greenhouse receives from test_status
Greenhouse's own contract is four keys — partner_status, partner_profile_url, partner_score and metadata — where metadata must be a non-nested object whose values are all Javascript primitives. JobCannon answers in exactly that shape, so everything below is displayed on the test results panel without any mapping on your side:
{
"partner_status": "complete",
"partner_profile_url": "https://jobcannon.io/assessments/big-five/result/9f1c2d84-5b7e-4a30-9c11-3e6d8f0a72b5",
"metadata": {
"Result": "High Openness to Experience",
"Assessment": "Big Five Personality",
"Openness to Experience": 64,
"Conscientiousness": 58,
"Extraversion": 63,
"Agreeableness": 53,
"Neuroticism": 18,
"Scale": "0-100 percentile against JobCannon norms"
}
}- **
Result** isprimary_outcome.label— the result in words. It is the row to copy onto a scorecard. - **
partner_profile_url** is the permalink to the full report. It is what a hiring manager actually needs; put it beside the result. - **
Scale** names the unit for every number in that same object, because it is not the same unit for every test: Big Five publishes percentiles against JobCannon norms, everything else a 0-100 percentage of maximum. If more dimensions exist than the panel carries, this row says how many are not shown. - A dimension with no scoreable answer is left out, not sent as 0 — a zero on a hiring card reads as "scored lowest possible".
- **
partner_scoreis never sent.** There is no such number for any instrument. It used to carry the highest of the candidate's dimension scores, which names a different dimension for every candidate and is therefore not comparable between two of them. If your scorecard has a required numeric field, write one named dimension row frommetadataand label the field with that dimension's name.
The full envelope documented above is not what Greenhouse receives — it is available on the JobCannon partner API (/api/v1/...) if you want to store the whole result yourself.
Session state maps as sent → invitation delivered, started → candidate opened it, complete → scored and readable, cancelled → withdrawn.
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://developers.greenhouse.io/assessment-partner-api.html Support: partners@jobcannon.io