Developers

The Skill Site API, version 1

Training plans The API is not included in the Teacher plan. Keys are made by an organisation admin.

Run and grade learners' code against your organisation's coding questions from your own site or course platform, or show the embeddable exercise and get each grade back. Learners there need no account of ours: you name them with your own ids.

Keys

An organisation admin makes keys under API in the organisation pages. A key is shown once; keep it on your server, never in a web page. Revoking a key stops it, and every embed made with it, at once.

Send every request to https://theskillsite.com/api/v1 (the main address: an organisation's own domain does not answer the API) with:

Authorization: Bearer tss_...
Content-Type: application/json

Errors

Errors are JSON with a matching HTTP status:

{ "error": { "code": "not_found", "message": "No such question or submission for this organisation." } }
Status Code When
400 invalid_json The body is not JSON
401 unauthorized No key, or a revoked one
404 not_found Not one of your organisation's questions or submissions
413 payload_too_large The body is over 200 KB
415 unsupported_media_type No Content-Type: application/json
422 invalid_request, question_has_no_tests A field is missing or too long (the message names it)
429 rate_limited Over the limit; Retry-After says when to try again
503 runner_busy The code runner is busy; try again shortly

Each organisation may make 30 runs and 30 submissions a minute, through the API and its embeds together, and each embedded learner 10 runs and 5 submissions of those. An organisation has one run at a time: a run asked for while another is still running answers 503 runner_busy. Code is at most 64 KB (65,536 bytes of UTF-8) and input at most 16 KB.

Questions

GET /questions lists the questions you may use: your organisation's own, graded by their output. Notebook questions, and unit-test questions (a test framework run over several files; built 11 November 2026, not yet deployed), are not offered through the API or embeds.

{
  "questions": [
    {
      "id": 2,
      "code": "demo-count-vowels",
      "title": "Count the vowels",
      "language": "python-3.14",
      "difficulty": "EASY"
    }
  ]
}

GET /questions/{id} gives one question with its visible tests. Hidden tests are counted, never shown.

{
  "id": 2,
  "code": "demo-count-vowels",
  "title": "Count the vowels",
  "prompt": "Read one line of text and print how many vowels ...",
  "language": "python-3.14",
  "difficulty": "EASY",
  "starter_code": "text = input()\n",
  "time_limit_ms": null,
  "tests": [{ "stdin": "hello", "expected_output": "2", "passes_on_clean_exit": false }],
  "hidden_tests": 3
}

A test with "passes_on_clean_exit": true passes when the program runs without an error, whatever it prints; its expected_output is empty. The question's files are not returned, text or binary: runs and grading give them to the program in its working directory, a binary file (an image, say) byte for byte as it was uploaded.

Running code

POST /runs with { "question_id": 2, "code": "...", "stdin": "Education" } runs the code once, in the question's language with its files and time limit, and answers { "status": "success", "output": "5" }. A program that fails to compile or crashes answers "status": "error" with the error as output.

Grading

POST /submissions with { "question_id": 2, "code": "...", "learner": { "id": "your-learner-id", "name": "Sam" } } answers 202 with the submission, "status": "waiting". It is graded in the background against every test, usually within seconds.

GET /submissions/{id}:

{
  "id": 1,
  "question_id": 2,
  "learner": { "id": "your-learner-id", "name": "Sam" },
  "status": "graded",
  "score": 100,
  "passed_tests": 4,
  "total_tests": 4,
  "hidden_tests": { "total": 1, "passed": 1 },
  "tests": [
    { "number": 1, "passed": true, "hidden": false, "result": "success" },
    { "number": 2, "passed": true, "hidden": true, "result": "success" }
  ],
  "error": null,
  "submitted_at": "2026-10-24T09:15:34.469Z",
  "graded_at": "2026-10-24T09:15:37.073Z"
}

status is waiting, graded, or failed (the runner refused the program, for example as too large; error says why). score is the percentage of test points passed.

Each submission also gives hidden_tests: how many hidden tests the question has and how many passed, as { "total": 2, "passed": 1 }. It is null when the submission has no result: still waiting, or failed because the runner refused the program. (On GET /questions/{id}, hidden_tests is the number of hidden tests.)

Grading stops when a submission has used 10 minutes of the code runner across its tests. The submission is then failed, with error set to "Grading stopped: this answer used more than 10 minutes of the code runner, so its remaining tests were not run and count as failed." The tests it did not reach show "result": "not_run", and they count as failed in passed_tests.

Result webhook

Set an https address under API, and every graded or failed submission is posted to it:

{ "type": "submission.graded", "submission": { ...as GET /submissions/{id} } }

Each post carries TheSkillSite-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> with the signing secret shown on the API page. Check it, and refuse posts whose t is more than a few minutes old. Answer with any 2xx status within five seconds; anything else, or no answer by then, is retried, further apart each time, for about three days.

import { createHmac, timingSafeEqual } from 'node:crypto';
const [t, v1] = header.split(',').map((part) => part.split('=')[1]);
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const valid = timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) && Date.now() / 1000 - Number(t) < 300;

The embeddable exercise

POST /embeds with { "question_id": 2, "learner": { "id": "your-learner-id", "name": "Sam" }, "origin": "https://courses.example.com" } answers 201 with { "url": "...", "expires_at": "..." }. Make one when the learner opens your page, and show it in an iframe on that origin:

<iframe src="https://theskillsite.com/embed/emb_..." style="width:100%;height:900px;border:0"></iframe>

Only pages on origin may frame it, and the link works for twelve hours. The learner sees the prompt, the visible tests and the starter code, runs their code with their own input, and submits. When it is graded the exercise tells your page, and only your page:

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://theskillsite.com' || event.data?.type !== 'theskillsite:graded') return;
  // event.data.submission is as GET /submissions/{id}
});

The webhook is posted for embedded submissions too; trust it, or GET /submissions/{id}, rather than the message for anything that matters, since a learner controls their own browser.

Try it with your next cohort

Start a 30-day free trial with up to 30 learners and no card, or see the demo for your kind of class and book a call with us.