All integrations

ClassMarker

BUSINESS · TASKS

Exam results, the question bank, and its categories in the account they connected.

Connecting ClassMarker in practice

ClassMarker allows 30 requests per hour per API key, which is two orders of magnitude tighter than most of this catalog and makes polling a non-starter — results are meant to arrive by webhook. Every request also carries a SHA256 hash of api_key + secret + timestamp valid within a five-minute window, so clock drift presents as an authentication failure.

Rate limits

  • 30 requests per hour per API key. Responses include next_request_after, the timestamp at which you may retry.
  • Adding or removing access codes does not count against the hourly allowance.
  • A maximum of 200 results per request; the limit parameter cannot exceed 200.

Worth knowing

  • Authentication is a signature, not a bearer token: hash('sha256', api_key + secret + timestamp) in lowercase, with the timestamp in seconds and only five minutes of allowed variance.
  • Webhooks retry within minutes on the first failure, then once an hour for 72 hours, and the webhook is set inactive after 1,000 consecutive failures; the counter resets on any 2XX and only the failing webhook is deactivated.
  • Webhook payloads are verified with an HMAC SHA256 in the X-Classmarker-Hmac-Sha256 header computed over the raw JSON body, so any middleware that reformats the body breaks verification.
  • Pagination is timestamp-based through finishedAfterTimestamp rather than page numbers — except question retrieval, which does use page — so two different walk patterns are needed in the same integration.
  • Access is granted per test result rather than by scope: ClassMarker’s wording is that you have "full control over adding and removing access to particular test results for each API key", and the account has to be upgraded before the API works at all.

Checked against ClassMarker API documentation (2026-09-29), ClassMarker webhooks (2026-09-29)

Acts as the person, not as itself

Each user connects their own account. Every call carries both identities — the agent and the person it is acting for — so the agent can never reach past what that individual can already do.

Credentials never touch the agent

Tokens live in the vault and attach server-side at call time. The agent holds a session, not a secret, and revoking access does not mean rotating a key.

Every call on the record

Who asked, which agent acted, which action ran, and the verdict that let it through — one audit trail across every integration, not one per vendor.

What an agent can do

Each action is granted on its own. An agent allowed to read is not thereby allowed to write, and the scope beside each row is what the acting user must have connected for it to run at all.

classmarker_delete_v1_accesslists_by_access_list_id_jsonWRITE

Remove access codes from an access list. DELETE /v1/accesslists/{access_list_id}.json. Removes up to 100 access codes from an API-controlled access list, revoking those codes' ability to start the exam. The list is named by path; the body is a JSON ARRAY OF STRINGS naming WHICH codes to remove, and it is not optional -- a delete that names no code is a delete that says nothing. Irreversible: re-adding a code is a separate call.

api
classmarker_get_v1_categories_jsonREAD

List question-bank categories. GET /v1/categories.json. Returns the whole category tree of the question bank: every parent category with the categories nested under it. Every account has the built-in `Generic Parent` (id 0) containing `Generic` (id 0). A parent with no children reports `categories: null` rather than an empty list.

api
classmarker_get_v1_groups_by_group_id_tests_by_test_id_recent_results_jsonREAD

Get recent results for one group exam. GET /v1/groups/{group_id}/tests/{test_id}/recent_results.json. Returns results for ONE exam inside ONE group, finished after a given moment. Narrower than the all-groups call and the right one when the group and exam are already known. An id that does not exist is not an error: the response is `no_results`, indistinguishable from a real exam nobody has taken, so confirm ids with the discovery call rather than reading an empty result as a wrong id.

api
classmarker_get_v1_groups_recent_results_jsonREAD

Get recent results across all groups. GET /v1/groups/recent_results.json. Returns exam results finished after a given moment, across every Group in the account. Use this to poll for new results; ClassMarker's own real-time delivery is webhooks, and this is the pull equivalent.

api
classmarker_get_v1_jsonREAD

List your groups, links and exams. GET /v1.json. Returns every Group and every Link in the ClassMarker account, each with the exams assigned to it, so a later call can name a specific group_id/test_id or link_id/test_id. This is the discovery call: start here when you do not already know the ids. Answers `no_results` with empty `groups` and `links` arrays on an account that has none.

api
classmarker_get_v1_links_by_link_id_tests_by_test_id_recent_results_jsonREAD

Get recent results for one link exam. GET /v1/links/{link_id}/tests/{test_id}/recent_results.json. Returns results for ONE exam behind ONE link, finished after a given moment. As with the group call, an unknown id answers `no_results` rather than an error.

api
classmarker_get_v1_links_recent_results_jsonREAD

Get recent results across all links. GET /v1/links/recent_results.json. Returns exam results finished after a given moment, across every Link in the account. The Link equivalent of the groups call: a Link is a public URL anyone can take the exam from, a Group is a roster of named test-takers.

api
classmarker_get_v1_questions_by_question_id_jsonREAD

Get one question. GET /v1/questions/{question_id}.json. Returns a single question from the question bank by id. A question id the account does not own answers `no_results` rather than an error.

api
classmarker_get_v1_questions_jsonREAD

List question-bank questions. GET /v1/questions.json. Returns one page of the account's question bank, each question with its type, category, text, options, correct answers, points and feedback. Paged: pass `page` to walk it. A page past the end is not an error -- it answers `ok` with an empty `questions` array.

api
classmarker_post_v1_accesslists_by_access_list_id_jsonWRITE

Add access codes to an access list. POST /v1/accesslists/{access_list_id}.json. Adds up to 100 access codes to an API-controlled access list, so the holders of those codes can start the exam the list guards. The list is named by path; the body is a JSON ARRAY OF STRINGS, one code per entry, each at most 255 characters. An access list id the account does not own answers `incorrectUrlId`.

api
classmarker_post_v1_categories_category_jsonWRITE

Create a category. POST /v1/categories/category.json. Creates a category inside a parent category and returns its `category_id`. Both `category_name` and `parent_category_id` are required; use parent 0 (`Generic Parent`) when there is no better home. Names must be unique across the account. `verify_only` validates without creating.

api
classmarker_post_v1_categories_parent_category_jsonWRITE

Create a parent category. POST /v1/categories/parent_category.json. Creates a top-level category in the question bank and returns its `parent_category_id`. Names must be unique: a duplicate answers `payloadValidationError` with `Parent category name already exists`. Pass `verify_only` to validate the payload without creating anything -- the response is `status: verified`.

api
classmarker_post_v1_questions_jsonWRITE

Add a question to the question bank. POST /v1/questions.json. Creates a question and returns it with its assigned `question_id`. Read the `body` schema before calling: ClassMarker's option format is specific and it rejects the obvious guesses. `verify_only` validates the payload without creating anything.

api
classmarker_put_v1_categories_parent_category_by_parent_category_id_jsonWRITE

Rename a parent category. PUT /v1/categories/parent_category/{parent_category_id}.json. Renames an existing top-level category. Send `parent_category_name` alone: `parent_category_id` is read-only and including it is refused, even though it is the id in the path. An id the account does not own answers `incorrectUrlId`. `verify_only` works here too.

api
classmarker_put_v1_questions_by_question_id_jsonWRITE

Update a question. PUT /v1/questions/{question_id}.json. Replaces an existing question. Send the WHOLE question, not a patch: the same fields the create call takes. A question id the account does not own answers `incorrectUrlId`. `verify_only` validates without saving.

api

Put ClassMarker behind one governed endpoint.

Same permissions, same audit trail, whatever else you connect next.