The assessment API reference for pre-employment testing: create assessments, invite candidates and read their results from your own systems. Everything the company dashboard does, the API does too.
The API is REST over HTTPS. Requests and replies are JSON, times are UTC in RFC 3339 (2026-10-03T12:00:00Z), and every ID starts with a prefix that says what it is.
POST/assessmentsCreate an assessment for the role.
POST/assessments/{id}/invitationsInvite a candidate and get their personal link.
EVENTresult.readyWe call your endpoint when they finish.
GET/invitations/{id}/resultRead the result with your key.
Sandbox
Keys that start with iqm_test_ work on sandbox data: sample questions, and no email ever reaches a candidate. Build and test your integration here.
Live
Keys that start with iqm_live_ work on live data. Every object carries livemode, so sandbox and live data can never mix.
Quick start
Five requests, from a new key to a candidate’s result, all in the sandbox. You need a terminal with curl.
Get a sandbox key
Sign in to the developer dashboard, open API keys and create a sandbox key. It starts with iqm_test_ and is shown once, so put it straight into your server’s secret store. Never put a key in a web page or an app: it is for servers only, and the API sends no CORS headers, so a browser cannot use it anyway.
Keep the key out of your shell history
read-rs IQM_KEY # paste the key; nothing is shownexport IQM_KEY
A 200 reply with "livemode": false means the key works on the sandbox. A 401 means the key was not sent or is not valid.
Create an assessment
The sandbox offers one test, sample-aptitude: 20 sample questions in four categories, with a 16-minute limit. Its questions are samples and its scores mean nothing; it is there to build and check an integration.
Send a new Idempotency-Key with every new request that creates something. If a request times out, send it again with the same key: you get the first reply, and nothing is created twice.
candidate_url is the candidate’s personal link, and it is shown only now. Open it in a browser and take the test: the practice questions first, then the timed part.
Until the test is finished the reply is 404 not_found. In production, don’t poll: add a webhook endpoint for result.ready and we tell you the moment a result exists. The full reply is described under Result.
The developer dashboard
You manage the integration yourself in the developer dashboard, open to your company’s admins and developers. Sign in with your work email, a 6-digit code we email you and your authenticator app. There is no password, and you never need to ask us for a key.
A switch at the top moves between the sandbox and live. Every new company can use the sandbox at once, and live mode opens after our team has checked the company.
Page
What you do there
Overview
A get-started checklist, today’s API calls, the error rate and response time, and an example request to copy.
API keys
Create a key, choose what it may do (its scopes) and when it expires, and revoke it. A key is shown once, and we keep only its fingerprint. Creating a live key needs a code from your authenticator app.
Webhooks
Add an endpoint, send it a test event, replay a delivery and rotate the signing secret, which is shown once. An endpoint that keeps failing is paused.
Logs & usage
Usage for the current month, and the last 30 days of API calls: time, request, status, key, duration and request ID. Logs never hold request bodies, query strings or personal data.
Test library
The shared tests your website may show, and how they look there: your brand or ours, and whether a visitor sees a score alone or a score with a percentile.
Embed on your website
The websites the embed may run on, which you add and remove yourself (at most 20 for each mode), and your public keys.
When you write to us about a call, quote its request ID from the log or from the error reply.
Keys and scopes
Send your key in the Authorization header of every request:
Header
Authorization: Bearer iqm_live_...
A key is shown once, when it is created, and we keep only its fingerprint. If it is lost, create a new one in the developer dashboard and revoke the old one.
Each key carries scopes, and each operation needs one. Give a key only the scopes its system uses.
A missing, unknown, revoked or expired key gets the same 401 unauthorized reply, whatever was wrong with it.
A valid key without the scope an operation needs gets 403 forbidden.
Keys are secret and for servers only. Never send one to a browser or put one in an app.
Scopes
Scope
Allows
Operations
catalogue:read
Read the ready-made tests, the categories and the role templates.
IDs are random strings with a prefix that says what they are. Treat them as opaque.
Prefix
Object
Prefix
Object
asm_
Assessment
bat_
Invitation batch
inv_
Invitation
whk_
Webhook endpoint
cnd_
Candidate
dlv_
Webhook delivery
res_
Result
evt_
Event
Repeat-safe requests
Every POST that creates something needs an Idempotency-Key header: any unique string of 8 to 255 characters, such as a UUID. Sending the same request again with the same key returns the first reply, marked Idempotent-Replayed: true, and creates nothing new. The same key with a different body is refused with idempotency_conflict. Keys are remembered for 24 hours.
Lists and pages
Lists come back one page at a time. Pass next_cursor back as cursor to get the next page; limit is 1 to 100 and defaults to 25.
Give each candidate your own external_id, for example their ID in your applicant tracking system, and find the invitation by it later. Invitations also take up to 20 metadata values of your own, returned unchanged.
Unknown fields
A request with a field the API doesn’t know is refused with validation_failed, so a typo never passes silently. Replies, on the other hand, can gain fields: ignore the ones you don’t know.
Errors
Every error has the same shape, RFC 9457 Problem Details (application/problem+json), with a stable code to act on and the request_id to quote to us. title and detail are for people and may change.
422 validation_failed
{"type":"about:blank","title":"Validation failed","status":422,"detail":"1 field is not acceptable.","code":"validation_failed","request_id":"req_7fae01f55a6b9aacd7a5e2f9","errors":[{"field":"sections[0].questions","code":"out_of_range","message":"Use between 5 and 40 questions per category."}]}
Code
Status
Meaning
bad_request
400
The request cannot be read, for example the body is not valid JSON.
unauthorized
401
The key is missing, unknown, revoked or expired. The reply is the same in every case.
forbidden
403
The key does not have the scope this operation needs.
not_found
404
No such object for this key. Another company’s objects always answer this, never forbidden.
method_not_allowed
405
The path exists, but not with this method.
conflict
409
The object is in a state that does not allow this, for example canceling a completed invitation.
idempotency_conflict
409
The Idempotency-Key was already used with a different body.
payload_too_large
413
The body is over 1 MB.
validation_failed
422
Some values are not acceptable. errors lists each field with a stable code.
limit_reached
403
A usage limit on your account is reached. limit says which one, how much is used and when it resets.
rate_limited
429
Too many requests. Wait for the number of seconds in Retry-After.
internal_error
500
Something failed on our side. Try again; if it keeps happening, send us the request_id.
unavailable
503
The API is briefly unavailable, for example during an update. Try again after Retry-After.
Rate limits
Limits are per key. Every reply to a valid key says where you stand:
Header
Meaning
RateLimit-Limit
Requests allowed in the current window for this key.
RateLimit-Remaining
Requests left in the current window.
RateLimit-Reset
Seconds until the window resets.
Over the limit, the reply is 429 rate_limited with Retry-After: wait that many seconds and try again. An address that keeps sending requests with a missing or invalid key is refused for up to 10 minutes, whatever key it sends.
Versions
This is version 1, and it only ever gains fields and endpoints. Nothing you build on it breaks. A change that would break something would come as /v2, announced ahead with Deprecation and Sunset headers on the old version.
Webhooks
Add an endpoint in the developer dashboard or with Add a webhook endpoint, and choose its events. The address must use HTTPS and be on the public internet. Its signing secret is shown once, when the endpoint is created.
Event
Sent when
result.ready
A result is ready. A candidate finished (or their time ran out) and the result can be read.
When the test was a student’s seat in a campus drive, data also carries drive_id, the
drive’s ID. It is left out for every other result. The roll number, the email address and
every score are never part of an event.
invitation.opened
A candidate opened their link. The first time the link is opened.
session.started
A candidate started the test. The candidate passed the consent and practice steps and started the timed test.
invitation.expired
An invitation expired unused. The deadline passed before the candidate started.
invitation.bounced
The invitation email bounced. Only for invitations we emailed. Check the address and re-issue the link.
usage.threshold_reached
A usage counter reached 80% or 100% of its limit. The event data carries the metric and the threshold; read /usage for the figures.
shared_session.completed
A shared session is over. The person finished (or their time ran out) and the result can be read with getSharedSession.
To change a secret, replace it: for 24 hours each delivery is signed with both the old and the new secret, so your receiver can switch over without missing an event.
Candidate links
Every invitation gives the candidate a personal link, candidate_url. It works for that candidate and that assessment only.
It is shown once. The reply that creates the invitation carries it, and so does Re-issue the candidate’s link, which also stops the old link working. We keep only a hash of it, so treat it like a password: never log it or send it to analytics.
You choose who sends it. With "send_email": true we email the candidate. With false we send nothing, and your system delivers the link by email, text message or your careers site.
Deadlines and extra time.expires_in_days sets the deadline (1 to 60 days). Move the deadline or give extra time later; extra_time_percent gives 25%, 50% or 100% more time as an accommodation, and each change is written to the audit log.
After the test. Set the assessment’s redirect_url to send candidates back to your own site when they finish.
On our page, always. Hiring tests run on our test page, from the personal link: that is where the server clock, the copy block and the integrity signals live. To run a test inside your own website, see shared tests.
Campus drives. A student’s seat in a campus drive is an invitation too, made without an email and opened only from the drive’s sign-in page, never from a link. Its invitation, its result and its result.ready event carry the drive’s drive_id; every other invitation and result has null there.
Shared tests for your website
Job boards, career sites, universities, publishers and coaches can show our tests to their own visitors. Shared tests come from a separate pool we set aside for sharing, never from the questions we keep for hiring, and the answer key never leaves our server whichever way you choose. A company that signs up to show tests on its website, rather than to hire, gets the developer dashboard and the Test library, with no hiring features.
The test library
Pick the shared tests your website shows in the developer dashboard, and how they look there: your brand or ours, and whether a visitor sees a score alone or a score with a percentile. In the sandbox the library holds sample-shared-reasoning, with sample questions; live shared tests come with an account that includes them.
The question API, for your own design
Your server starts a session, shows one unit at a time in your own design, and sends the answers back; we score them. A unit is one question, or a set of questions that share a passage, a table or a chart, and it never carries the answer key. Four operations, for a key with the “Use shared tests” permission (scope shared:use):
Start a shared session for that test, with your own reference for the visitor. The reply carries the first unit and the time left.
Answer the unit on screen, and the reply carries the next one. Sending the same answers again is safe.
When the visitor finishes, or the time runs out, we send shared_session.completed to your webhook endpoint, and Get a shared session returns the result.
A session is one person on your website, not a candidate: it carries your reference and never a name or an email address. Results from your own design are marked as such, because the copy block and the integrity signals run only on our page.
Embed on your website
One script with a public key runs our test page inside yours. Public keys start with iqm_pk_live_ or iqm_pk_test_ and are safe in a page: they can only start a shared test, and only on the websites you list in the developer dashboard. You add and remove those websites yourself, at once, so a copied code cannot run anywhere else. The clock and the scoring stay on our server, each visitor sees their own result, and you read every result through the API.
{"object":"list","data":[{"id":"numerical-reasoning","object":"test","name":"Numerical Reasoning","description":"Working with numbers, tables and charts: percentages, rates, averages and data.","categories":[{"category":"numerical_reasoning","questions":18}],"questions":18,"time_limit_minutes":20,"languages":["en"],"reliability":null,"comparison_group":null}],"has_more":false,"next_cursor":null}
Get one ready-made test
GET/tests/{test_id}
What the test measures, its length, languages, reliability and comparison group.
{"id":"numerical-reasoning","object":"test","name":"Numerical Reasoning","description":"Working with numbers, tables and charts: percentages, rates, averages and data.","categories":[{"category":"numerical_reasoning","questions":18}],"questions":18,"time_limit_minutes":20,"languages":["en"],"reliability":null,"comparison_group":null}
List the categories for custom tests
GET/categories
Each category with its topics, question formats, languages and the builder’s limits.
{"object":"list","data":[{"id":"numerical_reasoning","object":"category","name":"Numerical reasoning","domain":"Quantitative & data reasoning","topics":[{"id":"arithmetic","name":"Arithmetic"},{"id":"percentages","name":"Percentages"},{"id":"rates_and_time","name":"Rates and time"},{"id":"averages","name":"Averages"},{"id":"data_tables","name":"Data tables"}],"min_questions":5,"max_questions":40,"recommended_questions":8,"seconds_per_question":60,"difficulties":["easy","medium","hard","mixed"],"languages":["en"]}],"has_more":false,"next_cursor":null}
List the role templates
GET/role-templates
Ready combinations of tests for common roles, a quick start for an assessment.
{"object":"list","data":[{"id":"asm_7Q2mX8aB1cD3eF5gH7jK9pL0","object":"assessment","name":"Operations Analyst - Oct","status":"active","purpose":"hiring","version":1,"test":null,"role_template":null,"sections":[{"category":"numerical_reasoning","name":"Numerical reasoning","questions":10,"difficulty":"mixed"},{"category":"logical_reasoning","name":"Logical reasoning","questions":10,"difficulty":"medium"},{"category":"attention_to_detail","name":"Attention to detail","questions":15,"difficulty":"mixed"}],"time_limit_minutes":35,"settings":{"practice_questions":true,"shuffle_sections":false,"language":"en","expires_in_days":7,"retake_after_days":null,"redirect_url":null},"warnings":[],"counts":{"invited":41,"started":33,"completed":30},"livemode":true,"created_at":"2026-10-01T09:30:00Z","updated_at":"2026-10-01T09:30:00Z"}],"has_more":false,"next_cursor":null}
Create an assessment
POST/assessments
Start from exactly one of: a ready-made test, a role_template, or your own sections (a custom test). The reply lists the builder’s warnings, for example a category with too few questions for a reliable sub-score, or an assessment longer than 40 minutes.
A section is one of our categories (category) or one of your own skills (skill), the questions your company wrote or uploaded in the dashboard’s Questions area: each candidate gets a random set of the skill’s ready questions. Sections of your own skills need the plan’s own-questions feature as well as custom tests (403 without it), and use your own active skills only: another company’s skill, an archived one and one that does not exist are all unknown_skill. The dashboard’s builder and this endpoint share every rule.
{"id":"asm_7Q2mX8aB1cD3eF5gH7jK9pL0","object":"assessment","name":"Operations Analyst - Oct","status":"active","purpose":"hiring","version":1,"test":null,"role_template":null,"sections":[{"category":"numerical_reasoning","name":"Numerical reasoning","questions":10,"difficulty":"mixed"},{"category":"logical_reasoning","name":"Logical reasoning","questions":10,"difficulty":"medium"},{"category":"attention_to_detail","name":"Attention to detail","questions":15,"difficulty":"mixed"}],"time_limit_minutes":35,"settings":{"practice_questions":true,"shuffle_sections":false,"language":"en","expires_in_days":7,"retake_after_days":null,"redirect_url":null},"warnings":[],"counts":{"invited":41,"started":33,"completed":30},"livemode":true,"created_at":"2026-10-01T09:30:00Z","updated_at":"2026-10-01T09:30:00Z"}
Change an assessment
PATCH/assessments/{assessment_id}
Change the name, the status or the settings. Changing sections or time_limit_minutes creates a new version: candidates who have started keep the version they started, and results show which version each candidate took. An invitation whose test has not begun takes the newest version when it begins.
Only what the request names changes: settings not named keep their values, and leaving out time_limit_minutes keeps the current time (the warnings say when it no longer fits the questions). active and closed can go either way; either can be archived. An archived assessment no longer changes (conflict). Every change is written to the audit log.
One of your own active skills, the group of questions your company wrote or uploaded in the dashboard’s Questions area (unknown_skill for any other ID). Each candidate gets a random set of the skill’s ready questions, at most as many as it holds at that difficulty. No topics. Needs the plan’s own-questions feature.
sections[].questionsrequired
integer
How many questions the candidate gets from this section. A category takes 5 to 40 (and the category’s own limits, see /categories); a skill takes 1 to 40, at most the questions it holds ready. 1 to 40.
sections[].difficulty
string
mixed spreads questions across easy, medium and hard. One of easy, medium, hard, mixed.
sections[].topics
array of string
Only these topics of the category (topic IDs from /categories). Leave out for all. Scores are still reported per category, never per topic. Not for a skill section. Up to 10 items.
sections[].time_limit_minutes
integer
A time for this section alone. Leave out to use the assessment’s total time. Not available yet (not_available). At least 1.
time_limit_minutes
integer
1 to 240.
settings
object
settings.practice_questions
boolean
A few practice questions before the timed part, one of each kind of question the test uses first, on the test screen itself: candidates try each of its controls before the clock starts. They are never scored, have no clock, and never show whether an answer is right (the test never does either). Default true.
settings.shuffle_sections
boolean
Default false.
settings.language
string
Only en is available yet. Default "en".
settings.expires_in_days
integer
The default deadline of new invitations. 1 to 60. Default 7.
settings.retake_after_days
integer or null
null means no retakes. Retakes are not available yet; a number is refused with not_available. At least 1.
settings.redirect_url
string (uri) or null
Where candidates go after finishing (HTTPS).
settings.allow_going_back
boolean
Candidates can go back to earlier questions, and change their answers, until they finish or the time runs out: Previous, the question map and the review screen. With false the test goes forward only: Next, and an answer can change only while its question is on screen. Each attempt keeps the rules it started with, so a change here never changes a test in progress. Compare results within one assessment: tests taken under different rules are not strictly comparable. Default true.
settings.allow_marking
boolean
Candidates can mark questions to come back to (the mark is only theirs: never scored, never shown to you). Takes effect only with allow_going_back. Default true.
Returns candidate_url, the candidate’s personal link. It is shown only now, and again only if the link is re-issued with resend: we store the link’s hash, not the link. With send_email: false, we send nothing and your system delivers the link.
A unique string per operation, such as a UUID. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
Field
Type
Description
candidaterequired
object
candidate.emailrequired
string (email)
Up to 190 characters.
candidate.first_name
string or null
Up to 100 characters.
candidate.last_name
string or null
Up to 100 characters.
candidate.external_id
string or null
Your own ID, for example the ATS candidate ID. null, or left out, means none. Up to 191 characters.
send_email
boolean
false means your system delivers the link. Default true.
expires_in_days
integer
Leave out for the assessment’s default. 1 to 60.
extra_time_percent
integer
Extra time as an accommodation. The change is written to the audit log. One of 0, 25, 50, 100. Default 0.
metadata
object
Up to 20 string values of your own, returned as they are. Keys start with a letter and have at most 40 letters, digits, “_”, “.” or “-“. Up to 20 entries.
Reply
201 The invitation, with the candidate’s link. Returns Invitation.
A unique string per operation, such as a UUID. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
Field
Type
Description
invitationsrequired
array of object
Up to 500 items.
invitations[].candidaterequired
object
invitations[].candidate.emailrequired
string (email)
Up to 190 characters.
invitations[].candidate.first_name
string or null
Up to 100 characters.
invitations[].candidate.last_name
string or null
Up to 100 characters.
invitations[].candidate.external_id
string or null
Your own ID, for example the ATS candidate ID. null, or left out, means none. Up to 191 characters.
invitations[].send_email
boolean
false means your system delivers the link. Default true.
invitations[].expires_in_days
integer
Leave out for the assessment’s default. 1 to 60.
invitations[].extra_time_percent
integer
Extra time as an accommodation. The change is written to the audit log. One of 0, 25, 50, 100. Default 0.
invitations[].metadata
object
Up to 20 string values of your own, returned as they are. Keys start with a letter and have at most 40 letters, digits, “_”, “.” or “-“. Up to 20 entries.
Reply
202 The batch was accepted and queued. Returns Invitation batch.
The batch’s progress and, once done, one line per candidate: the invitation (with candidate_url) or the reason it failed. The links stay readable for 24 hours.
{"id":"bat_1Qw3Er5Ty7Ui9Op1As3Df5Gh","object":"invitation_batch","assessment_id":"asm_7Q2mX8aB1cD3eF5gH7jK9pL0","status":"done","total":2,"succeeded":1,"failed":1,"created_at":"2026-10-01T09:40:00Z","lines":[{"index":0,"invitation":{"id":"inv_8KD2wT4yU6iO8pA0sD2fG4hJ","object":"invitation","assessment_id":"asm_7Q2mX8aB1cD3eF5gH7jK9pL0","assessment_version":1,"candidate":{"id":"cnd_3Nx5Bv7Mc9Lk1Jh3Gf5Ds7Aq","object":"candidate","email":"sam@example.com","first_name":"Sam","last_name":null,"external_id":"ats-9921","created_at":"2026-10-01T09:31:00Z"},"status":"invited","expires_at":"2026-10-08T09:31:00Z","extra_time_percent":0,"opened_at":null,"started_at":null,"completed_at":null,"result_id":null,"metadata":{},"livemode":true,"created_at":"2026-10-01T09:31:00Z","candidate_url":"https://iqmetrics.org/assess/c/9f1bQ7xK2mN4pR6sT8vW0yZ3aB5cD7eF9gH1jK3mN5p"}},{"index":1,"error":{"type":"about:blank","title":"Validation failed","status":422,"detail":"1 field is not acceptable.","code":"validation_failed","request_id":"req_2c9d41e07b8a5f3c6d1e9a04","errors":[{"field":"candidate.email","code":"duplicate","message":"alex@example.com already has an open invitation to this assessment."}]}}]}
Get an invitation
GET/invitations/{invitation_id}
The invitation’s status and dates. The link itself is never shown again.
Issues a new link and returns it in candidate_url. The old link stops working at once. Sends the invitation email again unless send_email is false. Only for invitations that have not been completed, canceled or expired.
Move the invitation’s deadline, or give extra time
POST/invitations/{invitation_id}/extend
Moves expires_at (an expired invitation that was never started opens again), and/or sets the candidate’s extra time for accommodations. Every change is written to the audit log.
{"object":"list","data":[{"id":"res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl","object":"result","invitation_id":"inv_8KD2wT4yU6iO8pA0sD2fG4hJ","assessment_id":"asm_7Q2mX8aB1cD3eF5gH7jK9pL0","assessment_version":1,"candidate_id":"cnd_3Nx5Bv7Mc9Lk1Jh3Gf5Ds7Aq","score":{"correct":17,"total":24},"categories":[{"category":"numerical_reasoning","name":"Numerical reasoning","correct":6,"total":8,"source":"iqmetrics","band":"above_typical"},{"category":"verbal_reasoning","name":"Verbal reasoning","correct":5,"total":8,"source":"iqmetrics","band":"typical"},{"category":"abstract_reasoning","name":"Abstract reasoning","correct":6,"total":8,"source":"iqmetrics","band":"above_typical"}],"band":"above_typical","rank":{"position":3,"of":41},"percentile":{"value":64,"group":"All candidates - General Aptitude","size":1240,"provisional":false},"time_used_seconds":801,"time_limit_seconds":900,"finished":true,"signals":{"tab_switches":1,"fullscreen_exits":0,"paste_attempts":0,"copy_attempts":0,"device_changes":0,"fast_answers":0},"report_pdf":{"url":"https://iqmetrics.org/api/v1/results/res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl/report.pdf?expires=1791036900&signature=org_2Kd4Fh6Jl8Zx0Cv2Bn4Mq6Wr.live.Wq3Rt5Yu7Io9Pa1Sd3Fg5Hj7Kl9Zx1Cv3Bn5Mq7Wr9T","expires_at":"2026-10-03T13:15:00Z"},"livemode":true,"completed_at":"2026-10-03T12:15:00Z"}],"has_more":false,"next_cursor":null}
Download a result’s PDF report
GET/results/{result_id}/report.pdf
The hiring manager’s report of one result, as a PDF: the summary, each category, an interview guide for the candidate’s weaker categories, and how to read the score. Use the link in the Result’s report_pdf as it is: it carries its own permission, so it needs no key, and it works for an hour. Treat it like a password, and fetch the result again for a new link. A link that is wrong or has expired, or whose result is deleted, gets 404 not_found, whatever the reason. Downloads are limited per address, and each one is written to the company’s audit log.
Public: no key needed.
Parameters
Name
In
Type
Description
result_idrequired
path
string
The result’s ID.
expiresrequired
query
integer
When the link stops working, in Unix seconds. Part of the link. At least 0.
signaturerequired
query
string
The link’s signature. Part of the link.
Reply
200 The PDF report, as a file to save. No content.
Deletes the candidate and everything linked to them: invitations, attempts, answers, results, reports and delivery logs. It cannot be undone. The deletion itself is recorded in the audit log without personal data.
The URL must use HTTPS and reach the public internet: private, loopback, link-local and cloud-metadata addresses are refused. The reply carries the signing secret, shown once.
Returns a new secret, shown once. For 24 hours each delivery is signed with both the old and the new secret (two v1, signatures), so your receiver can switch without missing events.
Queues a signed webhook.test event for the endpoint, even a paused one, and returns the delivery; the worker sends it within a minute. At most 20 tests and replays a minute per endpoint.
Sends the same event again as a new delivery, with the same webhook-id, so a receiver that already has it can ignore it. It goes even to a paused endpoint. Events are kept for 30 days; an older one is not_found.
Every counter the plan limits, with what is used, what is reserved by open invitations, and when it resets. A counter without a limit shows limit: null.
The shared tests this key’s company may use in this mode. Their IDs start a shared session; they cannot be used for hiring assessments, and hiring tests cannot start a shared session.
{"object":"list","data":[{"id":"sample-shared-reasoning","object":"shared_test","name":"Sample Shared Reasoning","description":"The sandbox version of a shared test: numerical and logical reasoning, with sample questions.","categories":[{"category":"numerical_reasoning","questions":6},{"category":"logical_reasoning","questions":6}],"questions":12,"time_limit_minutes":12,"languages":["en"],"reliability":null,"comparison_group":null}],"has_more":false,"next_cursor":null}
Start a shared session
POST/shared-sessions
Starts a shared test for one person on your website. The clock starts at once: the reply carries the first unit (a question on its own, or a set with its shared passage, table or picture) and the deadline. Show the unit in your own design and send its answers with submitSharedAnswers. Pass your own reference to match the session to your visitor; never put personal data in it. Each key may start 120 sessions a minute.
{"id":"ssn_4Fg6Hj8Kl0Zx2Cv4Bn6Mq8Wr","object":"shared_session","livemode":false,"test_id":"sample-shared-reasoning","reference":"visit-20261003-0042","status":"active","created_at":"2026-10-03T12:20:00Z","deadline":"2026-10-03T12:32:05Z","seconds_left":725,"time_limit_seconds":725,"answered":0,"total":13,"unit":{"type":"question","seq":1,"stimulus":null,"questions":[{"seq":1,"category":"Numerical reasoning","format":"single_choice","stem":"A tank holds 80 litres. It is filled by a further 25%. How many litres does it hold now?","picture":null,"options":[{"id":"a","text":"95"},{"id":"b","text":"100"},{"id":"c","text":"105"},{"id":"d","text":"120"}],"decimals":null,"unit":null,"answer":null}]},"result":null}
Get a shared session
GET/shared-sessions/{session_id}
Where the session stands: the unit on screen and the time left while it runs, or the result once it is over. A session whose time ran out is scored when it is read, and by us shortly after its deadline in any case.
{"id":"ssn_4Fg6Hj8Kl0Zx2Cv4Bn6Mq8Wr","object":"shared_session","livemode":false,"test_id":"sample-shared-reasoning","reference":"visit-20261003-0042","status":"active","created_at":"2026-10-03T12:20:00Z","deadline":"2026-10-03T12:32:05Z","seconds_left":725,"time_limit_seconds":725,"answered":0,"total":13,"unit":{"type":"question","seq":1,"stimulus":null,"questions":[{"seq":1,"category":"Numerical reasoning","format":"single_choice","stem":"A tank holds 80 litres. It is filled by a further 25%. How many litres does it hold now?","picture":null,"options":[{"id":"a","text":"95"},{"id":"b","text":"100"},{"id":"c","text":"105"},{"id":"d","text":"120"}],"decimals":null,"unit":null,"answer":null}]},"result":null}
Answer the unit on screen
POST/shared-sessions/{session_id}/answers
The answers to the unit on screen, all at once: name the unit by its seq, and give each of its questions at most once, with an option’s id, the typed number, or null to skip. A question left out counts as not correct. The reply carries the next unit, or the result when the test is over. Sending the answers to a unit already answered changes nothing and returns the session as it stands, so a retry after a lost reply is safe; a later unit is refused with 409 conflict.
{"id":"ssn_4Fg6Hj8Kl0Zx2Cv4Bn6Mq8Wr","object":"shared_session","livemode":false,"test_id":"sample-shared-reasoning","reference":"visit-20261003-0042","status":"active","created_at":"2026-10-03T12:20:00Z","deadline":"2026-10-03T12:32:05Z","seconds_left":725,"time_limit_seconds":725,"answered":0,"total":13,"unit":{"type":"question","seq":1,"stimulus":null,"questions":[{"seq":1,"category":"Numerical reasoning","format":"single_choice","stem":"A tank holds 80 litres. It is filled by a further 25%. How many litres does it hold now?","picture":null,"options":[{"id":"a","text":"95"},{"id":"b","text":"100"},{"id":"c","text":"105"},{"id":"d","text":"120"}],"decimals":null,"unit":null,"answer":null}]},"result":null}
Objects
The objects the API returns, field by field.
Assessment
A company’s set-up for a role: a ready-made test or custom sections, the time limit and the settings. Changing the sections or the time makes a new version.
Field
Type
Description
idrequired
string
objectrequired
string
Always assessment.
namerequired
string
statusrequired
string
draft (not yet usable), active (invitations allowed), closed (no new invitations; open ones can finish), archived. One of draft, active, closed, archived.
purposerequired
string
hiring (results go to the company) or development (internal testing; feedback and retakes can be switched on). One of hiring, development.
versionrequired
integer
Goes up when sections or time change. At least 1.
test
string or null
The ready-made test it was made from, if any.
role_template
string or null
The role template it was made from, if any.
sectionsrequired
array of object
sections[].category
string
A category ID from /categories. Present for one of our categories.
sections[].skill
string
A skill ID. Present for one of your own skills.
sections[].namerequired
string
sections[].questionsrequired
integer
1 to 40.
sections[].difficultyrequired
string
mixed spreads questions across easy, medium and hard. One of easy, medium, hard, mixed.
sections[].topics
array of string
sections[].time_limit_minutes
integer
At least 1.
time_limit_minutesrequired
integer
settingsrequired
object
settings.practice_questions
boolean
A few practice questions before the timed part, one of each kind of question the test uses first, on the test screen itself: candidates try each of its controls before the clock starts. They are never scored, have no clock, and never show whether an answer is right (the test never does either). Default true.
settings.shuffle_sections
boolean
Default false.
settings.language
string
Only en is available yet. Default "en".
settings.expires_in_days
integer
The default deadline of new invitations. 1 to 60. Default 7.
settings.retake_after_days
integer or null
null means no retakes. Retakes are not available yet; a number is refused with not_available. At least 1.
settings.redirect_url
string (uri) or null
Where candidates go after finishing (HTTPS).
settings.allow_going_back
boolean
Candidates can go back to earlier questions, and change their answers, until they finish or the time runs out: Previous, the question map and the review screen. With false the test goes forward only: Next, and an answer can change only while its question is on screen. Each attempt keeps the rules it started with, so a change here never changes a test in progress. Compare results within one assessment: tests taken under different rules are not strictly comparable. Default true.
settings.allow_marking
boolean
Candidates can mark questions to come back to (the mark is only theirs: never scored, never shown to you). Takes effect only with allow_going_back. Default true.
warningsrequired
array of object
warnings[].coderequired
string
draws_most: a section of your own skill draws more than 80% of the questions the skill holds ready, so people who take the test twice will see most of them again. One of few_questions, long_assessment, tight_timing, narrow_topics, draws_most.
warnings[].messagerequired
string
countsrequired
object
counts.invitedrequired
integer
counts.startedrequired
integer
counts.completedrequired
integer
livemoderequired
boolean
created_atrequired
string (date-time)
updated_atrequired
string (date-time)
Example
Assessment
{"id":"asm_7Q2mX8aB1cD3eF5gH7jK9pL0","object":"assessment","name":"Operations Analyst - Oct","status":"active","purpose":"hiring","version":1,"test":null,"role_template":null,"sections":[{"category":"numerical_reasoning","name":"Numerical reasoning","questions":10,"difficulty":"mixed"},{"category":"logical_reasoning","name":"Logical reasoning","questions":10,"difficulty":"medium"},{"category":"attention_to_detail","name":"Attention to detail","questions":15,"difficulty":"mixed"}],"time_limit_minutes":35,"settings":{"practice_questions":true,"shuffle_sections":false,"language":"en","expires_in_days":7,"retake_after_days":null,"redirect_url":null},"warnings":[],"counts":{"invited":41,"started":33,"completed":30},"livemode":true,"created_at":"2026-10-01T09:30:00Z","updated_at":"2026-10-01T09:30:00Z"}
Invitation
One candidate’s link to one assessment, and where it stands. The link itself, candidate_url, appears only when the invitation is created or its link is re-issued.
One of invited, opened, started, completed, expired, cancelled, bounced.
candidate_url
string (uri)
Only when the invitation is created or its link is re-issued. Treat it like a password.
expires_atrequired
string (date-time)
extra_time_percentrequired
integer
opened_at
string (date-time) or null
started_at
string (date-time) or null
completed_at
string (date-time) or null
result_id
string or null
metadatarequired
object
source
string
Who made the invitation: the company (through the API, the dashboard or a CSV list), or the candidate, through the assessment’s public link. One of company, public_link.
drive_id
string or null
The campus drive this seat belongs to, or null for every other invitation.
What a candidate achieved: the score, the categories, rank, percentile, time and integrity signals. It never contains an IQ number.
Field
Type
Description
idrequired
string
objectrequired
string
Always result.
invitation_idrequired
string
assessment_idrequired
string
assessment_versionrequired
integer
candidate_idrequired
string
drive_id
string or null
The campus drive of the seat that took the test, or null for every other result.
scorerequired
object
score.correctrequired
integer
score.totalrequired
integer
categoriesrequired
array of object
categories[].categoryrequired
string
A category ID, or, for a section of your own skill, the skill’s ID.
categories[].namerequired
string
categories[].correctrequired
integer
categories[].totalrequired
integer
categories[].sourcerequired
string
Where the section’s questions came from: iqmetrics for one of our categories, company for one of your own skills. A result from before your own questions existed says iqmetrics. One of iqmetrics, company.
categories[].bandrequired
string or null
A band against the test’s comparison group, or null while the test has none. Bands come only from measured data, so a new test or a custom test starts without them. One of below_typical, typical, above_typical, well_above.
bandrequired
string or null
A band against the test’s comparison group, or null while the test has none. Bands come only from measured data, so a new test or a custom test starts without them. One of below_typical, typical, above_typical, well_above.
rankrequired
object
Among the candidates of the same assessment, worked out when read.
rank.positionrequired
integer
rank.ofrequired
integer
percentilerequired
object or null
Against a named comparison group, or null when none exists yet (for example a new custom test).
percentile.valuerequired
integer
0 to 100.
percentile.grouprequired
string
percentile.sizerequired
integer
percentile.provisionalrequired
boolean
time_used_secondsrequired
integer
time_limit_secondsrequired
integer
The candidate’s time limit: the assessment’s, stretched by the candidate’s extra time, plus the time for any unscored trial questions (each gets the test’s average time per question, so they never take time from the questions that count).
finishedrequired
boolean
false when the time ran out.
signalsrequired
object
Integrity signals during the timed part. Signals, not proof: they never change a score, and nothing should decide about a candidate on them alone. tab_switches the candidate left the page (another tab, window or app); fullscreen_exits left full screen, where the browser offered it; paste_attempts and copy_attempts tried to paste or copy, which the page blocks; device_changes continued from another device or network (the link opened again); fast_answers answered within 3 seconds of the question appearing. Since ADR 0041 (0 for results completed before): focus_losses left the test window while the page stayed on screen (another app or screen); away_seconds the seconds away from the test window in all, hidden or out of focus; right_clicks right-clicks on the test page; screenshot_keys PrintScreen and the screenshot shortcuts a browser can see (most operating-system screenshots never reach a web page, so this can be low, never too high); shortcut_keys the keys to copy, cut, paste, select all, print, save, find or view the source (the category only, never which key or anything typed); network_changes the same open page continued from another network, such as Wi-Fi to mobile data; answer_changes answers changed after the first (a clear then another answer counts once). Changing an answer is allowed until the finish, so answer_changes is no signal of concern by itself. These seven are always sent; they are optional here only so that older clients and examples stay valid.
signals.tab_switchesrequired
integer
At least 0.
signals.fullscreen_exitsrequired
integer
At least 0.
signals.paste_attemptsrequired
integer
At least 0.
signals.copy_attemptsrequired
integer
At least 0.
signals.focus_losses
integer
At least 0.
signals.right_clicks
integer
At least 0.
signals.screenshot_keys
integer
At least 0.
signals.shortcut_keys
integer
At least 0.
signals.device_changesrequired
integer
At least 0.
signals.fast_answersrequired
integer
At least 0.
signals.network_changes
integer
At least 0.
signals.away_seconds
integer
Seconds, at most 86400. At least 0.
signals.answer_changes
integer
At least 0.
report_pdfrequired
object or null
A signed link to the hiring manager’s PDF report (GET /results/{result_id}/report.pdf), made new on every reply and valid for one hour. It needs no key, so treat it like a password. Null only when no link can be made.
report_pdf.urlrequired
string (uri)
report_pdf.expires_atrequired
string (date-time)
livemoderequired
boolean
completed_atrequired
string (date-time)
Example
Result
{"id":"res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl","object":"result","invitation_id":"inv_8KD2wT4yU6iO8pA0sD2fG4hJ","assessment_id":"asm_7Q2mX8aB1cD3eF5gH7jK9pL0","assessment_version":1,"candidate_id":"cnd_3Nx5Bv7Mc9Lk1Jh3Gf5Ds7Aq","score":{"correct":17,"total":24},"categories":[{"category":"numerical_reasoning","name":"Numerical reasoning","correct":6,"total":8,"source":"iqmetrics","band":"above_typical"},{"category":"verbal_reasoning","name":"Verbal reasoning","correct":5,"total":8,"source":"iqmetrics","band":"typical"},{"category":"abstract_reasoning","name":"Abstract reasoning","correct":6,"total":8,"source":"iqmetrics","band":"above_typical"}],"band":"above_typical","rank":{"position":3,"of":41},"percentile":{"value":64,"group":"All candidates - General Aptitude","size":1240,"provisional":false},"time_used_seconds":801,"time_limit_seconds":900,"finished":true,"signals":{"tab_switches":1,"fullscreen_exits":0,"paste_attempts":0,"copy_attempts":0,"device_changes":0,"fast_answers":0},"report_pdf":{"url":"https://iqmetrics.org/api/v1/results/res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl/report.pdf?expires=1791036900&signature=org_2Kd4Fh6Jl8Zx0Cv2Bn4Mq6Wr.live.Wq3Rt5Yu7Io9Pa1Sd3Fg5Hj7Kl9Zx1Cv3Bn5Mq7Wr9T","expires_at":"2026-10-03T13:15:00Z"},"livemode":true,"completed_at":"2026-10-03T12:15:00Z"}
Invitation batch
Up to 500 invitations sent at once and processed in the background.
One event sent to one endpoint, with its attempts.
Field
Type
Description
idrequired
string
objectrequired
string
Always webhook_delivery.
event_idrequired
string
event_typerequired
string
One of result.ready, invitation.opened, session.started, invitation.expired, invitation.bounced, usage.threshold_reached, shared_session.completed, webhook.test.
The body of every webhook. It carries IDs only: fetch the objects with your key.
Field
Type
Description
idrequired
string
typerequired
string
One of result.ready, invitation.opened, session.started, invitation.expired, invitation.bounced, usage.threshold_reached, shared_session.completed, webhook.test.
created_atrequired
string (date-time)
livemoderequired
boolean
datarequired
object
The IDs this event is about, and for usage events the metric and threshold. drive_id is in a result.ready event only when the test was a seat in a campus drive; it is left out otherwise.
data.invitation_id
string
data.result_id
string
data.assessment_id
string
data.candidate_id
string
data.drive_id
string
data.metric
string
data.threshold_percent
integer
One of 80, 100.
data.shared_session_id
string
data.test_id
string
For shared_session.completed, the shared test’s ID.
Measured reliability (KR-20), or null until measured. Only measured values are ever shown.
comparison_group
object or null
comparison_group.namerequired
string
comparison_group.sizerequired
integer
comparison_group.provisionalrequired
boolean
True while the group is still small.
Example
Test
{"id":"numerical-reasoning","object":"test","name":"Numerical Reasoning","description":"Working with numbers, tables and charts: percentages, rates, averages and data.","categories":[{"category":"numerical_reasoning","questions":18}],"questions":18,"time_limit_minutes":20,"languages":["en"],"reliability":null,"comparison_group":null}
Category
A category for custom tests, with its topics and the builder’s limits.
Field
Type
Description
idrequired
string
objectrequired
string
Always category.
namerequired
string
domainrequired
string
topicsrequired
array of object
topics[].idrequired
string
topics[].namerequired
string
min_questionsrequired
integer
max_questionsrequired
integer
recommended_questionsrequired
integer
Enough for a reliable sub-score.
seconds_per_question
integer
The typical time per question.
difficultiesrequired
array of string
languagesrequired
array of string
Example
Category
{"id":"numerical_reasoning","object":"category","name":"Numerical reasoning","domain":"Quantitative & data reasoning","topics":[{"id":"arithmetic","name":"Arithmetic"},{"id":"percentages","name":"Percentages"},{"id":"rates_and_time","name":"Rates and time"},{"id":"averages","name":"Averages"},{"id":"data_tables","name":"Data tables"}],"min_questions":5,"max_questions":40,"recommended_questions":8,"seconds_per_question":60,"difficulties":["easy","medium","hard","mixed"],"languages":["en"]}
A test from the shared pool, for other websites. Its ID starts a shared session; it is never a hiring test.
Field
Type
Description
idrequired
string
objectrequired
string
Always shared_test.
namerequired
string
descriptionrequired
string
categoriesrequired
array of object
categories[].categoryrequired
string
categories[].questionsrequired
integer
questionsrequired
integer
time_limit_minutesrequired
integer
languagesrequired
array of string
reliability
number or null
Measured reliability, or null until measured. Only measured values are ever shown.
comparison_group
object or null
comparison_group.namerequired
string
comparison_group.sizerequired
integer
comparison_group.provisionalrequired
boolean
True while the group is still small.
Example
Shared test
{"id":"sample-shared-reasoning","object":"shared_test","name":"Sample Shared Reasoning","description":"The sandbox version of a shared test: numerical and logical reasoning, with sample questions.","categories":[{"category":"numerical_reasoning","questions":6},{"category":"logical_reasoning","questions":6}],"questions":12,"time_limit_minutes":12,"languages":["en"],"reliability":null,"comparison_group":null}
Shared session
One person taking a shared test on your website: the unit on screen and the time left while it runs, then the result. It carries your own reference, never candidate details.
Field
Type
Description
idrequired
string
objectrequired
string
Always shared_session.
livemoderequired
boolean
test_idrequired
string
referencerequired
string or null
statusrequired
string
One of active, completed.
created_atrequired
string (date-time)
deadlinerequired
string (date-time) or null
When the time runs out.
seconds_leftrequired
integer or null
While active, the seconds until the deadline.
time_limit_secondsrequired
integer
answeredrequired
integer
The questions already behind the person.
totalrequired
integer
The questions of the test, trial questions included.
unitrequired
object or null
While active, the unit on screen; null once the session is over.
unit.typerequired
string
One of question, set.
unit.seqrequired
integer
The number of its first question; send it as unit. At least 1.
unit.stimulusrequired
object or null
For a set, its shared material; null for a question on its own.
unit.stimulus.kindrequired
string
One of text, table, picture.
unit.stimulus.titlerequired
string or null
unit.stimulus.textrequired
string or null
unit.stimulus.columnsrequired
array of string
unit.stimulus.rowsrequired
array of array of string
unit.stimulus.noterequired
string or null
unit.stimulus.picturerequired
object or null
unit.stimulus.picture.svgrequired
string
An SVG document rebuilt from an allowlist. Show it as an image.
unit.stimulus.picture.altrequired
string
Its text alternative.
unit.questionsrequired
array of object
Up to 6 items.
unit.questions[].seqrequired
integer
At least 1.
unit.questions[].categoryrequired
string
The category’s name.
unit.questions[].formatrequired
string
One of single_choice, true_false_cannot_say, picture_choice, numeric.
unit.questions[].stemrequired
string
Plain text; line breaks are meaningful.
unit.questions[].picturerequired
object or null
unit.questions[].picture.svgrequired
string
An SVG document rebuilt from an allowlist. Show it as an image.
unit.questions[].picture.altrequired
string
Its text alternative.
unit.questions[].optionsrequired
array of object or object
In this session’s order. Pictures for picture_choice, none for numeric. Up to 6 items.
unit.questions[].decimalsrequired
integer or null
For numeric, the decimal places asked for. 0 to 6.
unit.questions[].unitrequired
string or null
For numeric, a unit to show after the box, such as “%”.
unit.questions[].answerrequired
string or null
Always null here; answers are sent per unit.
resultrequired
object or null
Once the session is over, its result; null while it runs.
result.correctrequired
integer
Scored questions answered correctly. Trial questions do not count.
result.totalrequired
integer
Scored questions.
result.categoriesrequired
array of object
result.categories[].categoryrequired
string
A category ID, or, for a section of your own skill, the skill’s ID.
result.categories[].namerequired
string
result.categories[].correctrequired
integer
result.categories[].totalrequired
integer
result.categories[].sourcerequired
string
Where the section’s questions came from: iqmetrics for one of our categories, company for one of your own skills. A result from before your own questions existed says iqmetrics. One of iqmetrics, company.
result.categories[].bandrequired
string or null
A band against the test’s comparison group, or null while the test has none. Bands come only from measured data, so a new test or a custom test starts without them. One of below_typical, typical, above_typical, well_above.
result.time_used_secondsrequired
integer
result.time_limit_secondsrequired
integer
result.finishedrequired
boolean
False when the time ran out.
result.integrityrequired
string
The test ran in your design, so no integrity signals were collected. One of not_monitored.
result.scoring_versionrequired
string
result.completed_atrequired
string (date-time)
Example
Shared session
{"id":"ssn_4Fg6Hj8Kl0Zx2Cv4Bn6Mq8Wr","object":"shared_session","livemode":false,"test_id":"sample-shared-reasoning","reference":"visit-20261003-0042","status":"active","created_at":"2026-10-03T12:20:00Z","deadline":"2026-10-03T12:32:05Z","seconds_left":725,"time_limit_seconds":725,"answered":0,"total":13,"unit":{"type":"question","seq":1,"stimulus":null,"questions":[{"seq":1,"category":"Numerical reasoning","format":"single_choice","stem":"A tank holds 80 litres. It is filled by a further 25%. How many litres does it hold now?","picture":null,"options":[{"id":"a","text":"95"},{"id":"b","text":"100"},{"id":"c","text":"105"},{"id":"d","text":"120"}],"decimals":null,"unit":null,"answer":null}]},"result":null}
Problem (an error)
Every error has this shape (RFC 9457). code is stable; title and detail are for people.
Field
Type
Description
typerequired
string
A URI for the problem type; about:blank for now.
titlerequired
string
statusrequired
integer
400 to 599.
detail
string
coderequired
string
One of bad_request, unauthorized, forbidden, not_found, method_not_allowed, conflict, idempotency_conflict, payload_too_large, validation_failed, limit_reached, rate_limited, internal_error, unavailable.
request_idrequired
string
errors
array of object
For validation_failed, one entry per field.
errors[].fieldrequired
string
The field’s path, such as sections[0].questions.
errors[].coderequired
string
A stable code: required, invalid, too_long, too_many, out_of_range, one_of, unknown_field, unknown_category, unknown_skill, unknown_topic, unknown_test, unknown_role_template, duplicate, not_enough_questions or not_available (a feature that is not available yet).
errors[].messagerequired
string
limit
object
For limit_reached.
limit.metricrequired
string
limit.usedrequired
integer
limit.allowedrequired
integer
limit.resets_at
string (date-time) or null
Example
Problem (an error)
{"type":"about:blank","title":"Validation failed","status":422,"detail":"1 field is not acceptable.","code":"validation_failed","request_id":"req_7fae01f55a6b9aacd7a5e2f9","errors":[{"field":"sections[0].questions","code":"out_of_range","message":"Use between 5 and 40 questions per category."}]}
Get your sandbox keys
Create your company account with your work email. The sandbox is ready as soon as you sign in: create a sandbox key in the developer dashboard and start building.