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.
GETList tests
All tests of the company with statistics limited to the locations of the testing.results permission.
| Token scope | read |
|---|---|
| Permission |
testing.manage or testing.results |
| Plan feature | testing |
| MCP tool | testing_tests_list · read-only |
Query parameters
page
integer
1. Minimum 1.
per_page
integer
25. Value 1–100.
status
array<string>
search
string
updated_since
date-time
Responses
| Status | Description |
|---|---|
| 200 | OK — a page of results with meta |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage / testing.results, or the plan has no testing
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
Test settings. Sections with questions and correct answers (sections, problems) are returned only with testing.manage; otherwise they are null.
| Token scope | read |
|---|---|
| Permission |
testing.manage or testing.results |
| Plan feature | testing |
| MCP tool | testing_tests_get · read-only |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage / testing.results, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"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
Creates a draft test with one section. Optionally pass questions to fill it in one call, then publish it.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_create |
Request body
title
string
Required description
string | null
category
string | null
cover_color
string | null
#2563eb.
Maximum length 20. time_limit_minutes
integer | null
pass_score
integer
attempts_limit
integer | null
retry_cooldown_hours
integer | null
shuffle_questions
boolean
shuffle_options
boolean
navigation
string
linear — no going back to previous questions.
freelinearshow_result
string
nonescoreanswersfullallow_app
boolean
allow_web
boolean
certificate_enabled
boolean
certificate_valid_days
integer | null
public_enabled
boolean
proctoring
object
enabled
boolean
camera
boolean
identity_photo
boolean
snapshot_interval
integer
fullscreen
boolean
block_copy
boolean
face_detection
boolean
max_violations
integer | null
questions
array<object>
type
string
Required singlemultipletrue_falsetextnumberorderingmatchingessaysection_id
integer
body
string
Required options
object
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
partial_credit
boolean
is_critical
boolean
explanation
string | null
show_result = full.
Maximum length 5000. media_url
string | null
testing.tests.images.upload).
Maximum length 1024. Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
Partial update of the test settings. Enabling strict mode (proctoring.enabled) also enables taking the test on the website.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_update |
Path parameters
id
integer
Required Request body
title
string
description
string | null
category
string | null
cover_color
string | null
#2563eb.
Maximum length 20. time_limit_minutes
integer | null
pass_score
integer
attempts_limit
integer | null
retry_cooldown_hours
integer | null
shuffle_questions
boolean
shuffle_options
boolean
navigation
string
linear — no going back to previous questions.
freelinearshow_result
string
nonescoreanswersfullallow_app
boolean
allow_web
boolean
certificate_enabled
boolean
certificate_valid_days
integer | null
public_enabled
boolean
proctoring
object
enabled
boolean
camera
boolean
identity_photo
boolean
snapshot_interval
integer
fullscreen
boolean
block_copy
boolean
face_detection
boolean
max_violations
integer | null
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
Publishes the test so it can be assigned. Fails with validation_failed listing the problems (no questions, missing correct answers…).
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_publish |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"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
Move a test to draft, archived or published (same checks as publish).
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_status |
Path parameters
id
integer
Required Request body
status
string
Required draftpublishedarchivedResponses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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": []
}
]
}
]
}
}POSTRegenerate the public link
Issues a new open link for the test; the old one stops working.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_public_link |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
curl -X POST 'https://app.jecheck.io/api/ext/v1/testing/tests/42/public-link' \
-H 'Authorization: Bearer $JECHECK_TOKEN'const res = await fetch('https://app.jecheck.io/api/ext/v1/testing/tests/42/public-link', {
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/public-link",
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/public-link', [
'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_public_link",
"arguments": {
"id": 42
}
}
}{
"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
Creates a draft copy with all sections and questions.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_duplicate |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"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
Deletes the test and closes its assignments. Results of past attempts are kept.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_delete · destructive |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"deleted": true
}
}GETGet test 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 scope | read |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_tests_analytics · read-only |
Path parameters
id
integer
Required Query parameters
date_from
date
date_to
date
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_sections_create |
Path parameters
id
integer
Required Request body
title
string
Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"data": {
"id": 78,
"title": "Hygiene",
"pick_count": null
}
}PATCHUpdate a section
Rename a section or set pick_count — how many random questions of the section each attempt gets (null — all).
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_sections_update |
Path parameters
id
integer
Required section_id
integer
Required Request body
title
string
pick_count
integer | null
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"id": 78,
"title": "Hygiene",
"pick_count": 5
}
}DELDelete a section
Questions of the deleted section move to another section.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_sections_delete · destructive |
Path parameters
id
integer
Required section_id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"deleted": true
}
}POSTAdd a question
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 scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_questions_create |
Path parameters
id
integer
Required Request body
type
string
Required singlemultipletrue_falsetextnumberorderingmatchingessaysection_id
integer
body
string
Required options
object
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
partial_credit
boolean
is_critical
boolean
explanation
string | null
show_result = full.
Maximum length 5000. media_url
string | null
testing.tests.images.upload).
Maximum length 1024. Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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": []
}
}
}{
"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
Adds up to 100 questions to a section in one call; items without a known type or text are skipped.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_questions_batch |
Path parameters
id
integer
Required Request body
section_id
integer | null
questions
array<object>
Required type
string
Required singlemultipletrue_falsetextnumberorderingmatchingessaysection_id
integer
body
string
Required options
object
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
partial_credit
boolean
is_critical
boolean
explanation
string | null
show_result = full.
Maximum length 5000. media_url
string | null
testing.tests.images.upload).
Maximum length 1024. Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
]
}
}
}{
"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
Partial update. Changing type converts the options where possible.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_questions_update |
Path parameters
id
integer
Required question_id
integer
Required Request body
type
string
singlemultipletrue_falsetextnumberorderingmatchingessaysection_id
integer
body
string
options
object
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
partial_credit
boolean
is_critical
boolean
explanation
string | null
show_result = full.
Maximum length 5000. media_url
string | null
testing.tests.images.upload).
Maximum length 1024. Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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": []
}
}
}{
"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
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_questions_duplicate |
Path parameters
id
integer
Required question_id
integer
Required Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"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
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_questions_delete · destructive |
Path parameters
id
integer
Required question_id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"deleted": true
}
}POSTUpload a question image
Uploads an image (up to 10 MB, converted to JPEG) and returns its URL for media_url of a question.
| Token scope | write |
|---|---|
| Permission |
testing.manage |
| Plan feature | testing |
| MCP tool | testing_tests_images_upload |
| Content type | multipart/form-data application/json |
Path parameters
id
integer
Required Request body
image
file
Required Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.manage, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"url": "https://files.jecheck.io/exams/761/questions/a8Kd02.jpg"
}
}GETList assignments of a test
Assignments with progress and per-employee status. Only employees of the locations of testing.results are included.
| Token scope | read |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignments_list · read-only |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"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
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 scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignments_create |
Path parameters
id
integer
Required Request body
targets
array<object>
Required kind
string
Required department — a location.
userpositiondepartmentallid
integer | null
all).
title
string | null
message
string | null
starts_at
date-time | null
due_at
date-time | null
recur_days
integer | null
recur_due_days
integer | null
close_after_due
boolean
auto_enroll
boolean
remind
boolean
Responses
| Status | Description |
|---|---|
| 201 | Created |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
| Token scope | read |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignments_get · read-only |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"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
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 scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignments_update |
Path parameters
id
integer
Required Request body
status
string
activecloseddue_at
date-time | null
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
Adds employees who now match the targets (new hires in the position or location) and notifies them.
| Token scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignments_sync |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"added": 2
}
}POSTRemind assignees
Sends a reminder to everyone who has not passed yet.
| Token scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignments_remind |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"reminded": 4
}
}DELDelete an assignment
| Token scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignments_delete · destructive |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"deleted": true
}
}POSTReset attempts of an employee
Starts a new cycle for one employee of an assignment (previous attempts stay in history) and notifies them.
| Token scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignees_restart |
Path parameters
id
integer
Required Request body
due_at
date-time | null
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"data": {
"ok": true
}
}POSTRemind an employee
| Token scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignees_remind |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"ok": true
}
}PATCHChange the deadline of an employee
| Token scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignees_update |
Path parameters
id
integer
Required Request body
due_at
date-time | null
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"data": {
"ok": true
}
}DELRemove an employee from an assignment
| Token scope | write |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_assignees_delete · destructive |
Path parameters
id
integer
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"deleted": true
}
}GETList attempts
Attempts of employees of the locations of testing.results (guests of open links — only with the permission for all locations), newest first.
| Token scope | read |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_attempts_list · read-only |
Query parameters
page
integer
1. Minimum 1.
per_page
integer
25. Value 1–100.
test_id
integer
user_id
integer
location_ids
array<integer>
status
string
flagged — strict-mode attempts with a low trust score.
passedfailedreviewannulledin_progressflaggedsearch
string
date_from
date
date_to
date
updated_since
date-time
Responses
| Status | Description |
|---|---|
| 200 | OK — a page of results with meta |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"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
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 scope | read |
|---|---|
| Permission |
testing.results |
| Plan feature | testing |
| MCP tool | testing_attempts_get · read-only |
Path parameters
uuid
uuid
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.results, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
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 scope | write |
|---|---|
| Permission |
testing.review |
| Plan feature | testing |
| MCP tool | testing_attempts_grade |
Path parameters
uuid
uuid
Required Request body
question_id
integer
Required points
number
Required feedback
string | null
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.review, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
Cancels the result (e.g. cheating in strict mode); the employee can take the test again.
| Token scope | write |
|---|---|
| Permission |
testing.review |
| Plan feature | testing |
| MCP tool | testing_attempts_annul |
Path parameters
uuid
uuid
Required Request body
note
string
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.review, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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."
}
}
}{
"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
Marks the attempt as reviewed (e.g. after checking a strict-mode recording).
| Token scope | write |
|---|---|
| Permission |
testing.review |
| Plan feature | testing |
| MCP tool | testing_attempts_confirm |
Path parameters
uuid
uuid
Required Request body
note
string | null
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Token lacks testing.review, is read-only, or the plan has no testing
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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."
}
}
}{
"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
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 scope | read |
|---|---|
| Permission | Any employee; results follow the token's visibility |
| MCP tool | testing_my_list · read-only |
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Plan without API access
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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": {}
}
}{
"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
| Token scope | read |
|---|---|
| Permission | Any employee; results follow the token's visibility |
| MCP tool | testing_my_history · read-only |
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Plan without API access
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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": {}
}
}{
"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
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 scope | write |
|---|---|
| Permission | Any employee; results follow the token's visibility |
| MCP tool | testing_my_start |
Request body
assignee_id
integer
Required assignee_id from testing.my.list.
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Read-only token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
| web_only | web_only — The test can only be taken on the website (strict mode with camera)
|
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
}
}
}{
"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
Questions, saved answers and the remaining time of an attempt of the acting user.
| Token scope | read |
|---|---|
| Permission | Any employee; results follow the token's visibility |
| MCP tool | testing_my_attempts_get · read-only |
Path parameters
uuid
uuid
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Plan without API access
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
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 scope | write |
|---|---|
| Permission | Any employee; results follow the token's visibility |
| MCP tool | testing_my_attempts_answer |
Path parameters
uuid
uuid
Required Request body
question_id
integer
Required response
object | null
time_spent_sec
integer
flagged
boolean
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Read-only token
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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
}
}
}{
"data": {
"saved": true,
"answered": true,
"remaining_sec": 1120
}
}POSTFinish my attempt
Submits the attempt and returns the result in the scope allowed by the test (show).
| Token scope | write |
|---|---|
| Permission | Any employee; results follow the token's visibility |
| MCP tool | testing_my_attempts_finish |
Path parameters
uuid
uuid
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Read-only token
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
| Token scope | read |
|---|---|
| Permission | Any employee; results follow the token's visibility |
| MCP tool | testing_my_attempts_result · read-only |
Path parameters
uuid
uuid
Required Responses
| Status | Description |
|---|---|
| 200 | OK |
| 401 | unauthorized — Missing, invalid, revoked or expired token
|
| 403 | forbidden · insufficient_scope · plan_required — Plan without API access
|
| 404 | not_found — The record does not exist or is not visible to the token
|
| 409 | conflict — The attempt is not finished yet
|
| 422 | validation_failed — Invalid parameters — see error.fields
|
| 429 | rate_limited — Too many requests — retry after Retry-After seconds
|
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"
}
}
}{
"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
}
}