The platform API Reference

iTestHub Client API

Every endpoint, parameter and field, rendered from the same OpenAPI document your code generator should read. Nothing here is written out by hand, so nothing here can describe an endpoint that does not exist.

Version
1.0.0
Base URL
https://app.itesthub.com
Specification
OpenAPI 3.0.3
Operations
7

Overview

How this API behaves

The interface an applicant tracking system integrates against: order a psychometric assessment for a candidate, send them the link, and pull back a scored result that names the exact test version and norm group it was scored against.

Six endpoints, one credential, one signed webhook -- and a sandbox you can build the whole integration against before a single real candidate exists.

Authentication. Every request carries a client API key as Authorization: Bearer ith_live_.... A key belongs to exactly one client and carries a set of scopes; each operation below states the scope it needs. Keys are issued by iTestHub -- there is no self-serve signup. A request without a valid key is 401; a valid key without the required scope is 403.

Test mode. A sandbox key is ith_test_... and is issued the same way a live one is. Everything it creates is test data and it can read, write and count nothing real; a live key likewise never sees anything a test key made. Sandbox candidates are never emailed, in any ordering mode, and sandbox results never reach norm data. POST /api/v1/sandbox/results/{result_id}/complete drives an order to completion on demand, with outcome of high, typical or low, so you can read back a scored result, fetch its report and receive a signed assessment.completed webhook -- with livemode: false in its envelope -- within a second of placing the order. The result is produced by the same scorer and the same serialiser a real one is: there is one shape of result in this API, not two.

Tenancy. A key reaches its own client's orders, results and reports, in its own environment, and nothing else. Another client's id -- or the other environment's -- returns the same 404 an id that never existed returns, so the answer never confirms that an object is real.

Ids and timestamps. Ids are opaque strings (ord_..., res_...). They are not database keys and are not sequential: store them, do not parse them. A listing cursor is opaque in the same way -- ours to issue, yours to hand back. Timestamps are ISO-8601, UTC, Z-suffixed.

Errors. Every error is {"code", "detail"}, plus "fields" when a request body failed validation field by field. Branch on code; show detail to a human.

Rate limits. Budgets are per key rather than per client, so two of your own integrations never starve each other. Over the limit is 429 with a Retry-After header in seconds.

Retries. POST /api/v1/orders accepts an Idempotency-Key header. Replaying it with the same body returns the original order and creates nothing; replaying it with a different body is 409.

This document is generated from the running code, and the human reference page at /platform/api/reference/ is rendered from this document. The narrative integration guide is at /platform/api/.

Authentication

One key, sent as a bearer token

clientApiKey

Type
http
Scheme
bearer
Key format
ith_live_<prefix>_<secret> | ith_test_<prefix>_<secret>

Your client API key, as Authorization: Bearer ith_live_.... Issued by iTestHub and shown once; it identifies your organisation and carries the scopes your integration was granted. A sandbox key is ith_test_... and is the same credential in every other respect -- it simply reaches test data instead of real data, and nothing either way round.

Endpoints

Every operation in the document

GET /api/v1/assessments

List the assessments this client can order

  • Scope assessments:read
  • Auth clientApiKey
  • Operation id api_v1_assessments_retrieve

Everything approved for client use in the iTestHub catalogue, with the code you order it by, how long it takes, what device it needs and the norm group its scores are compared against.

Scope: assessments:read.

GET /api/v1/assessments
curl -X GET "https://app.itesthub.com/api/v1/assessments" \
  -H "Authorization: Bearer $ITESTHUB_KEY"

A worked example with a real payload is on the API overview.

Responses

200 application/json

The catalogue this key's client can order from.

Response shape — types, not values
{
  "data": [
    {
      "code": "string",
      "name": "string",
      "type": "string or null",
      "description": "string",
      "time_estimate_minutes": "integer or null",
      "time_limit_minutes": "integer or null",
      "timed": "boolean",
      "device_requirement": "string",
      "norm_group": "string or null",
      "norms_provisional": "boolean"
    }
  ]
}
GET /api/v1/assessments 200 response fields
Name Type Required Notes
data array of object Required
data[].code string Required

The code to order this assessment by.

data[].name string Required
data[].type string or null Required

ability | sjt | personality | game

data[].description string Required
data[].time_estimate_minutes integer or null Required
data[].time_limit_minutes integer or null Required

Null for an untimed assessment.

data[].timed boolean Required
data[].device_requirement string Required

any | large_recommended | large_required

data[].norm_group string or null Required

The group scores are compared against.

data[].norms_provisional boolean Required

True while the norms are assumed rather than observed; standardised scores are withheld.

Error responses

Each of these carries the error envelope described under Errors.

GET /api/v1/assessments error responses
Status When
401

Missing, malformed, expired or revoked key.

403

The key lacks the scope this endpoint requires.

404

No such object for this client.

429

Rate limited. Retry after the Retry-After header.

GET /api/v1/orders

List your orders, to reconcile against your own records

  • Scope orders:write
  • Auth clientApiKey
  • Operation id api_v1_orders_list

Every order you have placed, newest first, with the status of each assessment on it. This is the reconciliation endpoint: we report status per reference, you hold the identity, you reconcile. Filter by your OWN references -- reference for the requisition and candidate_reference for an anonymously ordered candidate -- so you never have to store an iTestHub id to find a candidate again.

It matters most for anonymous orders, where nobody can ask us whether a particular person was invited, because we do not know who they are. Diff this listing against your own records and the difference is the list of references to chase. link_first_opened_at: null with every assessment still pending means the link has never been followed by anyone -- the signal that an invitation never arrived, as distinct from started-and-not-finished.

Paginated by opaque cursor: follow next_cursor until it is null. status matches an order with AT LEAST ONE assessment in that state.

Scope: orders:write -- the scope that places orders follows them.

GET /api/v1/orders
curl -X GET "https://app.itesthub.com/api/v1/orders" \
  -H "Authorization: Bearer $ITESTHUB_KEY"

A worked example with a real payload is on the API overview.

Query parameters

GET /api/v1/orders query parameters
Name Type Required Notes
candidate_reference string Optional

Exact match on an anonymous candidate's reference.

created_after string Optional

ISO-8601. Inclusive.

created_before string Optional

ISO-8601. Exclusive.

cursor string Optional

From a previous response's next_cursor.

limit integer Optional

1-200. Default 50.

mode string Optional

identified | identified_with_invitation | anonymous.

reference string Optional

Exact match on your requisition reference.

status string Optional

pending | in_progress | completed | expired (at least one assessment).

updated_after string Optional

ISO-8601. Latest start/completion on the order, or its creation.

updated_before string Optional

ISO-8601. Exclusive.

Responses

200 application/json

One page of orders, newest first.

Response shape — types, not values
{
  "data": [
    {
      "id": "string",
      "mode": "string",
      "reference": "string",
      "created_at": "string (date-time)",
      "expires_at": "string (date-time)",
      "candidate": {},
      "candidate_url": "string",
      "invitation_sent": "boolean",
      "link_first_opened_at": "string (date-time) or null",
      "assessments": [
        {
          "id": "string",
          "assessment_code": "string",
          "assessment_name": "string",
          "status": "string",
          "started_at": "string (date-time) or null",
          "completed_at": "string (date-time) or null",
          "candidate_url": "string"
        }
      ]
    }
  ],
  "next_cursor": "string or null",
  "has_more": "boolean"
}
GET /api/v1/orders 200 response fields
Name Type Required Notes
data array of object Required
data[].id string Required
data[].mode string Required

identified | identified_with_invitation | anonymous

data[].reference string Required
data[].created_at string (date-time) Required
data[].expires_at string (date-time) Required
data[].candidate object Required

{first_name, last_name, email} for an identified order; {reference} for an anonymous one. An anonymous order's candidate block carries no personal keys at all -- not even empty ones.

data[].candidate_url string Required

The link to send the candidate. Covers every assessment below.

data[].invitation_sent boolean Required

Whether iTestHub emailed the candidate. Only ever true for identified_with_invitation.

data[].link_first_opened_at string (date-time) or null Required

When the candidate's link was first opened, or null if it never has been. Null with every assessment still pending is the signal that an invitation never arrived.

data[].assessments array of object Required
data[].assessments[].id string Required

The result id: GET /api/v1/results/{id}.

data[].assessments[].assessment_code string Required
data[].assessments[].assessment_name string Required
data[].assessments[].status string Required

pending | in_progress | completed | expired

data[].assessments[].started_at string (date-time) or null Required
data[].assessments[].completed_at string (date-time) or null Required
data[].assessments[].candidate_url string Optional
next_cursor string or null Required

Pass as cursor for the next page. Null on the last page.

has_more boolean Required

Error responses

Each of these carries the error envelope described under Errors.

GET /api/v1/orders error responses
Status When
400

A filter or cursor this endpoint cannot read.

401

Missing, malformed, expired or revoked key.

403

The key lacks the scope this endpoint requires.

404

No such object for this client.

429

Rate limited. Retry after the Retry-After header.

POST /api/v1/orders

Order assessments for a candidate

  • Scope orders:write
  • Auth clientApiKey
  • Operation id api_v1_orders_create

Creates the candidate and their assignments and returns the link to send them.

mode chooses one of three ways to order, and is the only thing that decides whether we hold personal data or send an email:

  • identified (the default, and what every order did before this field existed) -- you give us the candidate's name and email, we return candidate_url, and iTestHub sends NO email. You invite them, in your own branding.
  • identified_with_invitation -- the same, and iTestHub emails the candidate the invitation and can send reminders.
  • anonymous -- you send candidate.reference and nothing else: your own opaque id for this person. iTestHub never receives, and so never stores, a name or an email address, and cannot email anyone. Any personal field in the body is a 400 naming the field -- it is rejected, never silently dropped.

Send an Idempotency-Key header (any string unique to this order in your system, e.g. your own application id). Replaying it with the same body returns the original order and creates nothing; replaying it with a different body is 409 idempotency_key_reuse.

Scope: orders:write.

POST /api/v1/orders
curl -X POST "https://app.itesthub.com/api/v1/orders" \
  -H "Authorization: Bearer $ITESTHUB_KEY" \
  -H "Idempotency-Key: <idempotency-key>" \
  -H "Content-Type: application/json" \
  --data @body.json

A worked example with a real payload is on the API overview.

Request headers

POST /api/v1/orders request headers
Name Type Required Notes
Idempotency-Key string Optional

Your own unique id for this order. Makes a retry safe.

Request body

Sent as application/json.

Body shape — types, not values
{
  "mode": "identified | identified_with_invitation | anonymous",
  "candidate": {
    "first_name": "string",
    "last_name": "string",
    "email": "string (email)",
    "reference": "string",
    "title": "Mr | Mrs | Ms | ",
    "gender": "string",
    "ethnicity": "string"
  },
  "assessments": [
    "string"
  ],
  "reference": "string",
  "expires_at": "string (date-time)"
}
POST /api/v1/orders request body fields
Name Type Required Notes
mode string Optional

How to place this order.

  • identified (default) -- you give us the candidate's details, we return the link, we send NO email. This is exactly the behaviour of every order placed before this field existed.
  • identified_with_invitation -- as above, and iTestHub emails the candidate their invitation through the platform's own invitation path.
  • anonymous -- you send only candidate.reference, your own opaque id. iTestHub never receives and never stores a name or an address, and therefore cannot email anyone: you send the link. Any personal field in the body is a 400 naming it, never a silent drop.

One of: identified   identified_with_invitation   anonymous

Default identified

candidate object Required

The candidate on an order. Which fields belong here is decided by the order's mode:

  • identified and identified_with_invitation: first_name, last_name and email are required. title, gender and ethnicity are optional demographic fields iTestHub never demands -- they are offered only because norm reporting can use them.
  • anonymous: reference is required and is the ONLY field allowed. Any other field is rejected outright with a 400 naming it; it is never quietly dropped, because a name that was dropped had still reached this server and its logs.
candidate.first_name string Optional

Identified orders: required.

candidate.last_name string Optional

Identified orders: required.

candidate.email string (email) Optional

Identified orders: required. Never sent, and never accepted, on an anonymous order.

candidate.reference string Optional

Anonymous orders: REQUIRED, and the only field allowed. Your own opaque id for this candidate -- an application id, a hash, anything meaningless to iTestHub. It is what you reconcile against, and it is what a client admin sees in place of a name. Not accepted on an identified order.

candidate.title string Optional

One of: Mr   Mrs   Ms  

candidate.gender string Optional

One of: Undisclosed   female   male   they  

candidate.ethnicity string Optional

One of: undisclosed   white   asian   black   mixed   other  

assessments array of string Required

Assessment codes from GET /api/v1/assessments.

reference string Optional

Your own reference for the vacancy or requisition. Candidates ordered under the same reference are grouped together, exactly as a campaign is in the iTestHub web app.

expires_at string (date-time) Optional

The completion deadline for this order, ISO-8601. Defaults to 30 days from now. Past it, an unfinished assessment reports status 'expired'.

Responses

200 application/json

A replay of an earlier Idempotency-Key. Nothing created.

Response shape — types, not values
{
  "id": "string",
  "mode": "string",
  "reference": "string",
  "created_at": "string (date-time)",
  "expires_at": "string (date-time)",
  "candidate": {},
  "candidate_url": "string",
  "invitation_sent": "boolean",
  "link_first_opened_at": "string (date-time) or null",
  "assessments": [
    {
      "id": "string",
      "assessment_code": "string",
      "assessment_name": "string",
      "status": "string",
      "started_at": "string (date-time) or null",
      "completed_at": "string (date-time) or null",
      "candidate_url": "string"
    }
  ]
}
POST /api/v1/orders 200 response fields
Name Type Required Notes
id string Required
mode string Required

identified | identified_with_invitation | anonymous

reference string Required
created_at string (date-time) Required
expires_at string (date-time) Required
candidate object Required

{first_name, last_name, email} for an identified order; {reference} for an anonymous one. An anonymous order's candidate block carries no personal keys at all -- not even empty ones.

candidate_url string Required

The link to send the candidate. Covers every assessment below.

invitation_sent boolean Required

Whether iTestHub emailed the candidate. Only ever true for identified_with_invitation.

link_first_opened_at string (date-time) or null Required

When the candidate's link was first opened, or null if it never has been. Null with every assessment still pending is the signal that an invitation never arrived.

assessments array of object Required
assessments[].id string Required

The result id: GET /api/v1/results/{id}.

assessments[].assessment_code string Required
assessments[].assessment_name string Required
assessments[].status string Required

pending | in_progress | completed | expired

assessments[].started_at string (date-time) or null Required
assessments[].completed_at string (date-time) or null Required
assessments[].candidate_url string Optional
201 application/json

Created.

The same Order body as the 200 response above.

Error responses

Each of these carries the error envelope described under Errors.

POST /api/v1/orders error responses
Status When
400

Invalid body -- including personal data on an anonymous order, with the field named.

401

Missing, malformed, expired or revoked key.

403

The key lacks the scope this endpoint requires.

404

No such object for this client.

409

The same Idempotency-Key with a different body.

415

A body that is not application/json. This API parses JSON only.

422

An assessment code you cannot order.

429

Rate limited. Retry after the Retry-After header.

GET /api/v1/orders/{order_id}

Order status

  • Scope orders:write
  • Auth clientApiKey
  • Operation id api_v1_orders_retrieve

Where each assessment on the order has got to: pending, in_progress, completed or expired (the order's own deadline passed with the assessment unfinished).

Scope: orders:write -- the scope that places orders follows them. Reading the SCORES needs results:read as well.

GET /api/v1/orders/{order_id}
curl -X GET "https://app.itesthub.com/api/v1/orders/{order_id}" \
  -H "Authorization: Bearer $ITESTHUB_KEY"

A worked example with a real payload is on the API overview.

Path parameters

GET /api/v1/orders/{order_id} path parameters
Name Type Required Notes
order_id string Required

The id an order came back with, e.g. ord_9c1d4f0b8a7e4d2fa3c5b6e7d8f90123.

Responses

200 application/json

The order, with the status of each assessment on it.

Response shape — types, not values
{
  "id": "string",
  "mode": "string",
  "reference": "string",
  "created_at": "string (date-time)",
  "expires_at": "string (date-time)",
  "candidate": {},
  "candidate_url": "string",
  "invitation_sent": "boolean",
  "link_first_opened_at": "string (date-time) or null",
  "assessments": [
    {
      "id": "string",
      "assessment_code": "string",
      "assessment_name": "string",
      "status": "string",
      "started_at": "string (date-time) or null",
      "completed_at": "string (date-time) or null",
      "candidate_url": "string"
    }
  ]
}
GET /api/v1/orders/{order_id} 200 response fields
Name Type Required Notes
id string Required
mode string Required

identified | identified_with_invitation | anonymous

reference string Required
created_at string (date-time) Required
expires_at string (date-time) Required
candidate object Required

{first_name, last_name, email} for an identified order; {reference} for an anonymous one. An anonymous order's candidate block carries no personal keys at all -- not even empty ones.

candidate_url string Required

The link to send the candidate. Covers every assessment below.

invitation_sent boolean Required

Whether iTestHub emailed the candidate. Only ever true for identified_with_invitation.

link_first_opened_at string (date-time) or null Required

When the candidate's link was first opened, or null if it never has been. Null with every assessment still pending is the signal that an invitation never arrived.

assessments array of object Required
assessments[].id string Required

The result id: GET /api/v1/results/{id}.

assessments[].assessment_code string Required
assessments[].assessment_name string Required
assessments[].status string Required

pending | in_progress | completed | expired

assessments[].started_at string (date-time) or null Required
assessments[].completed_at string (date-time) or null Required
assessments[].candidate_url string Optional

Error responses

Each of these carries the error envelope described under Errors.

GET /api/v1/orders/{order_id} error responses
Status When
401

Missing, malformed, expired or revoked key.

403

The key lacks the scope this endpoint requires.

404

No such object for this client.

429

Rate limited. Retry after the Retry-After header.

GET /api/v1/results/{result_id}

Fetch a result

  • Scope results:read
  • Auth clientApiKey
  • Operation id api_v1_results_retrieve

Before completion this returns the status and nothing else -- there is no partial score. Once complete it carries the raw score, the standardised scores, the percentile and band, any competency/scale breakdown, the norm group, and scored_against: the exact test version, its content hash and the norms the answers were scored under. That provenance is not decoration -- it is what lets you defend a score months later, when the catalogue has moved on.

Scope: results:read.

GET /api/v1/results/{result_id}
curl -X GET "https://app.itesthub.com/api/v1/results/{result_id}" \
  -H "Authorization: Bearer $ITESTHUB_KEY"

A worked example with a real payload is on the API overview.

Path parameters

GET /api/v1/results/{result_id} path parameters
Name Type Required Notes
result_id string Required

The id of one assessment on an order -- the assessments[].id an order response carries, e.g. res_4b2e6a1c9d3f4a5b8c7d6e5f4a3b2c1d.

Responses

200 application/json

The result. The scores and scored_against appear only once status is completed.

Response shape — types, not values
{
  "id": "string",
  "order_id": "string",
  "assessment": {},
  "status": "string",
  "completed_at": "string (date-time) or null",
  "scored_against": {
    "test_version_id": "integer or null",
    "test_version_number": "integer or null",
    "content_hash": "string or null",
    "norm_source": "string",
    "norm_group": "string or null",
    "form": "string"
  },
  "norms_provisional": "boolean",
  "scores": {},
  "report_types": [
    {}
  ]
}
GET /api/v1/results/{result_id} 200 response fields
Name Type Required Notes
id string Required
order_id string Required
assessment object Required
status string Required

pending | in_progress | completed | expired

completed_at string (date-time) or null Optional
scored_against object Optional
scored_against.test_version_id integer or null Required
scored_against.test_version_number integer or null Required
scored_against.content_hash string or null Required
scored_against.norm_source string Required

version | live

scored_against.norm_group string or null Required
scored_against.form string Required
norms_provisional boolean Optional
scores object Optional
report_types array of object Optional

Error responses

Each of these carries the error envelope described under Errors.

GET /api/v1/results/{result_id} error responses
Status When
401

Missing, malformed, expired or revoked key.

403

The key lacks the scope this endpoint requires.

404

No such object for this client.

429

Rate limited. Retry after the Retry-After header.

GET /api/v1/results/{result_id}/report

Download a result's report PDF

  • Scope reports:read
  • Auth clientApiKey
  • Operation id api_v1_results_report_retrieve

application/pdf. ?type= chooses the report; the types available for a given result are listed as report_types on the result itself. Asking for a type a questionnaire does not have returns 404 report_unavailable with the reason and the list -- it never silently sends a different report.

Scope: reports:read.

GET /api/v1/results/{result_id}/report
curl -X GET "https://app.itesthub.com/api/v1/results/{result_id}/report" \
  -H "Authorization: Bearer $ITESTHUB_KEY"

A worked example with a real payload is on the API overview.

Path parameters

GET /api/v1/results/{result_id}/report path parameters
Name Type Required Notes
result_id string Required

The same result id GET /api/v1/results/{result_id} takes.

Query parameters

GET /api/v1/results/{result_id}/report query parameters
Name Type Required Notes
type string Optional

A report type from the result's own report_types. Defaults to the profile report.

Responses

200 application/pdf

The report PDF.

Error responses

Each of these carries the error envelope described under Errors.

GET /api/v1/results/{result_id}/report error responses
Status When
401

Missing, malformed, expired or revoked key.

403

The key lacks the scope this endpoint requires.

404

No such result, or no such report for it.

409

The assessment is not completed yet.

429

Rate limited. Retry after the Retry-After header.

POST /api/v1/sandbox/results/{result_id}/complete

Sandbox only: drive an assessment to completion

  • Scope orders:write
  • Auth clientApiKey
  • Operation id api_v1_sandbox_results_complete_create

Sandbox only. A live key (ith_live_...) gets 403 sandbox_only here, before any lookup.

Answers this assessment on the candidate's behalf and finalises it through iTestHub's own completion path, so within a second of ordering you have a completed result with real scores, the scored_against provenance, a downloadable report and a signed assessment.completed webhook with livemode: false. It is the same scorer, the same serialiser and the same webhook emitter a real candidate's submission runs: there is no second shape of result anywhere in this API.

outcome is high, typical (the default) or low. It moves the distribution the simulated answers are drawn from rather than fixing a number, so repeated calls give different raw scores, percentiles and bands -- which is what you want when you are building a results screen.

Completing a result twice is 409 already_completed: an id you have already stored must not quietly start reporting a different score. Order again for another result.

Scope: orders:write -- the scope that places orders drives them.

POST /api/v1/sandbox/results/{result_id}/complete
curl -X POST "https://app.itesthub.com/api/v1/sandbox/results/{result_id}/complete" \
  -H "Authorization: Bearer $ITESTHUB_KEY" \
  -H "Content-Type: application/json" \
  --data @body.json

A worked example with a real payload is on the API overview.

Path parameters

POST /api/v1/sandbox/results/{result_id}/complete path parameters
Name Type Required Notes
result_id string Required

The same result id GET /api/v1/results/{result_id} takes.

Request body

Sent as application/json.

Body shape — types, not values
{
  "outcome": "high | typical | low"
}
POST /api/v1/sandbox/results/{result_id}/complete request body fields
Name Type Required Notes
outcome string Optional

How the simulated candidate performs: high, typical (the default) or low. Scores, percentiles and bands vary from call to call within the outcome you asked for.

One of: high   typical   low

Responses

200 application/json

The completed result -- byte for byte what GET /api/v1/results/{result_id} returns.

Response shape — types, not values
{
  "id": "string",
  "order_id": "string",
  "assessment": {},
  "status": "string",
  "completed_at": "string (date-time) or null",
  "scored_against": {
    "test_version_id": "integer or null",
    "test_version_number": "integer or null",
    "content_hash": "string or null",
    "norm_source": "string",
    "norm_group": "string or null",
    "form": "string"
  },
  "norms_provisional": "boolean",
  "scores": {},
  "report_types": [
    {}
  ]
}
POST /api/v1/sandbox/results/{result_id}/complete 200 response fields
Name Type Required Notes
id string Required
order_id string Required
assessment object Required
status string Required

pending | in_progress | completed | expired

completed_at string (date-time) or null Optional
scored_against object Optional
scored_against.test_version_id integer or null Required
scored_against.test_version_number integer or null Required
scored_against.content_hash string or null Required
scored_against.norm_source string Required

version | live

scored_against.norm_group string or null Required
scored_against.form string Required
norms_provisional boolean Optional
scores object Optional
report_types array of object Optional

Error responses

Each of these carries the error envelope described under Errors.

POST /api/v1/sandbox/results/{result_id}/complete error responses
Status When
400

An outcome that is not one of the three.

401

Missing, malformed, expired or revoked key.

403

sandbox_only: this endpoint does not exist for a live key.

404

No such object for this client.

409

already_completed: this result already has a score.

429

Rate limited. Retry after the Retry-After header.

Errors

One envelope, and the codes inside it

The envelope

Error — types, not values
{
  "code": "string",
  "detail": "string",
  "fields": {
    "<key>": [
      "string"
    ]
  }
}
Error envelope fields
Name Type Required Notes
code string Required

Machine-readable error code. Branch on this, not on detail.

One of: unauthenticated   forbidden   not_found   report_unavailable   validation_error   unknown_assessment   sandbox_only   already_completed   not_completed   idempotency_key_reuse   rate_limited   method_not_allowed   unsupported_media_type

detail string Required

A human-readable explanation.

fields object of array of string Optional

Per-field problems.

The codes

Error codes
Code Status Means
unauthenticated 401

No key, a malformed key, a revoked key, an expired key

forbidden 403

Your key lacks the scope this endpoint needs

not_found 404

No such object for your client (the same answer an id that never existed gets)

report_unavailable 404

No report of that type for this assessment; available_types lists what there is

validation_error 400

The request body is not valid; see fields

unknown_assessment 422

An assessment code you cannot order

sandbox_only 403

That endpoint exists only in the sandbox; your key is a live key

already_completed 409

That sandbox assessment has already been completed

not_completed 409

You asked for a report for an assessment that is not finished

idempotency_key_reuse 409

That Idempotency-Key was used for a different order

rate_limited 429

Slow down; see Retry-After

method_not_allowed 405

Wrong HTTP method for that path

unsupported_media_type 415

This is a JSON API: send Content-Type: application/json

The document

Point your code generator at this

Everything above is rendered from one OpenAPI document, served without authentication. If you are generating a client, read that rather than this page: the page is the document, formatted.

What is not in it

The document describes the client API and nothing else. The platform's other machine interfaces are internal, are not published, and are not something an integration should be built against. Webhook delivery is not an endpoint you call, so it has no entry here; it is described on the API overview.

Webhooks on the API overview

Something here not clear enough?

Keys are issued by us, and so is the answer to an awkward question about a field. Tell us which client the key is for and what you are building.