IQ Metrics
IIF Certified Assessment Start IQ Test
IIF Certified Assessment

Developer documentation

IQ Metrics for Business API documentation

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.

  • Base URLhttps://iqmetrics.org/api/v1
  • Version1.0.0-draft.2
  • FormatJSON · OpenAPI 3.1

Start the quick startDownload the OpenAPI file

Overview

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.

  1. POST/assessmentsCreate an assessment for the role.
  2. POST/assessments/{id}/invitationsInvite a candidate and get their personal link.
  3. EVENTresult.readyWe call your endpoint when they finish.
  4. 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.

  1. 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 shown
    export IQM_KEY
  2. Check that it works

    Request
    curl https://iqmetrics.org/api/v1/usage \
      -H "Authorization: Bearer $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.

  3. 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.

    Request
    curl -X POST https://iqmetrics.org/api/v1/assessments \
      -H "Authorization: Bearer $IQM_KEY" \
      -H "Idempotency-Key: 5f1c2a9e-8d4b-4c1e-9a7f-2b6d3e8c0a11" \
      -H "Content-Type: application/json" \
      -d '{"name": "Integration test", "test": "sample-aptitude"}'
    Reply 201, shortened
    {
      "id": "asm_7Q2mX8aB1cD3eF5gH7jK9pL0",
      "object": "assessment",
      "status": "active",
      "test": "sample-aptitude",
      "livemode": false
    }

    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.

  4. Invite a candidate: yourself

    Request
    curl -X POST https://iqmetrics.org/api/v1/assessments/asm_7Q2mX8aB1cD3eF5gH7jK9pL0/invitations \
      -H "Authorization: Bearer $IQM_KEY" \
      -H "Idempotency-Key: 0b8e3c1d-6f2a-4d9e-b7c5-9a1e4f3d2c10" \
      -H "Content-Type: application/json" \
      -d '{"candidate": {"email": "you@yourcompany.com", "first_name": "Sam"}, "send_email": false}'
    Reply 201, shortened
    {
      "id": "inv_8KD2wT4yU6iO8pA0sD2fG4hJ",
      "status": "invited",
      "candidate_url": "https://iqmetrics.org/assess/c/9f1bQ7xK2mN4pR6sT8vW0yZ3aB5cD7eF9gH1jK3mN5p",
      "expires_at": "2026-10-08T09:31:00Z"
    }

    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.

  5. Read the result

    Request
    curl https://iqmetrics.org/api/v1/invitations/inv_8KD2wT4yU6iO8pA0sD2fG4hJ/result \
      -H "Authorization: Bearer $IQM_KEY"

    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.

PageWhat you do there
OverviewA get-started checklist, today’s API calls, the error rate and response time, and an example request to copy.
API keysCreate 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.
WebhooksAdd 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 & usageUsage 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 libraryThe 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 websiteThe 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

ScopeAllowsOperations
catalogue:readRead the ready-made tests, the categories and the role templates.List the ready-made tests, Get one ready-made test, List the categories for custom tests, List the role templates
assessments:readRead assessments.List assessments, Get an assessment
assessments:writeCreate and change assessments.Create an assessment, Change an assessment
invitations:readRead invitations and where they stand.List an assessment’s invitations, Get an invitation
invitations:writeInvite candidates, re-issue links, cancel and extend invitations, and send batches.Invite a candidate, Invite up to 500 candidates at once, Get a batch of invitations, Re-issue the candidate’s link, Cancel an invitation, Move the invitation’s deadline, or give extra time
results:readRead results.Get the result of an invitation, List results
candidates:deleteDelete a candidate and everything linked to them.Delete a candidate’s data
webhooks:readRead webhook endpoints and their deliveries.List webhook endpoints, Get a webhook endpoint, List an endpoint’s deliveries
webhooks:writeAdd, change and remove endpoints; send test events, replay deliveries and rotate secrets.Add a webhook endpoint, Change a webhook endpoint, Remove a webhook endpoint, Replace the signing secret, Send a test event, Send a delivery again
usage:readRead the usage counters.Get usage in the current period
shared:useUse shared tests on your website: list them, and start and answer shared sessions.List the shared tests, Start a shared session, Get a shared session, Answer the unit on screen

Requests and lists

IDs

IDs are random strings with a prefix that says what they are. Treat them as opaque.

PrefixObjectPrefixObject
asm_Assessmentbat_Invitation batch
inv_Invitationwhk_Webhook endpoint
cnd_Candidatedlv_Webhook delivery
res_Resultevt_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.

A list
{
  "object": "list",
  "data": [ … ],
  "has_more": true,
  "next_cursor": "eyJpZCI6ImFzbV83UTJt…"
}

Your own values

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."
    }
  ]
}
CodeStatusMeaning
bad_request400The request cannot be read, for example the body is not valid JSON.
unauthorized401The key is missing, unknown, revoked or expired. The reply is the same in every case.
forbidden403The key does not have the scope this operation needs.
not_found404No such object for this key. Another company’s objects always answer this, never forbidden.
method_not_allowed405The path exists, but not with this method.
conflict409The object is in a state that does not allow this, for example canceling a completed invitation.
idempotency_conflict409The Idempotency-Key was already used with a different body.
payload_too_large413The body is over 1 MB.
validation_failed422Some values are not acceptable. errors lists each field with a stable code.
limit_reached403A usage limit on your account is reached. limit says which one, how much is used and when it resets.
rate_limited429Too many requests. Wait for the number of seconds in Retry-After.
internal_error500Something failed on our side. Try again; if it keeps happening, send us the request_id.
unavailable503The 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:

HeaderMeaning
RateLimit-LimitRequests allowed in the current window for this key.
RateLimit-RemainingRequests left in the current window.
RateLimit-ResetSeconds 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.

EventSent when
result.readyA 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.openedA candidate opened their link. The first time the link is opened.
session.startedA candidate started the test. The candidate passed the consent and practice steps and started the timed test.
invitation.expiredAn invitation expired unused. The deadline passed before the candidate started.
invitation.bouncedThe invitation email bounced. Only for invitations we emailed. Check the address and re-issue the link.
usage.threshold_reachedA 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.completedA shared session is over. The person finished (or their time ran out) and the result can be read with getSharedSession.
webhook.testA test event, sent when you call Send a test event.

What arrives

Each delivery is a POST with a small JSON body. It carries IDs only, never scores or personal details: fetch the objects with your key.

result.ready
{
  "id": "evt_6Yh4Tg2Rf0Ed8Ws6Qa4Zx2Cv",
  "type": "result.ready",
  "created_at": "2026-10-03T12:15:00Z",
  "livemode": true,
  "data": {
    "invitation_id": "inv_8KD2wT4yU6iO8pA0sD2fG4hJ",
    "result_id": "res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl",
    "assessment_id": "asm_7Q2mX8aB1cD3eF5gH7jK9pL0",
    "candidate_id": "cnd_3Nx5Bv7Mc9Lk1Jh3Gf5Ds7Aq"
  }
}
HeaderMeaning
webhook-idThe event’s ID. The same on every retry and replay, so use it to ignore repeats.
webhook-timestampWhen this attempt was signed, in seconds since 1970 (UTC).
webhook-signatureOne or more v1,<base64> signatures, separated by spaces.

Answer with any 2xx as soon as the delivery is stored, then do the work. A slow or failing answer counts as a failed delivery.

Checking signatures

Deliveries are signed in the Standard Webhooks format. Check every one before you trust it:

  1. Refuse a delivery whose webhook-timestamp is more than 5 minutes from your clock.
  2. Build the signed content: webhook-id, a dot, webhook-timestamp, a dot, then the raw request body, exactly as received.
  3. Compute HMAC-SHA256 of it, keyed with your secret: the part after whsec_, decoded from base64.
  4. Accept the delivery if the base64 of that result equals any v1, signature in the header. Compare in constant time.
Node.js
const crypto = require('crypto');

// secret: the endpoint's signing secret, "whsec_" + base64
// headers: the request headers, lower-case names; rawBody: the body as received
function verifyWebhook(secret, headers, rawBody) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'] || '';
  if (!id || !timestamp) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest('base64');

  return signatures.split(' ').some((entry) => {
    const [version, value] = entry.split(',');
    return version === 'v1' && value !== undefined && value.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
  });
}
Python
import base64, hashlib, hmac, time

# secret: the endpoint's signing secret, "whsec_" + base64
# headers: the request headers; raw_body: the body as received, in bytes
def verify_webhook(secret, headers, raw_body):
    msg_id = headers.get("webhook-id", "")
    timestamp = headers.get("webhook-timestamp", "")
    signatures = headers.get("webhook-signature", "")
    if not (msg_id and timestamp and signatures):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for entry in signatures.split():
        version, _, value = entry.partition(",")
        if version == "v1" and hmac.compare_digest(value, expected):
            return True
    return False
PHP
// $secret: the endpoint's signing secret, "whsec_" + base64
// $headers: the request headers, lower-case names; $rawBody: file_get_contents('php://input')
function verify_webhook(string $secret, array $headers, string $rawBody): bool
{
    $id = $headers['webhook-id'] ?? '';
    $timestamp = $headers['webhook-timestamp'] ?? '';
    $signatures = $headers['webhook-signature'] ?? '';
    if ($id === '' || $timestamp === '' || $signatures === '') {
        return false;
    }
    if (abs(time() - (int) $timestamp) > 300) {
        return false;
    }
    $key = base64_decode(preg_replace('/^whsec_/', '', $secret));
    $expected = base64_encode(hash_hmac('sha256', $id . '.' . $timestamp . '.' . $rawBody, $key, true));
    foreach (explode(' ', $signatures) as $entry) {
        $parts = explode(',', $entry, 2);
        if (count($parts) === 2 && $parts[0] === 'v1' && hash_equals($expected, $parts[1])) {
            return true;
        }
    }
    return false;
}

Retries and replays

  • If your endpoint doesn’t answer 2xx, we try again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours.
  • An endpoint that keeps failing is paused. When it is fixed, resume it with Change a webhook endpoint ("status": "active"), then send it what it missed with Send a delivery again.
  • Retries and replays keep the same webhook-id, so a receiver that already has the event can ignore it.
  • Check an endpoint at any time with Send a test event, and see every attempt in the delivery log, kept for 30 days.
  • 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.

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):

  1. List the shared tests to find a test’s ID.
  2. Start a shared session for that test, with your own reference for the visitor. The reply carries the first unit and the time left.
  3. Answer the unit on screen, and the reply carries the next one. Sending the same answers again is safe.
  4. 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.

The embed code
<script src="https://iqmetrics.org/assess/embed.js"
        data-key="iqm_pk_live_…"
        data-test="shared-reasoning" async></script>

API reference

Every operation of version 1, grouped by what it works on. Each example below is checked against the API contract when this page is built.

System

Health of the API.

Check that the API is up

GET/health

Public and minimal. It reveals nothing but whether the API and its database answer.

Public: no key needed.

Reply

200 The API is up. It has these fields:

FieldTypeDescription
status requiredstringOne of ok, degraded.
time requiredstring (date-time)

Errors 400 bad_request 429 rate_limited 503 unavailable

Request
curl "https://iqmetrics.org/api/v1/health"
Example reply: 200
Reply 200
{
  "status": "ok",
  "time": "2026-10-03T12:00:00Z"
}

Catalog

The ready-made tests, the categories for custom tests, and the role templates.

List the ready-made tests

GET/tests

The tests a company can use as they are, or as the starting point of a custom test.

Needs the catalogue:read scope.

Reply

200 The ready-made tests. Returns a list of Test objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/tests" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the catalogue:read scope.

Parameters
NameInTypeDescription
test_id requiredpathstringThe test’s ID, for example numerical-reasoning.
Reply

200 The test. Returns Test.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/tests/numerical-reasoning" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the catalogue:read scope.

Reply

200 The categories. Returns a list of Category objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/categories" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the catalogue:read scope.

Reply

200 The role templates. Returns a list of Role template objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/role-templates" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "object": "list",
  "data": [
    {
      "id": "finance-accounting",
      "object": "role_template",
      "name": "Finance / accounting",
      "tests": [
        "numerical-reasoning",
        "attention-to-detail"
      ],
      "time_limit_minutes": 30
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Assessments

A company’s set-up for a role – tests or custom sections, plus settings.

List assessments

GET/assessments

Newest first.

Needs the assessments:read scope.

Parameters
NameInTypeDescription
statusquerystringOnly assessments with this status. One of draft, active, closed, archived.
limitqueryintegerHow many items per page, 1-100. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Up to 200 characters.
Reply

200 One page of assessments. Returns a list of Assessment objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/assessments" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the assessments:write scope.

Parameters
NameInTypeDescription
Idempotency-Key requiredheaderstringA unique string per operation, such as a UUID. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body

Give exactly one of test, role_template or sections.

FieldTypeDescription
Reply

201 The assessment was created. Returns Assessment.

Errors 400 bad_request 401 unauthorized 403 limit_reached 403 forbidden 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/assessments" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Idempotency-Key: 5f1c2a9e-8d4b-4c1e-9a7f-2b6d3e8c0a11" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Operations Analyst - Oct",
    "purpose": "hiring",
    "sections": [
      {
        "category": "numerical_reasoning",
        "questions": 10,
        "difficulty": "mixed"
      },
      {
        "category": "logical_reasoning",
        "questions": 10,
        "difficulty": "medium"
      },
      {
        "category": "attention_to_detail",
        "questions": 15,
        "difficulty": "mixed"
      }
    ],
    "time_limit_minutes": 35,
    "settings": {
      "practice_questions": true,
      "shuffle_sections": false,
      "language": "en",
      "expires_in_days": 7,
      "allow_going_back": true,
      "allow_marking": true
    }
  }'
Example reply: 201
Reply 201
{
  "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": 0,
    "started": 0,
    "completed": 0
  },
  "livemode": true,
  "created_at": "2026-10-01T09:30:00Z",
  "updated_at": "2026-10-01T09:30:00Z"
}

Get an assessment

GET/assessments/{assessment_id}

The assessment, its current version, and its invited, started and completed counts.

Needs the assessments:read scope.

Parameters
NameInTypeDescription
assessment_id requiredpathstringThe assessment’s ID.
Reply

200 The assessment. Returns Assessment.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/assessments/asm_7Q2mX8aB1cD3eF5gH7jK9pL0" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the assessments:write scope.

Parameters
NameInTypeDescription
assessment_id requiredpathstringThe assessment’s ID.
Body
FieldTypeDescription
namestringUp to 190 characters.
statusstringOne of active, closed, archived.
sectionsarray of any or anyUp to 12 items.
sections[].categorystringA category ID from /categories.
sections[].skillstringOne 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[].questions requiredintegerHow 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[].difficultystringmixed spreads questions across easy, medium and hard. One of easy, medium, hard, mixed.
sections[].topicsarray of stringOnly 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_minutesintegerA time for this section alone. Leave out to use the assessment’s total time. Not available yet (not_available). At least 1.
time_limit_minutesinteger1 to 240.
settingsobject
settings.practice_questionsbooleanA 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_sectionsbooleanDefault false.
settings.languagestringOnly en is available yet. Default "en".
settings.expires_in_daysintegerThe default deadline of new invitations. 1 to 60. Default 7.
settings.retake_after_daysinteger or nullnull means no retakes. Retakes are not available yet; a number is refused with not_available. At least 1.
settings.redirect_urlstring (uri) or nullWhere candidates go after finishing (HTTPS).
settings.allow_going_backbooleanCandidates 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_markingbooleanCandidates 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.
Reply

200 The changed assessment. Returns Assessment.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited

Request
curl -X PATCH "https://iqmetrics.org/api/v1/assessments/asm_7Q2mX8aB1cD3eF5gH7jK9pL0" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "active"
  }'
Example reply: 200
Reply 200
{
  "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"
}

Invitations

One candidate’s link to one assessment, and its status.

List an assessment’s invitations

GET/assessments/{assessment_id}/invitations

Newest first. Filter by status, or find your own ATS record with external_id.

Needs the invitations:read scope.

Parameters
NameInTypeDescription
assessment_id requiredpathstringThe assessment’s ID.
statusquerystringOnly invitations with this status. One of invited, opened, started, completed, expired, cancelled, bounced.
external_idquerystringOnly the invitation whose candidate carries this external ID. Up to 191 characters.
limitqueryintegerHow many items per page, 1-100. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Up to 200 characters.
Reply

200 One page of invitations. Returns a list of Invitation objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/assessments/asm_7Q2mX8aB1cD3eF5gH7jK9pL0/invitations" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "object": "list",
  "data": [
    {
      "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": "Rivera",
        "external_id": "ats-9921",
        "created_at": "2026-10-01T09:31:00Z"
      },
      "status": "completed",
      "expires_at": "2026-10-08T09:31:00Z",
      "extra_time_percent": 0,
      "opened_at": "2026-10-03T11:58:40Z",
      "started_at": "2026-10-03T12:01:39Z",
      "completed_at": "2026-10-03T12:15:00Z",
      "result_id": "res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl",
      "metadata": {
        "requisition": "OPS-114"
      },
      "livemode": true,
      "created_at": "2026-10-01T09:31:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Invite a candidate

POST/assessments/{assessment_id}/invitations

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.

Needs the invitations:write scope.

Parameters
NameInTypeDescription
assessment_id requiredpathstringThe assessment’s ID.
Idempotency-Key requiredheaderstringA unique string per operation, such as a UUID. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
FieldTypeDescription
candidate requiredobject
candidate.email requiredstring (email)Up to 190 characters.
candidate.first_namestring or nullUp to 100 characters.
candidate.last_namestring or nullUp to 100 characters.
candidate.external_idstring or nullYour own ID, for example the ATS candidate ID. null, or left out, means none. Up to 191 characters.
send_emailbooleanfalse means your system delivers the link. Default true.
expires_in_daysintegerLeave out for the assessment’s default. 1 to 60.
extra_time_percentintegerExtra time as an accommodation. The change is written to the audit log. One of 0, 25, 50, 100. Default 0.
metadataobjectUp 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.

Errors 400 bad_request 401 unauthorized 403 limit_reached 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/assessments/asm_7Q2mX8aB1cD3eF5gH7jK9pL0/invitations" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Idempotency-Key: 5f1c2a9e-8d4b-4c1e-9a7f-2b6d3e8c0a11" \
  -H "Content-Type: application/json" \
  -d '{
    "candidate": {
      "email": "sam@example.com",
      "first_name": "Sam",
      "external_id": "ats-9921"
    },
    "send_email": false,
    "expires_in_days": 7
  }'
Example reply: 201
Reply 201
{
  "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"
}

Invite up to 500 candidates at once

POST/assessments/{assessment_id}/invitations/bulk

The batch is queued and processed in the background. Poll the batch, or wait for its invitations to show up. Each line succeeds or fails on its own.

Needs the invitations:write scope.

Parameters
NameInTypeDescription
assessment_id requiredpathstringThe assessment’s ID.
Idempotency-Key requiredheaderstringA unique string per operation, such as a UUID. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
FieldTypeDescription
invitations requiredarray of objectUp to 500 items.
invitations[].candidate requiredobject
invitations[].candidate.email requiredstring (email)Up to 190 characters.
invitations[].candidate.first_namestring or nullUp to 100 characters.
invitations[].candidate.last_namestring or nullUp to 100 characters.
invitations[].candidate.external_idstring or nullYour own ID, for example the ATS candidate ID. null, or left out, means none. Up to 191 characters.
invitations[].send_emailbooleanfalse means your system delivers the link. Default true.
invitations[].expires_in_daysintegerLeave out for the assessment’s default. 1 to 60.
invitations[].extra_time_percentintegerExtra time as an accommodation. The change is written to the audit log. One of 0, 25, 50, 100. Default 0.
invitations[].metadataobjectUp 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.

Errors 400 bad_request 401 unauthorized 403 limit_reached 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 413 payload_too_large 422 validation_failed 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/assessments/asm_7Q2mX8aB1cD3eF5gH7jK9pL0/invitations/bulk" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Idempotency-Key: 5f1c2a9e-8d4b-4c1e-9a7f-2b6d3e8c0a11" \
  -H "Content-Type: application/json" \
  -d '{
    "invitations": [
      {
        "candidate": {
          "email": "sam@example.com",
          "first_name": "Sam",
          "external_id": "ats-9921"
        },
        "send_email": false
      },
      {
        "candidate": {
          "email": "lee@example.com",
          "first_name": "Lee",
          "external_id": "ats-9922"
        },
        "send_email": false,
        "extra_time_percent": 25
      }
    ]
  }'
Example reply: 202
Reply 202
{
  "id": "bat_1Qw3Er5Ty7Ui9Op1As3Df5Gh",
  "object": "invitation_batch",
  "assessment_id": "asm_7Q2mX8aB1cD3eF5gH7jK9pL0",
  "status": "queued",
  "total": 2,
  "succeeded": 0,
  "failed": 0,
  "created_at": "2026-10-01T09:40:00Z"
}

Get a batch of invitations

GET/invitation-batches/{batch_id}

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.

Needs the invitations:write scope.

Parameters
NameInTypeDescription
batch_id requiredpathstringThe batch’s ID.
Reply

200 The batch. Returns Invitation batch.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/invitation-batches/bat_1Qw3Er5Ty7Ui9Op1As3Df5Gh" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the invitations:read scope.

Parameters
NameInTypeDescription
invitation_id requiredpathstringThe invitation’s ID.
Reply

200 The invitation. Returns Invitation.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/invitations/inv_8KD2wT4yU6iO8pA0sD2fG4hJ" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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": "Rivera",
    "external_id": "ats-9921",
    "created_at": "2026-10-01T09:31:00Z"
  },
  "status": "completed",
  "expires_at": "2026-10-08T09:31:00Z",
  "extra_time_percent": 0,
  "opened_at": "2026-10-03T11:58:40Z",
  "started_at": "2026-10-03T12:01:39Z",
  "completed_at": "2026-10-03T12:15:00Z",
  "result_id": "res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl",
  "metadata": {
    "requisition": "OPS-114"
  },
  "livemode": true,
  "created_at": "2026-10-01T09:31:00Z"
}

Re-issue the candidate’s link

POST/invitations/{invitation_id}/resend

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.

Needs the invitations:write scope.

Parameters
NameInTypeDescription
invitation_id requiredpathstringThe invitation’s ID.
Idempotency-KeyheaderstringOptional here. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
FieldTypeDescription
send_emailbooleanDefault true.
Reply

200 The invitation, with its new link. Returns Invitation.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/invitations/inv_8KD2wT4yU6iO8pA0sD2fG4hJ/resend" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "send_email": false
  }'
Example reply: 200
Reply 200
{
  "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": "opened",
  "expires_at": "2026-10-08T09:31:00Z",
  "extra_time_percent": 0,
  "opened_at": "2026-10-02T08:14:00Z",
  "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/Zp3kW8nQ1vT6yB4mR9sX2cF7hJ5dL0aG3eK8uN1wV6q"
}

Cancel an invitation

POST/invitations/{invitation_id}/cancel

The link stops working. A test already started is ended and not scored.

Needs the invitations:write scope.

Parameters
NameInTypeDescription
invitation_id requiredpathstringThe invitation’s ID.
Idempotency-KeyheaderstringOptional here. Repeats within 24 hours return the first reply. 8 to 255 characters.
Reply

200 The canceled invitation. Returns Invitation.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/invitations/inv_8KD2wT4yU6iO8pA0sD2fG4hJ/cancel" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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": "cancelled",
  "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"
}

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.

Needs the invitations:write scope.

Parameters
NameInTypeDescription
invitation_id requiredpathstringThe invitation’s ID.
Idempotency-KeyheaderstringOptional here. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
FieldTypeDescription
expires_in_daysintegerThe new deadline, counted from now. 1 to 60.
extra_time_percentintegerOne of 0, 25, 50, 100.
Reply

200 The changed invitation. Returns Invitation.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/invitations/inv_8KD2wT4yU6iO8pA0sD2fG4hJ/extend" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expires_in_days": 3
  }'
Example reply: 200
Reply 200
{
  "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-11T09: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"
}

Results

Scores, categories, rank, percentile, time and integrity signals, and each result’s PDF report.

Get the result of an invitation

GET/invitations/{invitation_id}/result

Available once the invitation is completed; until then the reply is 404 not_found.

Needs the results:read scope.

Parameters
NameInTypeDescription
invitation_id requiredpathstringThe invitation’s ID.
Reply

200 The result. Returns Result.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/invitations/inv_8KD2wT4yU6iO8pA0sD2fG4hJ/result" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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"
}

List results

GET/results

Newest first. Use since to fetch only what arrived after your last sync.

Needs the results:read scope.

Parameters
NameInTypeDescription
assessment_idquerystringOnly results of this assessment.
sincequerystring (date-time)Only results completed at or after this time.
limitqueryintegerHow many items per page, 1-100. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Up to 200 characters.
Reply

200 One page of results. Returns a list of Result objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/results" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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
NameInTypeDescription
result_id requiredpathstringThe result’s ID.
expires requiredqueryintegerWhen the link stops working, in Unix seconds. Part of the link. At least 0.
signature requiredquerystringThe link’s signature. Part of the link.
Reply

200 The PDF report, as a file to save. No content.

Request
curl "https://iqmetrics.org/api/v1/results/res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl/report.pdf"

Candidates

The people invited, and the deletion of their data.

Delete a candidate’s data

DELETE/candidates/{candidate_id}

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.

Needs the candidates:delete scope.

Parameters
NameInTypeDescription
candidate_id requiredpathstringThe candidate’s ID.
Reply

204 The candidate’s data is deleted. No content.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl -X DELETE "https://iqmetrics.org/api/v1/candidates/cnd_3Nx5Bv7Mc9Lk1Jh3Gf5Ds7Aq" \
  -H "Authorization: Bearer $IQM_KEY"

Webhooks

Endpoints that receive events, and their delivery log.

List webhook endpoints

GET/webhooks

The endpoints that receive events, with their status.

Needs the webhooks:read scope.

Parameters
NameInTypeDescription
limitqueryintegerHow many items per page, 1-100. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Up to 200 characters.
Reply

200 One page of endpoints. Returns a list of Webhook endpoint objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/webhooks" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "object": "list",
  "data": [
    {
      "id": "whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh",
      "object": "webhook_endpoint",
      "url": "https://ats.example.com/hooks/iqm",
      "events": [
        "result.ready",
        "invitation.expired"
      ],
      "description": "Our ATS",
      "status": "active",
      "paused_reason": null,
      "livemode": true,
      "created_at": "2026-10-01T10:02:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Add a webhook endpoint

POST/webhooks

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.

Needs the webhooks:write scope.

Parameters
NameInTypeDescription
Idempotency-Key requiredheaderstringA unique string per operation, such as a UUID. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
FieldTypeDescription
url requiredstring (uri)Up to 2048 characters.
events requiredarray of string
descriptionstring or nullPlain text. An empty one, or null, clears it. Up to 255 characters.
Reply

201 The endpoint, with its signing secret. Returns Webhook endpoint.

Errors 400 bad_request 401 unauthorized 403 limit_reached 403 forbidden 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/webhooks" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Idempotency-Key: 5f1c2a9e-8d4b-4c1e-9a7f-2b6d3e8c0a11" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://ats.example.com/hooks/iqm",
    "events": [
      "result.ready",
      "invitation.expired"
    ]
  }'
Example reply: 201
Reply 201
{
  "id": "whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh",
  "object": "webhook_endpoint",
  "url": "https://ats.example.com/hooks/iqm",
  "events": [
    "result.ready",
    "invitation.expired"
  ],
  "description": "Our ATS",
  "status": "active",
  "paused_reason": null,
  "livemode": true,
  "created_at": "2026-10-01T10:02:00Z",
  "secret": "whsec_ZXhhbXBsZS1zZWNyZXQtZm9yLXRoZS1kb2NzLW9ubHk="
}

Get a webhook endpoint

GET/webhooks/{webhook_id}

The endpoint and its status. The secret is never shown again.

Needs the webhooks:read scope.

Parameters
NameInTypeDescription
webhook_id requiredpathstringThe webhook endpoint’s ID.
Reply

200 The endpoint. Returns Webhook endpoint.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/webhooks/whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "id": "whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh",
  "object": "webhook_endpoint",
  "url": "https://ats.example.com/hooks/iqm",
  "events": [
    "result.ready",
    "invitation.expired"
  ],
  "description": "Our ATS",
  "status": "active",
  "paused_reason": null,
  "livemode": true,
  "created_at": "2026-10-01T10:02:00Z"
}

Change a webhook endpoint

PATCH/webhooks/{webhook_id}

Change its URL, events or description, or set status to active to resume a paused endpoint.

Needs the webhooks:write scope.

Parameters
NameInTypeDescription
webhook_id requiredpathstringThe webhook endpoint’s ID.
Body
FieldTypeDescription
urlstring (uri)Up to 2048 characters.
eventsarray of string
descriptionstring or nullPlain text. An empty one, or null, clears it. Up to 255 characters.
statusstringResume a paused endpoint. One of active.
Reply

200 The changed endpoint. Returns Webhook endpoint.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 422 validation_failed 429 rate_limited

Request
curl -X PATCH "https://iqmetrics.org/api/v1/webhooks/whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      "result.ready",
      "invitation.expired",
      "invitation.bounced"
    ],
    "description": "ATS production"
  }'
Example reply: 200
Reply 200
{
  "id": "whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh",
  "object": "webhook_endpoint",
  "url": "https://ats.example.com/hooks/iqm",
  "events": [
    "result.ready",
    "invitation.expired"
  ],
  "description": "Our ATS",
  "status": "active",
  "paused_reason": null,
  "livemode": true,
  "created_at": "2026-10-01T10:02:00Z"
}

Remove a webhook endpoint

DELETE/webhooks/{webhook_id}

Deliveries still waiting for this endpoint are dropped.

Needs the webhooks:write scope.

Parameters
NameInTypeDescription
webhook_id requiredpathstringThe webhook endpoint’s ID.
Reply

204 The endpoint is removed. No content.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl -X DELETE "https://iqmetrics.org/api/v1/webhooks/whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh" \
  -H "Authorization: Bearer $IQM_KEY"

Replace the signing secret

POST/webhooks/{webhook_id}/rotate-secret

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.

Needs the webhooks:write scope.

Parameters
NameInTypeDescription
webhook_id requiredpathstringThe webhook endpoint’s ID.
Idempotency-KeyheaderstringOptional here. Repeats within 24 hours return the first reply. 8 to 255 characters.
Reply

200 The endpoint, with its new secret. Returns Webhook endpoint.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/webhooks/whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh/rotate-secret" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "id": "whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh",
  "object": "webhook_endpoint",
  "url": "https://ats.example.com/hooks/iqm",
  "events": [
    "result.ready",
    "invitation.expired"
  ],
  "description": "Our ATS",
  "status": "active",
  "paused_reason": null,
  "livemode": true,
  "created_at": "2026-10-01T10:02:00Z",
  "secret": "whsec_ZXhhbXBsZS1zZWNyZXQtZm9yLXRoZS1kb2NzLW9ubHk="
}

Send a test event

POST/webhooks/{webhook_id}/test

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.

Needs the webhooks:write scope.

Parameters
NameInTypeDescription
webhook_id requiredpathstringThe webhook endpoint’s ID.
Idempotency-KeyheaderstringOptional here. Repeats within 24 hours return the first reply. 8 to 255 characters.
Reply

202 The test delivery was queued. Returns Webhook delivery.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/webhooks/whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh/test" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 202
Reply 202
{
  "id": "dlv_9Zx7Cv5Bn3Mm1Ll9Kk7Jj5Hh",
  "object": "webhook_delivery",
  "event_id": "evt_4Lk2Jh8Gf6Ds4Aa2Qw0Er8Ty",
  "event_type": "webhook.test",
  "status": "pending",
  "attempts": 0,
  "last_status_code": null,
  "next_attempt_at": "2026-10-03T12:20:00Z",
  "created_at": "2026-10-03T12:20:00Z"
}

List an endpoint’s deliveries

GET/webhooks/{webhook_id}/deliveries

The delivery log of the last 30 days, newest first.

Needs the webhooks:read scope.

Parameters
NameInTypeDescription
webhook_id requiredpathstringThe webhook endpoint’s ID.
statusquerystringOnly deliveries with this status. One of pending, succeeded, failed.
limitqueryintegerHow many items per page, 1-100. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Up to 200 characters.
Reply

200 One page of deliveries. Returns a list of Webhook delivery objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/webhooks/whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh/deliveries" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "object": "list",
  "data": [
    {
      "id": "dlv_9Zx7Cv5Bn3Mm1Ll9Kk7Jj5Hh",
      "object": "webhook_delivery",
      "event_id": "evt_6Yh4Tg2Rf0Ed8Ws6Qa4Zx2Cv",
      "event_type": "result.ready",
      "status": "succeeded",
      "attempts": 1,
      "last_status_code": 200,
      "next_attempt_at": null,
      "created_at": "2026-10-03T12:15:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Send a delivery again

POST/webhooks/{webhook_id}/deliveries/{delivery_id}/replay

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.

Needs the webhooks:write scope.

Parameters
NameInTypeDescription
webhook_id requiredpathstringThe webhook endpoint’s ID.
delivery_id requiredpathstringThe delivery’s ID.
Idempotency-KeyheaderstringOptional here. Repeats within 24 hours return the first reply. 8 to 255 characters.
Reply

202 The replay was queued. Returns Webhook delivery.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/webhooks/whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh/deliveries/dlv_9Zx7Cv5Bn3Mm1Ll9Kk7Jj5Hh/replay" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 202
Reply 202
{
  "id": "dlv_9Zx7Cv5Bn3Mm1Ll9Kk7Jj5Hh",
  "object": "webhook_delivery",
  "event_id": "evt_6Yh4Tg2Rf0Ed8Ws6Qa4Zx2Cv",
  "event_type": "result.ready",
  "status": "pending",
  "attempts": 1,
  "last_status_code": 200,
  "next_attempt_at": "2026-10-03T13:00:00Z",
  "created_at": "2026-10-03T12:15:00Z"
}

Usage

Counts of invitations, started and completed tests, against the plan’s limits.

Get usage in the current period

GET/usage

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.

Needs the usage:read scope.

Reply

200 The usage. Returns Usage.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/usage" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "object": "usage",
  "livemode": true,
  "counters": [
    {
      "metric": "invitations",
      "used": 42,
      "reserved": 0,
      "limit": null,
      "period_start": "2026-10-01T00:00:00Z",
      "resets_at": "2026-11-01T00:00:00Z"
    }
  ]
}

Shared tests

Tests for other websites, from the shared pool, run one unit at a time by the partner’s server.

List the shared tests

GET/shared-tests

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.

Needs the shared:use scope.

Reply

200 The shared tests. Returns a list of Shared test objects.

Errors 400 bad_request 401 unauthorized 403 forbidden 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/shared-tests" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the shared:use scope.

Parameters
NameInTypeDescription
Idempotency-Key requiredheaderstringA unique string per operation, such as a UUID. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
FieldTypeDescription
test_id requiredstringA shared test’s ID, from listSharedTests. Up to 64 characters.
referencestring or nullYour own ID for this session, such as your visit’s ID. Plain text; never personal data. null, or left out, means none. Up to 100 characters.
Reply

201 The session, with its first unit. Returns Shared session.

Errors 400 bad_request 401 unauthorized 403 limit_reached 403 forbidden 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited 503 unavailable

Request
curl -X POST "https://iqmetrics.org/api/v1/shared-sessions" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Idempotency-Key: 5f1c2a9e-8d4b-4c1e-9a7f-2b6d3e8c0a11" \
  -H "Content-Type: application/json" \
  -d '{
    "test_id": "sample-shared-reasoning",
    "reference": "visit-20261003-0042"
  }'
Example reply: 201
Reply 201
{
  "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.

Needs the shared:use scope.

Parameters
NameInTypeDescription
session_id requiredpathstringThe shared session’s ID.
Reply

200 The session. Returns Shared session.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 429 rate_limited

Request
curl "https://iqmetrics.org/api/v1/shared-sessions/ssn_4Fg6Hj8Kl0Zx2Cv4Bn6Mq8Wr" \
  -H "Authorization: Bearer $IQM_KEY"
Example reply: 200
Reply 200
{
  "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.

Needs the shared:use scope.

Parameters
NameInTypeDescription
session_id requiredpathstringThe shared session’s ID.
Idempotency-KeyheaderstringOptional here. Repeats within 24 hours return the first reply. 8 to 255 characters.
Body
FieldTypeDescription
unit requiredintegerThe seq of the unit on screen (the number of its first question). At least 1.
answers requiredarray of objectUp to 50 items.
answers[].seq requiredintegerA question of the unit on screen. At least 1.
answers[].choicestring or nullAn option’s id, the typed number, or null to skip.
Reply

200 The session, with the next unit or the result. Returns Shared session.

Errors 400 bad_request 401 unauthorized 403 forbidden 404 not_found 409 conflict 409 idempotency_conflict 422 validation_failed 429 rate_limited

Request
curl -X POST "https://iqmetrics.org/api/v1/shared-sessions/ssn_4Fg6Hj8Kl0Zx2Cv4Bn6Mq8Wr/answers" \
  -H "Authorization: Bearer $IQM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "unit": 1,
    "answers": [
      {
        "seq": 1,
        "choice": "b"
      }
    ]
  }'
Example reply: 200
Reply 200
{
  "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.

FieldTypeDescription
id requiredstring
object requiredstringAlways assessment.
name requiredstring
status requiredstringdraft (not yet usable), active (invitations allowed), closed (no new invitations; open ones can finish), archived. One of draft, active, closed, archived.
purpose requiredstringhiring (results go to the company) or development (internal testing; feedback and retakes can be switched on). One of hiring, development.
version requiredintegerGoes up when sections or time change. At least 1.
teststring or nullThe ready-made test it was made from, if any.
role_templatestring or nullThe role template it was made from, if any.
sections requiredarray of object
sections[].categorystringA category ID from /categories. Present for one of our categories.
sections[].skillstringA skill ID. Present for one of your own skills.
sections[].name requiredstring
sections[].questions requiredinteger1 to 40.
sections[].difficulty requiredstringmixed spreads questions across easy, medium and hard. One of easy, medium, hard, mixed.
sections[].topicsarray of string
sections[].time_limit_minutesintegerAt least 1.
time_limit_minutes requiredinteger
settings requiredobject
settings.practice_questionsbooleanA 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_sectionsbooleanDefault false.
settings.languagestringOnly en is available yet. Default "en".
settings.expires_in_daysintegerThe default deadline of new invitations. 1 to 60. Default 7.
settings.retake_after_daysinteger or nullnull means no retakes. Retakes are not available yet; a number is refused with not_available. At least 1.
settings.redirect_urlstring (uri) or nullWhere candidates go after finishing (HTTPS).
settings.allow_going_backbooleanCandidates 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_markingbooleanCandidates 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.
warnings requiredarray of object
warnings[].code requiredstringdraws_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[].message requiredstring
counts requiredobject
counts.invited requiredinteger
counts.started requiredinteger
counts.completed requiredinteger
livemode requiredboolean
created_at requiredstring (date-time)
updated_at requiredstring (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.

FieldTypeDescription
id requiredstring
object requiredstringAlways invitation.
assessment_id requiredstring
assessment_version requiredinteger
candidate requiredCandidate
status requiredstringOne of invited, opened, started, completed, expired, cancelled, bounced.
candidate_urlstring (uri)Only when the invitation is created or its link is re-issued. Treat it like a password.
expires_at requiredstring (date-time)
extra_time_percent requiredinteger
opened_atstring (date-time) or null
started_atstring (date-time) or null
completed_atstring (date-time) or null
result_idstring or null
metadata requiredobject
sourcestringWho 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_idstring or nullThe campus drive this seat belongs to, or null for every other invitation.
livemode requiredboolean
created_at requiredstring (date-time)
Example
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": "Rivera",
    "external_id": "ats-9921",
    "created_at": "2026-10-01T09:31:00Z"
  },
  "status": "completed",
  "expires_at": "2026-10-08T09:31:00Z",
  "extra_time_percent": 0,
  "opened_at": "2026-10-03T11:58:40Z",
  "started_at": "2026-10-03T12:01:39Z",
  "completed_at": "2026-10-03T12:15:00Z",
  "result_id": "res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl",
  "metadata": {
    "requisition": "OPS-114"
  },
  "livemode": true,
  "created_at": "2026-10-01T09:31:00Z"
}

Candidate

A person you invited. external_id is yours, for example the candidate’s ID in your applicant tracking system.

FieldTypeDescription
id requiredstring
object requiredstringAlways candidate.
email requiredstring (email)
first_namestring or null
last_namestring or null
external_idstring or null
created_at requiredstring (date-time)
Example
Candidate
{
  "id": "cnd_3Nx5Bv7Mc9Lk1Jh3Gf5Ds7Aq",
  "object": "candidate",
  "email": "sam@example.com",
  "first_name": "Sam",
  "last_name": "Rivera",
  "external_id": "ats-9921",
  "created_at": "2026-10-01T09:31:00Z"
}

Result

What a candidate achieved: the score, the categories, rank, percentile, time and integrity signals. It never contains an IQ number.

FieldTypeDescription
id requiredstring
object requiredstringAlways result.
invitation_id requiredstring
assessment_id requiredstring
assessment_version requiredinteger
candidate_id requiredstring
drive_idstring or nullThe campus drive of the seat that took the test, or null for every other result.
score requiredobject
score.correct requiredinteger
score.total requiredinteger
categories requiredarray of object
categories[].category requiredstringA category ID, or, for a section of your own skill, the skill’s ID.
categories[].name requiredstring
categories[].correct requiredinteger
categories[].total requiredinteger
categories[].source requiredstringWhere 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[].band requiredstring or nullA 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.
band requiredstring or nullA 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.
rank requiredobjectAmong the candidates of the same assessment, worked out when read.
rank.position requiredinteger
rank.of requiredinteger
percentile requiredobject or nullAgainst a named comparison group, or null when none exists yet (for example a new custom test).
percentile.value requiredinteger0 to 100.
percentile.group requiredstring
percentile.size requiredinteger
percentile.provisional requiredboolean
time_used_seconds requiredinteger
time_limit_seconds requiredintegerThe 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).
finished requiredbooleanfalse when the time ran out.
signals requiredobjectIntegrity 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_switches requiredintegerAt least 0.
signals.fullscreen_exits requiredintegerAt least 0.
signals.paste_attempts requiredintegerAt least 0.
signals.copy_attempts requiredintegerAt least 0.
signals.focus_lossesintegerAt least 0.
signals.right_clicksintegerAt least 0.
signals.screenshot_keysintegerAt least 0.
signals.shortcut_keysintegerAt least 0.
signals.device_changes requiredintegerAt least 0.
signals.fast_answers requiredintegerAt least 0.
signals.network_changesintegerAt least 0.
signals.away_secondsintegerSeconds, at most 86400. At least 0.
signals.answer_changesintegerAt least 0.
report_pdf requiredobject or nullA 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.url requiredstring (uri)
report_pdf.expires_at requiredstring (date-time)
livemode requiredboolean
completed_at requiredstring (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.

FieldTypeDescription
id requiredstring
object requiredstringAlways invitation_batch.
assessment_id requiredstring
status requiredstringOne of queued, processing, done.
total requiredinteger
succeeded requiredinteger
failed requiredinteger
linesarray of objectOnce done, one line per input, in the same order.
lines[].index requiredinteger
lines[].invitationInvitation
lines[].errorProblem (an error)
created_at requiredstring (date-time)
Example
Invitation batch
{
  "id": "bat_1Qw3Er5Ty7Ui9Op1As3Df5Gh",
  "object": "invitation_batch",
  "assessment_id": "asm_7Q2mX8aB1cD3eF5gH7jK9pL0",
  "status": "queued",
  "total": 2,
  "succeeded": 0,
  "failed": 0,
  "created_at": "2026-10-01T09:40:00Z"
}

Webhook endpoint

An address that receives events. The signing secret appears only when the endpoint is created or its secret is rotated.

FieldTypeDescription
id requiredstring
object requiredstringAlways webhook_endpoint.
url requiredstring (uri)
events requiredarray of string
descriptionstring or null
status requiredstringOne of active, paused.
paused_reasonstring or null
secretstringOnly when created or rotated. Keep it like a password.
livemode requiredboolean
created_at requiredstring (date-time)
Example
Webhook endpoint
{
  "id": "whk_2Wq4Er6Ty8Ui0Op2As4Df6Gh",
  "object": "webhook_endpoint",
  "url": "https://ats.example.com/hooks/iqm",
  "events": [
    "result.ready",
    "invitation.expired"
  ],
  "description": "Our ATS",
  "status": "active",
  "paused_reason": null,
  "livemode": true,
  "created_at": "2026-10-01T10:02:00Z"
}

Webhook delivery

One event sent to one endpoint, with its attempts.

FieldTypeDescription
id requiredstring
object requiredstringAlways webhook_delivery.
event_id requiredstring
event_type requiredstringOne of result.ready, invitation.opened, session.started, invitation.expired, invitation.bounced, usage.threshold_reached, shared_session.completed, webhook.test.
status requiredstringOne of pending, succeeded, failed.
attempts requiredinteger
last_status_codeinteger or null
next_attempt_atstring (date-time) or null
created_at requiredstring (date-time)
Example
Webhook delivery
{
  "id": "dlv_9Zx7Cv5Bn3Mm1Ll9Kk7Jj5Hh",
  "object": "webhook_delivery",
  "event_id": "evt_6Yh4Tg2Rf0Ed8Ws6Qa4Zx2Cv",
  "event_type": "result.ready",
  "status": "succeeded",
  "attempts": 1,
  "last_status_code": 200,
  "next_attempt_at": null,
  "created_at": "2026-10-03T12:15:00Z"
}

Event

The body of every webhook. It carries IDs only: fetch the objects with your key.

FieldTypeDescription
id requiredstring
type requiredstringOne of result.ready, invitation.opened, session.started, invitation.expired, invitation.bounced, usage.threshold_reached, shared_session.completed, webhook.test.
created_at requiredstring (date-time)
livemode requiredboolean
data requiredobjectThe 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_idstring
data.result_idstring
data.assessment_idstring
data.candidate_idstring
data.drive_idstring
data.metricstring
data.threshold_percentintegerOne of 80, 100.
data.shared_session_idstring
data.test_idstringFor shared_session.completed, the shared test’s ID.
Example
Event
{
  "id": "evt_6Yh4Tg2Rf0Ed8Ws6Qa4Zx2Cv",
  "type": "result.ready",
  "created_at": "2026-10-03T12:15:00Z",
  "livemode": true,
  "data": {
    "invitation_id": "inv_8KD2wT4yU6iO8pA0sD2fG4hJ",
    "result_id": "res_5Rt7Yu9Io1Pa3Sd5Fg7Hj9Kl",
    "assessment_id": "asm_7Q2mX8aB1cD3eF5gH7jK9pL0",
    "candidate_id": "cnd_3Nx5Bv7Mc9Lk1Jh3Gf5Ds7Aq"
  }
}

Usage

Counts for the current period, with the limit and when it resets.

FieldTypeDescription
object requiredstringAlways usage.
livemode requiredboolean
counters requiredarray of object
counters[].metric requiredstring
counters[].used requiredinteger
counters[].reserved requiredintegerHeld by open invitations when the billed unit is a completed test.
counters[].limit requiredinteger or null
counters[].period_start requiredstring (date-time)
counters[].resets_at requiredstring (date-time) or null
Example
Usage
{
  "object": "usage",
  "livemode": true,
  "counters": [
    {
      "metric": "invitations",
      "used": 42,
      "reserved": 0,
      "limit": null,
      "period_start": "2026-10-01T00:00:00Z",
      "resets_at": "2026-11-01T00:00:00Z"
    }
  ]
}

Test

A ready-made test from the catalog.

FieldTypeDescription
id requiredstring
object requiredstringAlways test.
name requiredstring
description requiredstring
categories requiredarray of object
categories[].category requiredstring
categories[].questions requiredinteger
questions requiredinteger
time_limit_minutes requiredinteger
languages requiredarray of string
reliabilitynumber or nullMeasured reliability (KR-20), or null until measured. Only measured values are ever shown.
comparison_groupobject or null
comparison_group.name requiredstring
comparison_group.size requiredinteger
comparison_group.provisional requiredbooleanTrue 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.

FieldTypeDescription
id requiredstring
object requiredstringAlways category.
name requiredstring
domain requiredstring
topics requiredarray of object
topics[].id requiredstring
topics[].name requiredstring
min_questions requiredinteger
max_questions requiredinteger
recommended_questions requiredintegerEnough for a reliable sub-score.
seconds_per_questionintegerThe typical time per question.
difficulties requiredarray of string
languages requiredarray 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"
  ]
}

Role template

A ready combination of tests for a common role.

FieldTypeDescription
id requiredstring
object requiredstringAlways role_template.
name requiredstring
tests requiredarray of string
time_limit_minutes requiredinteger
Example
Role template
{
  "id": "finance-accounting",
  "object": "role_template",
  "name": "Finance / accounting",
  "tests": [
    "numerical-reasoning",
    "attention-to-detail"
  ],
  "time_limit_minutes": 30
}

Shared test

A test from the shared pool, for other websites. Its ID starts a shared session; it is never a hiring test.

FieldTypeDescription
id requiredstring
object requiredstringAlways shared_test.
name requiredstring
description requiredstring
categories requiredarray of object
categories[].category requiredstring
categories[].questions requiredinteger
questions requiredinteger
time_limit_minutes requiredinteger
languages requiredarray of string
reliabilitynumber or nullMeasured reliability, or null until measured. Only measured values are ever shown.
comparison_groupobject or null
comparison_group.name requiredstring
comparison_group.size requiredinteger
comparison_group.provisional requiredbooleanTrue 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.

FieldTypeDescription
id requiredstring
object requiredstringAlways shared_session.
livemode requiredboolean
test_id requiredstring
reference requiredstring or null
status requiredstringOne of active, completed.
created_at requiredstring (date-time)
deadline requiredstring (date-time) or nullWhen the time runs out.
seconds_left requiredinteger or nullWhile active, the seconds until the deadline.
time_limit_seconds requiredinteger
answered requiredintegerThe questions already behind the person.
total requiredintegerThe questions of the test, trial questions included.
unit requiredobject or nullWhile active, the unit on screen; null once the session is over.
unit.type requiredstringOne of question, set.
unit.seq requiredintegerThe number of its first question; send it as unit. At least 1.
unit.stimulus requiredobject or nullFor a set, its shared material; null for a question on its own.
unit.stimulus.kind requiredstringOne of text, table, picture.
unit.stimulus.title requiredstring or null
unit.stimulus.text requiredstring or null
unit.stimulus.columns requiredarray of string
unit.stimulus.rows requiredarray of array of string
unit.stimulus.note requiredstring or null
unit.stimulus.picture requiredobject or null
unit.stimulus.picture.svg requiredstringAn SVG document rebuilt from an allowlist. Show it as an image.
unit.stimulus.picture.alt requiredstringIts text alternative.
unit.questions requiredarray of objectUp to 6 items.
unit.questions[].seq requiredintegerAt least 1.
unit.questions[].category requiredstringThe category’s name.
unit.questions[].format requiredstringOne of single_choice, true_false_cannot_say, picture_choice, numeric.
unit.questions[].stem requiredstringPlain text; line breaks are meaningful.
unit.questions[].picture requiredobject or null
unit.questions[].picture.svg requiredstringAn SVG document rebuilt from an allowlist. Show it as an image.
unit.questions[].picture.alt requiredstringIts text alternative.
unit.questions[].options requiredarray of object or objectIn this session’s order. Pictures for picture_choice, none for numeric. Up to 6 items.
unit.questions[].decimals requiredinteger or nullFor numeric, the decimal places asked for. 0 to 6.
unit.questions[].unit requiredstring or nullFor numeric, a unit to show after the box, such as “%”.
unit.questions[].answer requiredstring or nullAlways null here; answers are sent per unit.
result requiredobject or nullOnce the session is over, its result; null while it runs.
result.correct requiredintegerScored questions answered correctly. Trial questions do not count.
result.total requiredintegerScored questions.
result.categories requiredarray of object
result.categories[].category requiredstringA category ID, or, for a section of your own skill, the skill’s ID.
result.categories[].name requiredstring
result.categories[].correct requiredinteger
result.categories[].total requiredinteger
result.categories[].source requiredstringWhere 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[].band requiredstring or nullA 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_seconds requiredinteger
result.time_limit_seconds requiredinteger
result.finished requiredbooleanFalse when the time ran out.
result.integrity requiredstringThe test ran in your design, so no integrity signals were collected. One of not_monitored.
result.scoring_version requiredstring
result.completed_at requiredstring (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.

FieldTypeDescription
type requiredstringA URI for the problem type; about:blank for now.
title requiredstring
status requiredinteger400 to 599.
detailstring
code requiredstringOne 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_id requiredstring
errorsarray of objectFor validation_failed, one entry per field.
errors[].field requiredstringThe field’s path, such as sections[0].questions.
errors[].code requiredstringA 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[].message requiredstring
limitobjectFor limit_reached.
limit.metric requiredstring
limit.used requiredinteger
limit.allowed requiredinteger
limit.resets_atstring (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.

Already have an account? Sign in. New here? What the API does and the company dashboard.