Skip to content

Scoped API keys and signed webhooks

New integrations should use named, scoped API keys. Each key can be revoked without interrupting other integrations and receives only the permissions it needs. Legacy organization API Secret Keys remain compatible during migration.

Create a scoped API key

An Administrator creates keys from the organization's developer settings. The complete token is displayed only once. Live tokens begin with exm_live.; developer sandbox tokens begin with exm_test.. Store each token in a server-side secret manager.

Scope Allows
examinees:read Read examinee records through existing endpoints
examinees:write Create, update, and bulk-upsert examinees
exams:read Read exam definitions
exams:write Upload, configure, tag, and delete exams
groups:read Read groups and their membership
groups:write Create groups and change membership
assignments:read Read exam assignments
assignments:write Create, change, and delete unstarted assignments
results:read Read completed results and paper summaries
sessions:write Create single-use exam launch URLs
webhooks:read List endpoints and delivery history
webhooks:write Create endpoints, disable endpoints, and retry deliveries

Authenticate using the Bearer scheme:

curl --request GET \
  --header "Authorization: Bearer $EXAMINA_API_KEY" \
  --header "Accept: application/json" \
  "https://www.examina.io/api/v1/results?page=1&pageSize=25"

Do not place API keys in browser code, mobile applications, screenshots, source control, or support logs.

API keys are environment-bound. An exm_live. key works only on the live API. An exm_test. key works only at https://sandbox.examina.io/api/v1. Legacy Basic Authentication is accepted only by the live API.

Make mutations idempotent

Creation and update endpoints require an Idempotency-Key header. Generate a unique value for the logical operation and reuse it only when retrying that same request:

curl --request POST \
  --header "Authorization: Bearer $EXAMINA_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: candidate-import-2026-08-23-0001" \
  --data '{"code":"CANDIDATE-42","passcode":"temporary-secret","firstName":"Ada","lastName":"Okafor"}' \
  "https://www.examina.io/api/v1/examinees"

The key is retained for at least 24 hours. Repeating it with an identical body returns the original resource. Reusing it with different data returns HTTP 409.

Configure a signed webhook

Create an endpoint subscribed to result.completed:

curl --request POST \
  --header "Authorization: Bearer $EXAMINA_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: webhook-results-v1" \
  --data '{"url":"https://integrator.example/webhooks/examina","events":["result.completed"]}' \
  "https://www.examina.io/api/v1/webhook-endpoints"

The response includes a signingSecret beginning with whsec_. It is shown only once. Webhook URLs must use public HTTPS and must not resolve to a private, loopback, link-local, or multicast address.

Every delivery contains a JSON event. The request also includes:

The event envelope includes livemode and environment. Sandbox deliveries use "livemode": false and "environment": "test"; live deliveries use true and "live". Reject an unexpected environment before processing data.

Header Meaning
X-Examina-Event-Id Stable event identifier for deduplication
X-Examina-Timestamp Unix timestamp used in the signature
X-Examina-Signature v1= followed by the hexadecimal HMAC-SHA256 signature

Concatenate the timestamp, a period, and the exact raw request body. Calculate HMAC-SHA256 with the signing secret and compare it to the v1 signature using a constant-time comparison:

signed_content = timestamp + "." + raw_request_body
expected = hex(HMAC_SHA256(signing_secret, signed_content))

Return a 2xx response quickly and queue longer processing. Use the event ID to deduplicate processing, then retrieve the authoritative result from GET /results/{assignmentId}.

Inspect and retry deliveries

curl --header "Authorization: Bearer $EXAMINA_API_KEY" \
  "https://www.examina.io/api/v1/webhook-endpoints/deliveries?page=1&pageSize=25"

curl --request POST \
  --header "Authorization: Bearer $EXAMINA_API_KEY" \
  "https://www.examina.io/api/v1/webhook-endpoints/deliveries/DELIVERY_ID/retry"

The earlier organization-level form callback remains available for existing integrations but is deprecated. New integrations should use signed endpoint resources because they provide event IDs, signatures, delivery state, and replay.

Rotate or revoke credentials

Create a replacement key, deploy it to every consumer, verify successful calls, and then revoke the previous key. Because keys are independent, rotation does not require a simultaneous cutover. Revoke a key immediately if it may have been exposed.