jeCheckDevelopers
Get an API key
Developers/API reference/Employee testing

Employee testing

Knowledge tests for employees: build tests and questions, assign them to people, positions or locations, read results, grade open answers — and take assigned tests as the acting user. Results are limited to employees of the locations of the testing.results / testing.review permissions.

Operations
GET/testing/testsList tests GET/testing/tests/{id}Retrieve a test POST/testing/testsCreate a test PATCH/testing/tests/{id}Update test settings POST/testing/tests/{id}/publishPublish a test POST/testing/tests/{id}/statusChange test status POST/testing/tests/{id}/public-linkRegenerate the public link POST/testing/tests/{id}/duplicateDuplicate a test DELETE/testing/tests/{id}Delete a test GET/testing/tests/{id}/analyticsGet test analytics POST/testing/tests/{id}/sectionsAdd a section PATCH/testing/tests/{id}/sections/{section_id}Update a section DELETE/testing/tests/{id}/sections/{section_id}Delete a section POST/testing/tests/{id}/questionsAdd a question POST/testing/tests/{id}/questions/batchAdd questions in bulk PATCH/testing/tests/{id}/questions/{question_id}Update a question POST/testing/tests/{id}/questions/{question_id}/duplicateDuplicate a question DELETE/testing/tests/{id}/questions/{question_id}Delete a question POST/testing/tests/{id}/imagesUpload a question image GET/testing/tests/{id}/assignmentsList assignments of a test POST/testing/tests/{id}/assignmentsAssign a test GET/testing/assignments/{id}Retrieve an assignment PATCH/testing/assignments/{id}Update an assignment POST/testing/assignments/{id}/syncAdd new matching employees POST/testing/assignments/{id}/remindRemind assignees DELETE/testing/assignments/{id}Delete an assignment POST/testing/assignees/{id}/restartReset attempts of an employee POST/testing/assignees/{id}/remindRemind an employee PATCH/testing/assignees/{id}Change the deadline of an employee DELETE/testing/assignees/{id}Remove an employee from an assignment GET/testing/attemptsList attempts GET/testing/attempts/{uuid}Retrieve an attempt POST/testing/attempts/{uuid}/gradeGrade an open answer POST/testing/attempts/{uuid}/annulAnnul an attempt POST/testing/attempts/{uuid}/confirmConfirm an attempt result GET/testing/myList my assigned tests GET/testing/my/historyList my finished attempts POST/testing/my/startStart or continue my test GET/testing/my/attempts/{uuid}Get my attempt state POST/testing/my/attempts/{uuid}/answerSave an answer POST/testing/my/attempts/{uuid}/finishFinish my attempt GET/testing/my/attempts/{uuid}/resultGet my attempt result

GETList tests

GET /testing/tests

All tests of the company with statistics limited to the locations of the testing.results permission.

Token scoperead
Permission testing.manage or testing.results
Plan featuretesting
MCP tooltesting_tests_list · read-only

Query parameters

page integer
Page number, starting from 1. Default 1. Minimum 1.
per_page integer
Items per page (max 100). Default 25. Value 1–100.
status array<string>
Filter by status: draft, published, archived.
search string
Search in title and category. Maximum length 200.
updated_since date-time
Only tests changed after this moment.

Responses

StatusDescription
200OK — a page of results with meta
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage / testing.results, or the plan has no testing
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/tests?search=fridge' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests?search=fridge', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data, meta } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/tests",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    params={"search":"fridge"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/tests', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'query' => [
        'search' => 'fridge',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_list",
        "arguments": {
            "search": "fridge"
        }
    }
}
Response 200
{
  "data": [
    {
      "id": 58,
      "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
      "title": "Food safety basics",
      "description": "For new kitchen staff",
      "category": "Kitchen",
      "status": "published",
      "strict": false,
      "public_enabled": false,
      "time_limit_minutes": 20,
      "pass_score": 80,
      "questions_count": 15,
      "attempts": 42,
      "avg_percent": 84.5,
      "pass_rate": 76,
      "assigned": 30,
      "assigned_passed": 22,
      "overdue": 3,
      "pending_review": 1,
      "last_attempt_at": "2026-11-02T16:20:00+05:00",
      "updated_at": "2026-10-20T10:00:00+05:00",
      "url": "https://app.jecheck.io/ui/testing/58/results"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "has_more": false
  }
}

GETRetrieve a test

GET /testing/tests/{id}

Test settings. Sections with questions and correct answers (sections, problems) are returned only with testing.manage; otherwise they are null.

Token scoperead
Permission testing.manage or testing.results
Plan featuretesting
MCP tooltesting_tests_get · read-only

Path parameters

id integer Required
Test id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage / testing.results, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/tests/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/tests/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_get",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
      "enabled": false,
      "camera": true,
      "identity_photo": true,
      "snapshot_interval": 60,
      "fullscreen": true,
      "block_copy": true,
      "face_detection": true,
      "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
      {
        "id": 77,
        "title": "Storage",
        "pick_count": null,
        "questions": [
          {
            "id": 901,
            "section_id": 77,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "options": {
              "choices": [
                {
                  "id": "a1b2c3d4",
                  "text": "0…+4 °C",
                  "correct": true
                },
                {
                  "id": "e5f6g7h8",
                  "text": "+8…+12 °C",
                  "correct": false
                }
              ]
            },
            "points": 1,
            "partial_credit": false,
            "is_critical": true,
            "explanation": "SanPiN 2.3.6",
            "problems": []
          }
        ]
      }
    ]
  }
}

POSTCreate a test

POST /testing/tests

Creates a draft test with one section. Optionally pass questions to fill it in one call, then publish it.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_tests_create

Request body

title string Required
Test title. Length 2–255.
description string | null
Maximum length 5000.
category string | null
Maximum length 120.
cover_color string | null
Card colour, e.g. #2563eb. Maximum length 20.
time_limit_minutes integer | null
Time limit (null — unlimited). Value 1–600.
pass_score integer
Pass threshold, percent. Value 0–100.
attempts_limit integer | null
Attempts per assignment cycle (null — unlimited). Value 1–100.
retry_cooldown_hours integer | null
Pause between attempts, hours. Value 0–8760.
shuffle_questions boolean
shuffle_options boolean
navigation string
linear — no going back to previous questions.
freelinear
show_result string
What the employee sees after finishing.
nonescoreanswersfull
allow_app boolean
Can be taken in the mobile app.
allow_web boolean
Can be taken on the website.
certificate_enabled boolean
Issue a certificate on passing.
certificate_valid_days integer | null
Certificate validity; employees are re-assigned when it expires. Value 1–3650.
public_enabled boolean
Open link for people without an account.
proctoring object
Strict mode settings (partial update).
enabled boolean
Strict mode with camera (website only).
camera boolean
identity_photo boolean
snapshot_interval integer
Value 10–600.
fullscreen boolean
block_copy boolean
face_detection boolean
max_violations integer | null
Value 0–50.
questions array<object>
Questions to add to the first section. Maximum items 100.
type string Required
Question type.
singlemultipletrue_falsetextnumberorderingmatchingessay
section_id integer
Section (block) id; default — the first section.
body string Required
Question text. Maximum length 5000.
options object
Answer options by type: single/multiple — {"choices": [{"text": "…", "correct": true}]}; true_false — {"correct": true}; text — {"answers": ["…"], "case_sensitive": false}; number — {"value": 4, "tolerance": 0}; ordering — {"items": [{"text": "…"}]} in the correct order; matching — {"pairs": [{"left": "…", "right": "…"}]}; essay — {"min_words": 50, "rubric": "…"} (graded manually).
points number
Points, default 1. Value 0–100.
partial_credit boolean
Partial points for multiple / ordering / matching.
is_critical boolean
A wrong answer fails the whole test.
explanation string | null
Shown to the employee after the test when show_result = full. Maximum length 5000.
media_url string | null
Image URL (upload with testing.tests.images.upload). Maximum length 1024.

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Replace the broken fridge seal",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "category": "string",
    "cover_color": "string"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "title": "Replace the broken fridge seal",
      "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
      "category": "string",
      "cover_color": "string"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"title":"Replace the broken fridge seal","description":"Seal on fridge #2 is torn, temperature rises to +9 °C.","category":"string","cover_color":"string"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'title' => 'Replace the broken fridge seal',
        'description' => 'Seal on fridge #2 is torn, temperature rises to +9 °C.',
        'category' => 'string',
        'cover_color' => 'string',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_create",
        "arguments": {
            "title": "Replace the broken fridge seal",
            "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
            "category": "string",
            "cover_color": "string"
        }
    }
}
Response 201
{
  "data": {
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
      "enabled": false,
      "camera": true,
      "identity_photo": true,
      "snapshot_interval": 60,
      "fullscreen": true,
      "block_copy": true,
      "face_detection": true,
      "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
      {
        "id": 77,
        "title": "Storage",
        "pick_count": null,
        "questions": [
          {
            "id": 901,
            "section_id": 77,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "options": {
              "choices": [
                {
                  "id": "a1b2c3d4",
                  "text": "0…+4 °C",
                  "correct": true
                },
                {
                  "id": "e5f6g7h8",
                  "text": "+8…+12 °C",
                  "correct": false
                }
              ]
            },
            "points": 1,
            "partial_credit": false,
            "is_critical": true,
            "explanation": "SanPiN 2.3.6",
            "problems": []
          }
        ]
      }
    ]
  }
}

PATCHUpdate test settings

PATCH /testing/tests/{id}

Partial update of the test settings. Enabling strict mode (proctoring.enabled) also enables taking the test on the website.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_tests_update

Path parameters

id integer Required
Test id.

Request body

title string
Test title. Length 2–255.
description string | null
Maximum length 5000.
category string | null
Maximum length 120.
cover_color string | null
Card colour, e.g. #2563eb. Maximum length 20.
time_limit_minutes integer | null
Time limit (null — unlimited). Value 1–600.
pass_score integer
Pass threshold, percent. Value 0–100.
attempts_limit integer | null
Attempts per assignment cycle (null — unlimited). Value 1–100.
retry_cooldown_hours integer | null
Pause between attempts, hours. Value 0–8760.
shuffle_questions boolean
shuffle_options boolean
navigation string
linear — no going back to previous questions.
freelinear
show_result string
What the employee sees after finishing.
nonescoreanswersfull
allow_app boolean
Can be taken in the mobile app.
allow_web boolean
Can be taken on the website.
certificate_enabled boolean
Issue a certificate on passing.
certificate_valid_days integer | null
Certificate validity; employees are re-assigned when it expires. Value 1–3650.
public_enabled boolean
Open link for people without an account.
proctoring object
Strict mode settings (partial update).
enabled boolean
Strict mode with camera (website only).
camera boolean
identity_photo boolean
snapshot_interval integer
Value 10–600.
fullscreen boolean
block_copy boolean
face_detection boolean
max_violations integer | null
Value 0–50.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X PATCH 'https://app.jecheck.io/api/ext/v1/testing/tests/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Replace the broken fridge seal",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "category": "string",
    "cover_color": "string"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "title": "Replace the broken fridge seal",
      "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
      "category": "string",
      "cover_color": "string"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.patch(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"title":"Replace the broken fridge seal","description":"Seal on fridge #2 is torn, temperature rises to +9 °C.","category":"string","cover_color":"string"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('PATCH', 'testing/tests/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'title' => 'Replace the broken fridge seal',
        'description' => 'Seal on fridge #2 is torn, temperature rises to +9 °C.',
        'category' => 'string',
        'cover_color' => 'string',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_update",
        "arguments": {
            "id": 42,
            "title": "Replace the broken fridge seal",
            "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
            "category": "string",
            "cover_color": "string"
        }
    }
}
Response 200
{
  "data": {
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
      "enabled": false,
      "camera": true,
      "identity_photo": true,
      "snapshot_interval": 60,
      "fullscreen": true,
      "block_copy": true,
      "face_detection": true,
      "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
      {
        "id": 77,
        "title": "Storage",
        "pick_count": null,
        "questions": [
          {
            "id": 901,
            "section_id": 77,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "options": {
              "choices": [
                {
                  "id": "a1b2c3d4",
                  "text": "0…+4 °C",
                  "correct": true
                },
                {
                  "id": "e5f6g7h8",
                  "text": "+8…+12 °C",
                  "correct": false
                }
              ]
            },
            "points": 1,
            "partial_credit": false,
            "is_critical": true,
            "explanation": "SanPiN 2.3.6",
            "problems": []
          }
        ]
      }
    ]
  }
}

POSTPublish a test

POST /testing/tests/{id}/publish

Publishes the test so it can be assigned. Fails with validation_failed listing the problems (no questions, missing correct answers…).

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_tests_publish

Path parameters

id integer Required
Test id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/publish' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/publish', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/publish",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/publish', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_publish",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
      "enabled": false,
      "camera": true,
      "identity_photo": true,
      "snapshot_interval": 60,
      "fullscreen": true,
      "block_copy": true,
      "face_detection": true,
      "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
      {
        "id": 77,
        "title": "Storage",
        "pick_count": null,
        "questions": [
          {
            "id": 901,
            "section_id": 77,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "options": {
              "choices": [
                {
                  "id": "a1b2c3d4",
                  "text": "0…+4 °C",
                  "correct": true
                },
                {
                  "id": "e5f6g7h8",
                  "text": "+8…+12 °C",
                  "correct": false
                }
              ]
            },
            "points": 1,
            "partial_credit": false,
            "is_critical": true,
            "explanation": "SanPiN 2.3.6",
            "problems": []
          }
        ]
      }
    ]
  }
}

POSTChange test status

POST /testing/tests/{id}/status

Move a test to draft, archived or published (same checks as publish).

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_tests_status

Path parameters

id integer Required
Test id.

Request body

status string Required
draftpublishedarchived

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/status' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "status": "draft"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/status', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "status": "draft"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/status",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"status":"draft"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/status', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'status' => 'draft',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_status",
        "arguments": {
            "id": 42,
            "status": "draft"
        }
    }
}
Response 200
{
  "data": {
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
      "enabled": false,
      "camera": true,
      "identity_photo": true,
      "snapshot_interval": 60,
      "fullscreen": true,
      "block_copy": true,
      "face_detection": true,
      "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
      {
        "id": 77,
        "title": "Storage",
        "pick_count": null,
        "questions": [
          {
            "id": 901,
            "section_id": 77,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "options": {
              "choices": [
                {
                  "id": "a1b2c3d4",
                  "text": "0…+4 °C",
                  "correct": true
                },
                {
                  "id": "e5f6g7h8",
                  "text": "+8…+12 °C",
                  "correct": false
                }
              ]
            },
            "points": 1,
            "partial_credit": false,
            "is_critical": true,
            "explanation": "SanPiN 2.3.6",
            "problems": []
          }
        ]
      }
    ]
  }
}

POSTDuplicate a test

POST /testing/tests/{id}/duplicate

Creates a draft copy with all sections and questions.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_tests_duplicate

Path parameters

id integer Required
Test id.

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/duplicate' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/duplicate', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/duplicate",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/duplicate', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_duplicate",
        "arguments": {
            "id": 42
        }
    }
}
Response 201
{
  "data": {
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
      "enabled": false,
      "camera": true,
      "identity_photo": true,
      "snapshot_interval": 60,
      "fullscreen": true,
      "block_copy": true,
      "face_detection": true,
      "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
      {
        "id": 77,
        "title": "Storage",
        "pick_count": null,
        "questions": [
          {
            "id": 901,
            "section_id": 77,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "options": {
              "choices": [
                {
                  "id": "a1b2c3d4",
                  "text": "0…+4 °C",
                  "correct": true
                },
                {
                  "id": "e5f6g7h8",
                  "text": "+8…+12 °C",
                  "correct": false
                }
              ]
            },
            "points": 1,
            "partial_credit": false,
            "is_critical": true,
            "explanation": "SanPiN 2.3.6",
            "problems": []
          }
        ]
      }
    ]
  }
}

DELDelete a test

DELETE /testing/tests/{id}

Deletes the test and closes its assignments. Results of past attempts are kept.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_tests_delete · destructive

Path parameters

id integer Required
Test id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X DELETE 'https://app.jecheck.io/api/ext/v1/testing/tests/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.delete(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('DELETE', 'testing/tests/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_delete",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "deleted": true
  }
}

GETGet test analytics

GET /testing/tests/{id}/analytics

Summary of finished attempts (pass rate, average, score distribution), per-question difficulty, results by section and by location — for employees of the locations of testing.results.

Token scoperead
Permission testing.results
Plan featuretesting
MCP tooltesting_tests_analytics · read-only

Path parameters

id integer Required
Test id.

Query parameters

date_from date
Attempts submitted from.
date_to date
Attempts submitted to.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/tests/42/analytics?date_from=2026-11-01' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/analytics?date_from=2026-11-01', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/analytics",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    params={"date_from":"2026-11-01"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/tests/42/analytics', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'query' => [
        'date_from' => '2026-11-01',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_analytics",
        "arguments": {
            "id": 42,
            "date_from": "2026-11-01"
        }
    }
}
Response 200
{
  "data": {
    "summary": {
      "attempts": 42,
      "participants": 30,
      "pass_rate": 76,
      "avg_percent": 84.5,
      "median_duration": 960,
      "pending_review": 1,
      "low_trust": 0,
      "distribution": [
        {
          "from": 90,
          "to": 100,
          "count": 12
        }
      ],
      "pass_score": 80
    },
    "questions": [
      {
        "id": 901,
        "exists": true,
        "position": 1,
        "type": "single",
        "body": "At what temperature must chilled food be stored?",
        "section": "Storage",
        "answers": 42,
        "correct_rate": 64,
        "avg_time": 38,
        "discrimination": 0.31,
        "flag": null,
        "choices": [
          {
            "text": "0…+4 °C",
            "correct": true,
            "share": 64
          }
        ],
        "top_wrong": null
      }
    ],
    "sections": [
      {
        "name": "Storage",
        "percent": 71
      }
    ],
    "locations": [
      {
        "id": 12,
        "name": "Abay Ave",
        "total": 10,
        "passed": 8,
        "completion": 80,
        "avg_percent": 85.2
      }
    ]
  }
}

POSTAdd a section

POST /testing/tests/{id}/sections
Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_sections_create

Path parameters

id integer Required
Test id.

Request body

title string
Maximum length 255.

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/sections' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Replace the broken fridge seal"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/sections', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "title": "Replace the broken fridge seal"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/sections",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"title":"Replace the broken fridge seal"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/sections', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'title' => 'Replace the broken fridge seal',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_sections_create",
        "arguments": {
            "id": 42,
            "title": "Replace the broken fridge seal"
        }
    }
}
Response 201
{
  "data": {
    "id": 78,
    "title": "Hygiene",
    "pick_count": null
  }
}

PATCHUpdate a section

PATCH /testing/tests/{id}/sections/{section_id}

Rename a section or set pick_count — how many random questions of the section each attempt gets (null — all).

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_sections_update

Path parameters

id integer Required
Test id.
section_id integer Required
Section id.

Request body

title string
Maximum length 255.
pick_count integer | null
Minimum 1.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X PATCH 'https://app.jecheck.io/api/ext/v1/testing/tests/42/sections/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Replace the broken fridge seal",
    "pick_count": 42
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/sections/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "title": "Replace the broken fridge seal",
      "pick_count": 42
  }),
});
const { data } = await res.json();
import os, requests

res = requests.patch(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/sections/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"title":"Replace the broken fridge seal","pick_count":42},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('PATCH', 'testing/tests/42/sections/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'title' => 'Replace the broken fridge seal',
        'pick_count' => 42,
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_sections_update",
        "arguments": {
            "id": 42,
            "section_id": 42,
            "title": "Replace the broken fridge seal",
            "pick_count": 42
        }
    }
}
Response 200
{
  "data": {
    "id": 78,
    "title": "Hygiene",
    "pick_count": 5
  }
}

DELDelete a section

DELETE /testing/tests/{id}/sections/{section_id}

Questions of the deleted section move to another section.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_sections_delete · destructive

Path parameters

id integer Required
Test id.
section_id integer Required
Section id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X DELETE 'https://app.jecheck.io/api/ext/v1/testing/tests/42/sections/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/sections/42', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.delete(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/sections/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('DELETE', 'testing/tests/42/sections/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_sections_delete",
        "arguments": {
            "id": 42,
            "section_id": 42
        }
    }
}
Response 200
{
  "data": {
    "deleted": true
  }
}

POSTAdd a question

POST /testing/tests/{id}/questions

Adds a question to a section. Answer options by type: single/multiple — {"choices": [{"text": "…", "correct": true}]}; true_false — {"correct": true}; text — {"answers": ["…"], "case_sensitive": false}; number — {"value": 4, "tolerance": 0}; ordering — {"items": [{"text": "…"}]} in the correct order; matching — {"pairs": [{"left": "…", "right": "…"}]}; essay — {"min_words": 50, "rubric": "…"} (graded manually).

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_questions_create

Path parameters

id integer Required
Test id.

Request body

type string Required
Question type.
singlemultipletrue_falsetextnumberorderingmatchingessay
section_id integer
Section (block) id; default — the first section.
body string Required
Question text. Maximum length 5000.
options object
Answer options by type: single/multiple — {"choices": [{"text": "…", "correct": true}]}; true_false — {"correct": true}; text — {"answers": ["…"], "case_sensitive": false}; number — {"value": 4, "tolerance": 0}; ordering — {"items": [{"text": "…"}]} in the correct order; matching — {"pairs": [{"left": "…", "right": "…"}]}; essay — {"min_words": 50, "rubric": "…"} (graded manually).
points number
Points, default 1. Value 0–100.
partial_credit boolean
Partial points for multiple / ordering / matching.
is_critical boolean
A wrong answer fails the whole test.
explanation string | null
Shown to the employee after the test when show_result = full. Maximum length 5000.
media_url string | null
Image URL (upload with testing.tests.images.upload). Maximum length 1024.

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/questions' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "single",
    "section_id": 42,
    "body": "string",
    "options": []
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/questions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "type": "single",
      "section_id": 42,
      "body": "string",
      "options": []
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/questions",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"type":"single","section_id":42,"body":"string","options":[]},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/questions', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'type' => 'single',
        'section_id' => 42,
        'body' => 'string',
        'options' => [
        ],
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_questions_create",
        "arguments": {
            "id": 42,
            "type": "single",
            "section_id": 42,
            "body": "string",
            "options": []
        }
    }
}
Response 201
{
  "data": {
    "id": 901,
    "section_id": 77,
    "type": "single",
    "body": "At what temperature must chilled food be stored?",
    "media_url": null,
    "options": {
      "choices": [
        {
          "id": "a1b2c3d4",
          "text": "0…+4 °C",
          "correct": true
        },
        {
          "id": "e5f6g7h8",
          "text": "+8…+12 °C",
          "correct": false
        }
      ]
    },
    "points": 1,
    "partial_credit": false,
    "is_critical": true,
    "explanation": "SanPiN 2.3.6",
    "problems": []
  }
}

POSTAdd questions in bulk

POST /testing/tests/{id}/questions/batch

Adds up to 100 questions to a section in one call; items without a known type or text are skipped.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_questions_batch

Path parameters

id integer Required
Test id.

Request body

section_id integer | null
Target section, default — the first one.
questions array<object> Required
Items 1–100.
type string Required
Question type.
singlemultipletrue_falsetextnumberorderingmatchingessay
section_id integer
Section (block) id; default — the first section.
body string Required
Question text. Maximum length 5000.
options object
Answer options by type: single/multiple — {"choices": [{"text": "…", "correct": true}]}; true_false — {"correct": true}; text — {"answers": ["…"], "case_sensitive": false}; number — {"value": 4, "tolerance": 0}; ordering — {"items": [{"text": "…"}]} in the correct order; matching — {"pairs": [{"left": "…", "right": "…"}]}; essay — {"min_words": 50, "rubric": "…"} (graded manually).
points number
Points, default 1. Value 0–100.
partial_credit boolean
Partial points for multiple / ordering / matching.
is_critical boolean
A wrong answer fails the whole test.
explanation string | null
Shown to the employee after the test when show_result = full. Maximum length 5000.
media_url string | null
Image URL (upload with testing.tests.images.upload). Maximum length 1024.

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/batch' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "section_id": 42,
    "questions": [
        {
            "type": "single",
            "section_id": 42,
            "body": "string"
        }
    ]
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/batch', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "section_id": 42,
      "questions": [
          {
              "type": "single",
              "section_id": 42,
              "body": "string"
          }
      ]
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/batch",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"section_id":42,"questions":[{"type":"single","section_id":42,"body":"string"}]},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/questions/batch', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'section_id' => 42,
        'questions' => [
            [
                'type' => 'single',
                'section_id' => 42,
                'body' => 'string',
            ],
        ],
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_questions_batch",
        "arguments": {
            "id": 42,
            "section_id": 42,
            "questions": [
                {
                    "type": "single",
                    "section_id": 42,
                    "body": "string"
                }
            ]
        }
    }
}
Response 201
{
  "data": {
    "added": 10,
    "sections": [
      {
        "id": 77,
        "title": "Storage",
        "pick_count": null,
        "questions": [
          {
            "id": 901,
            "section_id": 77,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "options": {
              "choices": [
                {
                  "id": "a1b2c3d4",
                  "text": "0…+4 °C",
                  "correct": true
                },
                {
                  "id": "e5f6g7h8",
                  "text": "+8…+12 °C",
                  "correct": false
                }
              ]
            },
            "points": 1,
            "partial_credit": false,
            "is_critical": true,
            "explanation": "SanPiN 2.3.6",
            "problems": []
          }
        ]
      }
    ]
  }
}

PATCHUpdate a question

PATCH /testing/tests/{id}/questions/{question_id}

Partial update. Changing type converts the options where possible.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_questions_update

Path parameters

id integer Required
Test id.
question_id integer Required
Question id.

Request body

type string
Question type.
singlemultipletrue_falsetextnumberorderingmatchingessay
section_id integer
Section (block) id; default — the first section.
body string
Question text. Maximum length 5000.
options object
Answer options by type: single/multiple — {"choices": [{"text": "…", "correct": true}]}; true_false — {"correct": true}; text — {"answers": ["…"], "case_sensitive": false}; number — {"value": 4, "tolerance": 0}; ordering — {"items": [{"text": "…"}]} in the correct order; matching — {"pairs": [{"left": "…", "right": "…"}]}; essay — {"min_words": 50, "rubric": "…"} (graded manually).
points number
Points, default 1. Value 0–100.
partial_credit boolean
Partial points for multiple / ordering / matching.
is_critical boolean
A wrong answer fails the whole test.
explanation string | null
Shown to the employee after the test when show_result = full. Maximum length 5000.
media_url string | null
Image URL (upload with testing.tests.images.upload). Maximum length 1024.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X PATCH 'https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "single",
    "section_id": 42,
    "body": "string",
    "options": []
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "type": "single",
      "section_id": 42,
      "body": "string",
      "options": []
  }),
});
const { data } = await res.json();
import os, requests

res = requests.patch(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"type":"single","section_id":42,"body":"string","options":[]},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('PATCH', 'testing/tests/42/questions/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'type' => 'single',
        'section_id' => 42,
        'body' => 'string',
        'options' => [
        ],
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_questions_update",
        "arguments": {
            "id": 42,
            "question_id": 42,
            "type": "single",
            "section_id": 42,
            "body": "string",
            "options": []
        }
    }
}
Response 200
{
  "data": {
    "id": 901,
    "section_id": 77,
    "type": "single",
    "body": "At what temperature must chilled food be stored?",
    "media_url": null,
    "options": {
      "choices": [
        {
          "id": "a1b2c3d4",
          "text": "0…+4 °C",
          "correct": true
        },
        {
          "id": "e5f6g7h8",
          "text": "+8…+12 °C",
          "correct": false
        }
      ]
    },
    "points": 1,
    "partial_credit": false,
    "is_critical": true,
    "explanation": "SanPiN 2.3.6",
    "problems": []
  }
}

POSTDuplicate a question

POST /testing/tests/{id}/questions/{question_id}/duplicate
Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_questions_duplicate

Path parameters

id integer Required
Test id.
question_id integer Required
Question id.

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42/duplicate' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42/duplicate', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42/duplicate",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/questions/42/duplicate', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_questions_duplicate",
        "arguments": {
            "id": 42,
            "question_id": 42
        }
    }
}
Response 201
{
  "data": {
    "id": 901,
    "section_id": 77,
    "type": "single",
    "body": "At what temperature must chilled food be stored?",
    "media_url": null,
    "options": {
      "choices": [
        {
          "id": "a1b2c3d4",
          "text": "0…+4 °C",
          "correct": true
        },
        {
          "id": "e5f6g7h8",
          "text": "+8…+12 °C",
          "correct": false
        }
      ]
    },
    "points": 1,
    "partial_credit": false,
    "is_critical": true,
    "explanation": "SanPiN 2.3.6",
    "problems": []
  }
}

DELDelete a question

DELETE /testing/tests/{id}/questions/{question_id}
Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_questions_delete · destructive

Path parameters

id integer Required
Test id.
question_id integer Required
Question id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X DELETE 'https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.delete(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/questions/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('DELETE', 'testing/tests/42/questions/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_questions_delete",
        "arguments": {
            "id": 42,
            "question_id": 42
        }
    }
}
Response 200
{
  "data": {
    "deleted": true
  }
}

POSTUpload a question image

POST /testing/tests/{id}/images

Uploads an image (up to 10 MB, converted to JPEG) and returns its URL for media_url of a question.

Token scopewrite
Permission testing.manage
Plan featuretesting
MCP tooltesting_tests_images_upload
Content typemultipart/form-data application/json

Path parameters

id integer Required
Test id.

Request body

image file Required

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/images' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -F '[email protected]'
const form = new FormData();
form.append('image', fileInput.files[0]);

const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/images', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.JECHECK_TOKEN}` },
  body: form,
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/images",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    data=[],
    files=[("image", open("photo.jpg", "rb"))],
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/images', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'multipart' => [
        ['name' => 'image', 'contents' => fopen('photo.jpg', 'r')],
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_tests_images_upload",
        "arguments": {
            "id": 42
        }
    }
}
Response 201
{
  "data": {
    "url": "https://files.jecheck.io/exams/761/questions/a8Kd02.jpg"
  }
}

GETList assignments of a test

GET /testing/tests/{id}/assignments

Assignments with progress and per-employee status. Only employees of the locations of testing.results are included.

Token scoperead
Permission testing.results
Plan featuretesting
MCP tooltesting_assignments_list · read-only

Path parameters

id integer Required
Test id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/tests/42/assignments' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/assignments', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/assignments",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/tests/42/assignments', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignments_list",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": [
    {
      "id": 140,
      "title": "Onboarding — October",
      "message": "Please pass by Friday",
      "test": {
        "id": 58,
        "title": "Food safety basics"
      },
      "status": "active",
      "starts_at": null,
      "due_at": "2026-11-07T18:00:00+05:00",
      "recur_days": 365,
      "recur_due_days": 14,
      "close_after_due": false,
      "auto_enroll": true,
      "created_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
      },
      "created_at": "2026-10-31T09:00:00+05:00",
      "targets": [
        {
          "kind": "position",
          "id": 7
        }
      ],
      "progress": {
        "total": 12,
        "passed": 8,
        "failed": 1,
        "in_progress": 1,
        "review": 0,
        "not_started": 2,
        "overdue": 0,
        "completion": 67
      },
      "assignees": [
        {
          "id": 5012,
          "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
          },
          "position": "Cook",
          "status": "passed",
          "overdue": false,
          "attempts": 1,
          "best_percent": 86.7,
          "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
          "due_at": "2026-11-07T18:00:00+05:00",
          "completed_at": "2026-11-02T16:20:00+05:00",
          "valid_until": "2027-11-02T16:20:00+05:00",
          "cycle": 1,
          "link": "https://app.jecheck.io/exam/Xy7…"
        }
      ]
    }
  ]
}

POSTAssign a test

POST /testing/tests/{id}/assignments

Assigns a published test to employees, positions, locations or the whole company. Employees get a notification with a personal link; with auto_enroll new employees matching the targets are added automatically, with recur_days the test is re-assigned periodically. With a location-scoped permission only user and department targets within those locations are allowed.

Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignments_create

Path parameters

id integer Required
Test id.

Request body

targets array<object> Required
Minimum items 1.
kind string Required
department — a location.
userpositiondepartmentall
id integer | null
User, position or location id (omit for all).
title string | null
Maximum length 255.
message string | null
Message to employees. Maximum length 1000.
starts_at date-time | null
Open from (notifications are sent then).
due_at date-time | null
Deadline (future).
recur_days integer | null
Repeat every N days. Value 1–3650.
recur_due_days integer | null
Days to pass each repetition, default 14. Value 1–365.
close_after_due boolean
Close the test for an employee after the deadline.
auto_enroll boolean
Add new matching employees automatically, default true.
remind boolean
Send deadline reminders, default true.

Responses

StatusDescription
201Created
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/assignments' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "targets": [
        {
            "kind": "user",
            "id": 42
        }
    ],
    "title": "Replace the broken fridge seal",
    "message": "string",
    "starts_at": "2026-11-03T09:00:00+05:00"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/assignments', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "targets": [
          {
              "kind": "user",
              "id": 42
          }
      ],
      "title": "Replace the broken fridge seal",
      "message": "string",
      "starts_at": "2026-11-03T09:00:00+05:00"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/tests/42/assignments",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"targets":[{"kind":"user","id":42}],"title":"Replace the broken fridge seal","message":"string","starts_at":"2026-11-03T09:00:00+05:00"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/tests/42/assignments', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'targets' => [
            [
                'kind' => 'user',
                'id' => 42,
            ],
        ],
        'title' => 'Replace the broken fridge seal',
        'message' => 'string',
        'starts_at' => '2026-11-03T09:00:00+05:00',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignments_create",
        "arguments": {
            "id": 42,
            "targets": [
                {
                    "kind": "user",
                    "id": 42
                }
            ],
            "title": "Replace the broken fridge seal",
            "message": "string",
            "starts_at": "2026-11-03T09:00:00+05:00"
        }
    }
}
Response 201
{
  "data": {
    "id": 140,
    "title": "Onboarding — October",
    "message": "Please pass by Friday",
    "test": {
      "id": 58,
      "title": "Food safety basics"
    },
    "status": "active",
    "starts_at": null,
    "due_at": "2026-11-07T18:00:00+05:00",
    "recur_days": 365,
    "recur_due_days": 14,
    "close_after_due": false,
    "auto_enroll": true,
    "created_by": {
      "id": 311,
      "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
      "name": "Aigerim Sadykova"
    },
    "created_at": "2026-10-31T09:00:00+05:00",
    "targets": [
      {
        "kind": "position",
        "id": 7
      }
    ],
    "progress": {
      "total": 12,
      "passed": 8,
      "failed": 1,
      "in_progress": 1,
      "review": 0,
      "not_started": 2,
      "overdue": 0,
      "completion": 67
    },
    "assignees": [
      {
        "id": 5012,
        "user": {
          "id": 311,
          "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
          "name": "Aigerim Sadykova"
        },
        "position": "Cook",
        "status": "passed",
        "overdue": false,
        "attempts": 1,
        "best_percent": 86.7,
        "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
        "due_at": "2026-11-07T18:00:00+05:00",
        "completed_at": "2026-11-02T16:20:00+05:00",
        "valid_until": "2027-11-02T16:20:00+05:00",
        "cycle": 1,
        "link": "https://app.jecheck.io/exam/Xy7…"
      }
    ]
  }
}

GETRetrieve an assignment

GET /testing/assignments/{id}
Token scoperead
Permission testing.results
Plan featuretesting
MCP tooltesting_assignments_get · read-only

Path parameters

id integer Required
Assignment id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/assignments/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignments/42', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/assignments/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/assignments/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignments_get",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "id": 140,
    "title": "Onboarding — October",
    "message": "Please pass by Friday",
    "test": {
      "id": 58,
      "title": "Food safety basics"
    },
    "status": "active",
    "starts_at": null,
    "due_at": "2026-11-07T18:00:00+05:00",
    "recur_days": 365,
    "recur_due_days": 14,
    "close_after_due": false,
    "auto_enroll": true,
    "created_by": {
      "id": 311,
      "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
      "name": "Aigerim Sadykova"
    },
    "created_at": "2026-10-31T09:00:00+05:00",
    "targets": [
      {
        "kind": "position",
        "id": 7
      }
    ],
    "progress": {
      "total": 12,
      "passed": 8,
      "failed": 1,
      "in_progress": 1,
      "review": 0,
      "not_started": 2,
      "overdue": 0,
      "completion": 67
    },
    "assignees": [
      {
        "id": 5012,
        "user": {
          "id": 311,
          "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
          "name": "Aigerim Sadykova"
        },
        "position": "Cook",
        "status": "passed",
        "overdue": false,
        "attempts": 1,
        "best_percent": 86.7,
        "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
        "due_at": "2026-11-07T18:00:00+05:00",
        "completed_at": "2026-11-02T16:20:00+05:00",
        "valid_until": "2027-11-02T16:20:00+05:00",
        "cycle": 1,
        "link": "https://app.jecheck.io/exam/Xy7…"
      }
    ]
  }
}

PATCHUpdate an assignment

PATCH /testing/assignments/{id}

Close / reopen the assignment (status) or move the deadline for everyone who has not passed yet (due_at). Allowed to the author or with testing.results for all locations.

Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignments_update

Path parameters

id integer Required
Assignment id.

Request body

status string
activeclosed
due_at date-time | null
New deadline.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X PATCH 'https://app.jecheck.io/api/ext/v1/testing/assignments/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "status": "active",
    "due_at": "2026-11-03T09:00:00+05:00"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignments/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "status": "active",
      "due_at": "2026-11-03T09:00:00+05:00"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.patch(
    "https://app.jecheck.io/api/ext/v1/testing/assignments/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"status":"active","due_at":"2026-11-03T09:00:00+05:00"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('PATCH', 'testing/assignments/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'status' => 'active',
        'due_at' => '2026-11-03T09:00:00+05:00',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignments_update",
        "arguments": {
            "id": 42,
            "status": "active",
            "due_at": "2026-11-03T09:00:00+05:00"
        }
    }
}
Response 200
{
  "data": {
    "id": 140,
    "title": "Onboarding — October",
    "message": "Please pass by Friday",
    "test": {
      "id": 58,
      "title": "Food safety basics"
    },
    "status": "active",
    "starts_at": null,
    "due_at": "2026-11-07T18:00:00+05:00",
    "recur_days": 365,
    "recur_due_days": 14,
    "close_after_due": false,
    "auto_enroll": true,
    "created_by": {
      "id": 311,
      "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
      "name": "Aigerim Sadykova"
    },
    "created_at": "2026-10-31T09:00:00+05:00",
    "targets": [
      {
        "kind": "position",
        "id": 7
      }
    ],
    "progress": {
      "total": 12,
      "passed": 8,
      "failed": 1,
      "in_progress": 1,
      "review": 0,
      "not_started": 2,
      "overdue": 0,
      "completion": 67
    },
    "assignees": [
      {
        "id": 5012,
        "user": {
          "id": 311,
          "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
          "name": "Aigerim Sadykova"
        },
        "position": "Cook",
        "status": "passed",
        "overdue": false,
        "attempts": 1,
        "best_percent": 86.7,
        "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
        "due_at": "2026-11-07T18:00:00+05:00",
        "completed_at": "2026-11-02T16:20:00+05:00",
        "valid_until": "2027-11-02T16:20:00+05:00",
        "cycle": 1,
        "link": "https://app.jecheck.io/exam/Xy7…"
      }
    ]
  }
}

POSTAdd new matching employees

POST /testing/assignments/{id}/sync

Adds employees who now match the targets (new hires in the position or location) and notifies them.

Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignments_sync

Path parameters

id integer Required
Assignment id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/assignments/42/sync' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignments/42/sync', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/assignments/42/sync",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/assignments/42/sync', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignments_sync",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "added": 2
  }
}

POSTRemind assignees

POST /testing/assignments/{id}/remind

Sends a reminder to everyone who has not passed yet.

Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignments_remind

Path parameters

id integer Required
Assignment id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/assignments/42/remind' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignments/42/remind', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/assignments/42/remind",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/assignments/42/remind', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignments_remind",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "reminded": 4
  }
}

DELDelete an assignment

DELETE /testing/assignments/{id}
Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignments_delete · destructive

Path parameters

id integer Required
Assignment id.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X DELETE 'https://app.jecheck.io/api/ext/v1/testing/assignments/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignments/42', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.delete(
    "https://app.jecheck.io/api/ext/v1/testing/assignments/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('DELETE', 'testing/assignments/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignments_delete",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "deleted": true
  }
}

POSTReset attempts of an employee

POST /testing/assignees/{id}/restart

Starts a new cycle for one employee of an assignment (previous attempts stay in history) and notifies them.

Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignees_restart

Path parameters

id integer Required
Assignee id (one employee in an assignment).

Request body

due_at date-time | null
New personal deadline.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/assignees/42/restart' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "due_at": "2026-11-03T09:00:00+05:00"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignees/42/restart', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "due_at": "2026-11-03T09:00:00+05:00"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/assignees/42/restart",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"due_at":"2026-11-03T09:00:00+05:00"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/assignees/42/restart', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'due_at' => '2026-11-03T09:00:00+05:00',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignees_restart",
        "arguments": {
            "id": 42,
            "due_at": "2026-11-03T09:00:00+05:00"
        }
    }
}
Response 200
{
  "data": {
    "ok": true
  }
}

POSTRemind an employee

POST /testing/assignees/{id}/remind
Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignees_remind

Path parameters

id integer Required
Assignee id (one employee in an assignment).

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/assignees/42/remind' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignees/42/remind', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/assignees/42/remind",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/assignees/42/remind', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignees_remind",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "ok": true
  }
}

PATCHChange the deadline of an employee

PATCH /testing/assignees/{id}
Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignees_update

Path parameters

id integer Required
Assignee id (one employee in an assignment).

Request body

due_at date-time | null Required
Personal deadline (null — none).

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X PATCH 'https://app.jecheck.io/api/ext/v1/testing/assignees/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "due_at": "2026-11-03T09:00:00+05:00"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignees/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "due_at": "2026-11-03T09:00:00+05:00"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.patch(
    "https://app.jecheck.io/api/ext/v1/testing/assignees/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"due_at":"2026-11-03T09:00:00+05:00"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('PATCH', 'testing/assignees/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'due_at' => '2026-11-03T09:00:00+05:00',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignees_update",
        "arguments": {
            "id": 42,
            "due_at": "2026-11-03T09:00:00+05:00"
        }
    }
}
Response 200
{
  "data": {
    "ok": true
  }
}

DELRemove an employee from an assignment

DELETE /testing/assignees/{id}
Token scopewrite
Permission testing.results
Plan featuretesting
MCP tooltesting_assignees_delete · destructive

Path parameters

id integer Required
Assignee id (one employee in an assignment).

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X DELETE 'https://app.jecheck.io/api/ext/v1/testing/assignees/42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/assignees/42', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.delete(
    "https://app.jecheck.io/api/ext/v1/testing/assignees/42",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('DELETE', 'testing/assignees/42', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_assignees_delete",
        "arguments": {
            "id": 42
        }
    }
}
Response 200
{
  "data": {
    "deleted": true
  }
}

GETList attempts

GET /testing/attempts

Attempts of employees of the locations of testing.results (guests of open links — only with the permission for all locations), newest first.

Token scoperead
Permission testing.results
Plan featuretesting
MCP tooltesting_attempts_list · read-only

Query parameters

page integer
Page number, starting from 1. Default 1. Minimum 1.
per_page integer
Items per page (max 100). Default 25. Value 1–100.
test_id integer
Only this test.
user_id integer
Only this employee.
location_ids array<integer>
Only employees of these locations.
status string
flagged — strict-mode attempts with a low trust score.
passedfailedreviewannulledin_progressflagged
search string
Search by employee or guest name / email. Maximum length 100.
date_from date
Started from.
date_to date
Started to.
updated_since date-time
Only attempts changed after this moment — use for incremental sync.

Responses

StatusDescription
200OK — a page of results with meta
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/attempts?test_id=42' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/attempts?test_id=42', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data, meta } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/attempts",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    params={"test_id":42},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/attempts', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'query' => [
        'test_id' => 42,
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_attempts_list",
        "arguments": {
            "test_id": 42
        }
    }
}
Response 200
{
  "data": [
    {
      "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
      "test": {
        "id": 58,
        "title": "Food safety basics"
      },
      "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
      },
      "guest": null,
      "channel": "app",
      "strict": false,
      "status": "completed",
      "percent": 86.7,
      "score": 13,
      "max_score": 15,
      "passed": true,
      "critical_failed": false,
      "finish_reason": "manual",
      "started_at": "2026-11-02T16:02:00+05:00",
      "submitted_at": "2026-11-02T16:20:00+05:00",
      "duration_sec": 1080,
      "violations": 0,
      "trust_score": null,
      "trust_level": null,
      "reviewed": false,
      "certificate_number": "JC-2026-000412",
      "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total": 1,
    "has_more": false
  }
}

GETRetrieve an attempt

GET /testing/attempts/{uuid}

Attempt with every question: the employee's answer, the correct answer, points and manual-review state. can_review tells whether the token may grade it.

Token scoperead
Permission testing.results
Plan featuretesting
MCP tooltesting_attempts_get · read-only

Path parameters

uuid uuid Required
Attempt UUID.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_attempts_get",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    }
}
Response 200
{
  "data": {
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
      "id": 58,
      "title": "Food safety basics"
    },
    "user": {
      "id": 311,
      "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
      "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
      {
        "question_id": 901,
        "n": 1,
        "section": "Storage",
        "type": "single",
        "body": "At what temperature must chilled food be stored?",
        "your_answer": "0…+4 °C",
        "correct_answer": "0…+4 °C",
        "is_correct": true,
        "needs_review": false,
        "points": 1,
        "max_points": 1,
        "is_critical": true,
        "feedback": null,
        "rubric": null,
        "time_spent_sec": 41,
        "graded_manually": false
      }
    ]
  }
}

POSTGrade an open answer

POST /testing/attempts/{uuid}/grade

Sets points (and optional feedback) for a question, usually an essay waiting for manual review. When all answers are graded the attempt gets its final result and the employee is notified.

Token scopewrite
Permission testing.review
Plan featuretesting
MCP tooltesting_attempts_grade

Path parameters

uuid uuid Required
Attempt UUID.

Request body

question_id integer Required
points number Required
Minimum 0.
feedback string | null
Maximum length 1000.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.review, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/grade' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "question_id": 42,
    "points": 4.5,
    "feedback": "string"
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/grade', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "question_id": 42,
      "points": 4.5,
      "feedback": "string"
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/grade",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"question_id":42,"points":4.5,"feedback":"string"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/grade', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'question_id' => 42,
        'points' => 4.5,
        'feedback' => 'string',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_attempts_grade",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "question_id": 42,
            "points": 4.5,
            "feedback": "string"
        }
    }
}
Response 200
{
  "data": {
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
      "id": 58,
      "title": "Food safety basics"
    },
    "user": {
      "id": 311,
      "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
      "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
      {
        "question_id": 901,
        "n": 1,
        "section": "Storage",
        "type": "single",
        "body": "At what temperature must chilled food be stored?",
        "your_answer": "0…+4 °C",
        "correct_answer": "0…+4 °C",
        "is_correct": true,
        "needs_review": false,
        "points": 1,
        "max_points": 1,
        "is_critical": true,
        "feedback": null,
        "rubric": null,
        "time_spent_sec": 41,
        "graded_manually": false
      }
    ]
  }
}

POSTAnnul an attempt

POST /testing/attempts/{uuid}/annul

Cancels the result (e.g. cheating in strict mode); the employee can take the test again.

Token scopewrite
Permission testing.review
Plan featuretesting
MCP tooltesting_attempts_annul

Path parameters

uuid uuid Required
Attempt UUID.

Request body

note string Required
Reason. Length 1–1000.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.review, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/annul' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "note": "Seal on fridge #2 is torn, temperature rises to +9 °C."
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/annul', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "note": "Seal on fridge #2 is torn, temperature rises to +9 °C."
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/annul",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"note":"Seal on fridge #2 is torn, temperature rises to +9 °C."},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/annul', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'note' => 'Seal on fridge #2 is torn, temperature rises to +9 °C.',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_attempts_annul",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "note": "Seal on fridge #2 is torn, temperature rises to +9 °C."
        }
    }
}
Response 200
{
  "data": {
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
      "id": 58,
      "title": "Food safety basics"
    },
    "user": {
      "id": 311,
      "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
      "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
      {
        "question_id": 901,
        "n": 1,
        "section": "Storage",
        "type": "single",
        "body": "At what temperature must chilled food be stored?",
        "your_answer": "0…+4 °C",
        "correct_answer": "0…+4 °C",
        "is_correct": true,
        "needs_review": false,
        "points": 1,
        "max_points": 1,
        "is_critical": true,
        "feedback": null,
        "rubric": null,
        "time_spent_sec": 41,
        "graded_manually": false
      }
    ]
  }
}

POSTConfirm an attempt result

POST /testing/attempts/{uuid}/confirm

Marks the attempt as reviewed (e.g. after checking a strict-mode recording).

Token scopewrite
Permission testing.review
Plan featuretesting
MCP tooltesting_attempts_confirm

Path parameters

uuid uuid Required
Attempt UUID.

Request body

note string | null
Maximum length 1000.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Token lacks testing.review, is read-only, or the plan has no testing
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/confirm' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "note": "Seal on fridge #2 is torn, temperature rises to +9 °C."
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/confirm', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "note": "Seal on fridge #2 is torn, temperature rises to +9 °C."
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/confirm",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"note":"Seal on fridge #2 is torn, temperature rises to +9 °C."},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/confirm', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'note' => 'Seal on fridge #2 is torn, temperature rises to +9 °C.',
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_attempts_confirm",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "note": "Seal on fridge #2 is torn, temperature rises to +9 °C."
        }
    }
}
Response 200
{
  "data": {
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
      "id": 58,
      "title": "Food safety basics"
    },
    "user": {
      "id": 311,
      "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
      "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
      {
        "question_id": 901,
        "n": 1,
        "section": "Storage",
        "type": "single",
        "body": "At what temperature must chilled food be stored?",
        "your_answer": "0…+4 °C",
        "correct_answer": "0…+4 °C",
        "is_correct": true,
        "needs_review": false,
        "points": 1,
        "max_points": 1,
        "is_critical": true,
        "feedback": null,
        "rubric": null,
        "time_spent_sec": 41,
        "graded_manually": false
      }
    ]
  }
}

GETList my assigned tests

GET /testing/my

Tests assigned to the acting user with availability: can_start, reason, open_attempt, web_only (strict-mode tests are taken on the website by url).

Token scoperead
Permission Any employee; results follow the token's visibility
MCP tooltesting_my_list · read-only

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Plan without API access
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/my' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/my', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/my",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/my', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_my_list",
        "arguments": {}
    }
}
Response 200
{
  "data": [
    {
      "assignee_id": 5012,
      "title": "Food safety basics",
      "description": "For new kitchen staff",
      "category": "Kitchen",
      "message": "Please pass by Friday",
      "status": "assigned",
      "overdue": false,
      "due_at": "2026-11-07T18:00:00+05:00",
      "starts_at": null,
      "valid_until": null,
      "best_percent": null,
      "pass_score": 80,
      "questions_count": 15,
      "time_limit_minutes": 20,
      "strict": false,
      "web_only": false,
      "can_start": true,
      "reason": null,
      "open_attempt": null,
      "attempts_left": 3,
      "last_attempt": null,
      "url": "https://app.jecheck.io/exam/Xy7…"
    }
  ]
}

GETList my finished attempts

GET /testing/my/history
Token scoperead
Permission Any employee; results follow the token's visibility
MCP tooltesting_my_history · read-only

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Plan without API access
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/my/history' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/my/history', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/my/history",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/my/history', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_my_history",
        "arguments": {}
    }
}
Response 200
{
  "data": [
    {
      "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
      "title": "Food safety basics",
      "status": "completed",
      "submitted_at": "2026-11-02T16:20:00+05:00",
      "percent": 86.7,
      "passed": true,
      "certificate_number": "JC-2026-000412"
    }
  ]
}

POSTStart or continue my test

POST /testing/my/start

Starts an attempt of an assigned test as the acting user (or returns the open one) and returns questions without correct answers. Strict-mode and website-only tests return 409 web_only with web_url to open in a browser.

Token scopewrite
Permission Any employee; results follow the token's visibility
MCP tooltesting_my_start

Request body

assignee_id integer Required
assignee_id from testing.my.list.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Read-only token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
web_onlyweb_only — The test can only be taken on the website (strict mode with camera)
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/my/start' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "assignee_id": 42
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/my/start', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "assignee_id": 42
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/my/start",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"assignee_id":42},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/my/start', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'assignee_id' => 42,
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_my_start",
        "arguments": {
            "assignee_id": 42
        }
    }
}
Response 200
{
  "data": {
    "attempt": {
      "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
      "status": "in_progress",
      "channel": "app",
      "strict": false,
      "started_at": "2026-11-02T16:02:00+05:00",
      "deadline_at": "2026-11-02T16:22:00+05:00",
      "remaining_sec": 1150,
      "current_index": 0,
      "violations": 0,
      "identity_photo": false
    },
    "test": {
      "title": "Food safety basics",
      "description": "For new kitchen staff",
      "navigation": "free",
      "pass_score": 80,
      "time_limit_minutes": 20,
      "questions_count": 15,
      "max_score": 15,
      "proctoring": null
    },
    "questions": [
      {
        "id": 901,
        "type": "single",
        "body": "At what temperature must chilled food be stored?",
        "media_url": null,
        "points": 1,
        "section": "Storage",
        "multiple_hint": null,
        "choices": [
          {
            "id": "a1b2c3d4",
            "text": "0…+4 °C"
          },
          {
            "id": "e5f6g7h8",
            "text": "+8…+12 °C"
          }
        ],
        "response": null,
        "flagged": false
      }
    ]
  }
}

GETGet my attempt state

GET /testing/my/attempts/{uuid}

Questions, saved answers and the remaining time of an attempt of the acting user.

Token scoperead
Permission Any employee; results follow the token's visibility
MCP tooltesting_my_attempts_get · read-only

Path parameters

uuid uuid Required
Attempt UUID.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Plan without API access
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_my_attempts_get",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    }
}
Response 200
{
  "data": {
    "attempt": {
      "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
      "status": "in_progress",
      "channel": "app",
      "strict": false,
      "started_at": "2026-11-02T16:02:00+05:00",
      "deadline_at": "2026-11-02T16:22:00+05:00",
      "remaining_sec": 1150,
      "current_index": 0,
      "violations": 0,
      "identity_photo": false
    },
    "test": {
      "title": "Food safety basics",
      "description": "For new kitchen staff",
      "navigation": "free",
      "pass_score": 80,
      "time_limit_minutes": 20,
      "questions_count": 15,
      "max_score": 15,
      "proctoring": null
    },
    "questions": [
      {
        "id": 901,
        "type": "single",
        "body": "At what temperature must chilled food be stored?",
        "media_url": null,
        "points": 1,
        "section": "Storage",
        "multiple_hint": null,
        "choices": [
          {
            "id": "a1b2c3d4",
            "text": "0…+4 °C"
          },
          {
            "id": "e5f6g7h8",
            "text": "+8…+12 °C"
          }
        ],
        "response": null,
        "flagged": false
      }
    ]
  }
}

POSTSave an answer

POST /testing/my/attempts/{uuid}/answer

Saves the answer to one question. response by type: single {"choice": "<id>"}, multiple {"choices": ["<id>"]}, true_false {"value": true}, text/essay {"text": "…"}, number {"value": 4}, ordering {"order": ["<id>", …]}, matching {"pairs": {"<left id>": "<right id>"}}. Pass flagged alone to mark the question for later.

Token scopewrite
Permission Any employee; results follow the token's visibility
MCP tooltesting_my_attempts_answer

Path parameters

uuid uuid Required
Attempt UUID.

Request body

question_id integer Required
response object | null
Answer, see the description.
time_spent_sec integer
Minimum 0.
flagged boolean

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Read-only token
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/answer' \
  -H 'Authorization: Bearer $JECHECK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "question_id": 42,
    "response": [],
    "time_spent_sec": 42,
    "flagged": true
}'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/answer', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      "question_id": 42,
      "response": [],
      "time_spent_sec": 42,
      "flagged": true
  }),
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/answer",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
    json={"question_id":42,"response":[],"time_spent_sec":42,"flagged":True},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/answer', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
    'json' => [
        'question_id' => 42,
        'response' => [
        ],
        'time_spent_sec' => 42,
        'flagged' => true,
    ],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_my_attempts_answer",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "question_id": 42,
            "response": [],
            "time_spent_sec": 42,
            "flagged": true
        }
    }
}
Response 200
{
  "data": {
    "saved": true,
    "answered": true,
    "remaining_sec": 1120
  }
}

POSTFinish my attempt

POST /testing/my/attempts/{uuid}/finish

Submits the attempt and returns the result in the scope allowed by the test (show).

Token scopewrite
Permission Any employee; results follow the token's visibility
MCP tooltesting_my_attempts_finish

Path parameters

uuid uuid Required
Attempt UUID.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Read-only token
404not_found — The record does not exist or is not visible to the token
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/finish' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/finish', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.post(
    "https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/finish",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('POST', 'testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/finish', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_my_attempts_finish",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    }
}
Response 200
{
  "data": {
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "title": "Food safety basics",
    "status": "completed",
    "show": "score",
    "finish_reason": "manual",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "pass_score": 80,
    "questions_count": 15,
    "certificate_number": "JC-2026-000412",
    "pdf_url": "https://app.jecheck.io/exam/a/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a/pdf",
    "retry": null,
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "correct_count": 13
  }
}

GETGet my attempt result

GET /testing/my/attempts/{uuid}/result
Token scoperead
Permission Any employee; results follow the token's visibility
MCP tooltesting_my_attempts_result · read-only

Path parameters

uuid uuid Required
Attempt UUID.

Responses

StatusDescription
200OK
401unauthorized — Missing, invalid, revoked or expired token
403forbidden · insufficient_scope · plan_required — Plan without API access
404not_found — The record does not exist or is not visible to the token
409conflict — The attempt is not finished yet
422validation_failed — Invalid parameters — see error.fields
429rate_limited — Too many requests — retry after Retry-After seconds
Request
curl 'https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/result' \
  -H 'Authorization: Bearer $JECHECK_TOKEN'
const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/result', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.JECHECK_TOKEN}`,
  },
});
const { data } = await res.json();
import os, requests

res = requests.get(
    "https://app.jecheck.io/api/ext/v1/testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/result",
    headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
)
res.raise_for_status()
data = res.json()["data"]
$client = new GuzzleHttp\Client(['base_uri' => 'https://app.jecheck.io/api/ext/v1/']);

$res = $client->request('GET', 'testing/my/attempts/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/result', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('JECHECK_TOKEN')],
]);
$data = json_decode($res->getBody(), true)['data'];
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
        "name": "testing_my_attempts_result",
        "arguments": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    }
}
Response 200
{
  "data": {
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "title": "Food safety basics",
    "status": "completed",
    "show": "score",
    "finish_reason": "manual",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "pass_score": 80,
    "questions_count": 15,
    "certificate_number": "JC-2026-000412",
    "pdf_url": "https://app.jecheck.io/exam/a/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a/pdf",
    "retry": null,
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "correct_count": 13
  }
}