Skip to content

For developers

Receive test results on your platform

When a test ends, Echelio can send its results to your school’s platform: a learning platform, student records, or a script of your own. This page is for the developer who sets up the receiving end.

Last updated October 29, 2026

How it works

An administrator of your school sets an address under Organization → Webhook in Echelio. From then on, each test that ends is sent to that address as an HTTPS POST with a JSON body. It is part of the Organization plan.

  • Every test of the school: code tests, quizzes and mixed tests. Practice is never sent.
  • A full copy each time: every student’s results, not only what changed. Keep the latest copy.
  • Signed as the Standard Webhooks specification describes, so you can check each request came from Echelio.
  • No code and no answers: scores, participation, how each question ended, finish times and tab switches.

When it is sent

A test is sent about a minute after it ends, when its end time passes or a teacher stops it. It is sent again, as a new full copy, whenever a result changes after that:

  • a submission made before the end finishes grading;
  • a teacher grades an answer, or accepts an answer for every student who gave it;
  • the AI grades an open answer;
  • a student is removed from the class, or added back.

Changes within a minute of each other go in one copy, so a teacher grading 30 answers sends one or two copies, not thirty.

Each copy of a test has a sequence one higher than the last. While answers still wait for grading, session.final is false; a later copy brings the settled results.

Tests that ended before the address was set are not sent by themselves; an administrator can send one from its page. To try your side, use Send a test event under Organization → Webhook: it sends made-up results with "type": "test".

The request

Echelio sends a POST to your address with these headers:

content-type
always application/json;
user-agent
Echelio-Webhooks/1;
webhook-id
the delivery’s id, the same on every retry of it;
webhook-timestamp
when it was sent, in seconds since 1 January 1970 (UTC);
webhook-signature
v1, followed by the signature, explained below.

Answer with any 2xx status within 10 seconds. Anything else counts as a failure and is tried again, a redirect included: redirects are not followed. The address must use HTTPS and be reachable from the internet.

What it contains

A mixed test, sent while one answer still waits for grading:

{
  "type": "session.results",
  "sequence": 2,
  "sentAt": "2026-11-03T10:15:02.000Z",
  "organization": {
    "id": "cmo1x8r2k0000",
    "name": "Example School"
  },
  "class": {
    "id": "cmc4b7t9p0002",
    "name": "Web development · Group A"
  },
  "session": {
    "id": "cms9d2v6h0004",
    "name": "Week 3 test",
    "format": "MIXED",
    "startsAt": "2026-11-03T08:30:00.000Z",
    "endsAt": "2026-11-03T10:00:00.000Z",
    "closedAt": "2026-11-03T10:00:00.000Z",
    "stoppedEarly": false,
    "final": false,
    "codeWeight": 60,
    "resultsUrl": "https://echelio.com/teacher/sessions/cms9d2v6h0004",
    "questions": [
      {
        "key": "q1",
        "part": "QUIZ",
        "points": 2
      },
      {
        "key": "q2",
        "part": "QUIZ",
        "points": 3
      },
      {
        "key": "c1",
        "part": "CODE",
        "title": "Sum of digits",
        "language": "PYTHON",
        "weight": 1
      }
    ]
  },
  "students": [
    {
      "id": "cmu2f5k8n0006",
      "name": "Ada Example",
      "email": "[email protected]",
      "removed": false,
      "participation": "SUBMITTED",
      "provisional": true,
      "rank": 1,
      "score": 82.5,
      "quiz": {
        "points": 2,
        "totalPoints": 5,
        "score": 40
      },
      "code": {
        "score": 100,
        "passed": 1
      },
      "completedAt": "2026-11-03T09:41:30.000Z",
      "completionMinutes": 71.5,
      "tabSwitches": 2,
      "timeAwayMinutes": 0.8,
      "questions": [
        {
          "key": "q1",
          "outcome": "CORRECT"
        },
        {
          "key": "q2",
          "outcome": "TO_REVIEW"
        },
        {
          "key": "c1",
          "score": 100
        }
      ]
    },
    {
      "id": "cmu2f5k8n0007",
      "name": "Bo Example",
      "email": "[email protected]",
      "removed": false,
      "participation": "MISSED",
      "provisional": false,
      "rank": null,
      "score": null,
      "quiz": {
        "points": null,
        "totalPoints": 5,
        "score": null
      },
      "code": {
        "score": null,
        "passed": null
      },
      "completedAt": null,
      "completionMinutes": null,
      "tabSwitches": 0,
      "timeAwayMinutes": 0,
      "questions": [
        {
          "key": "q1",
          "outcome": "NOT_REACHED"
        },
        {
          "key": "q2",
          "outcome": "NOT_REACHED"
        },
        {
          "key": "c1",
          "score": null
        }
      ]
    }
  ]
}

The top level

type
session.results, or test for a test event.
sequence
Grows by one with each copy of the same test, from 1. A test event has 0.
sentAt
When this copy was built, in ISO 8601 (UTC).
organization
Your school: id and name.
class
The class the test was given to: id and name.

session

id
The test’s id in Echelio, the same in every copy.
name
The test’s name, as the teacher wrote it.
format
CODE, QUIZ or MIXED.
startsAt, endsAt
The test’s scheduled start and end.
closedAt
When it ended: its end time, or when a teacher stopped it.
stoppedEarly
Whether a teacher stopped it before its end time.
final
True once no student’s result can still change by grading.
codeWeight
Mixed tests: the code part’s share of the score, in percent. Null otherwise.
resultsUrl
The test’s results page in Echelio, for teachers.
questions
The questions and problems, in the order students meet them.

session.questions[]

key
q1, q2… for quiz questions, c1, c2… for code problems. Students’ results use the same keys.
part
QUIZ or CODE.
points
Quiz questions: what the question is worth.
title, language, weight
Code problems: the problem’s title, its programming language, and its weight in the score.

students[]

id
The student’s id in Echelio.
name, email
The student’s name, and the email they sign in with.
removed
True when a teacher removed the student from the class after the test; their results stay.
participation
How the student took part (values below).
provisional
True while one of the student’s answers or submissions still waits for grading.
rank
The student’s place in the class; null without a score.
score
The student’s score on the whole test; null when they did not take it.
quiz
Quizzes and mixed tests: points, totalPoints and the quiz part’s score. Null in a code test.
code
Code and mixed tests: the code part’s score and how many problems were passed in full. Null in a quiz.
completedAt, completionMinutes
When the student finished, and how long after the start, in minutes; null if they never finished.
tabSwitches, timeAwayMinutes
How often the student left the test’s tab, and for how long in all; null when the test does not record tab switches.
questions
Each question’s outcome (quiz) or best score (code), by key.

Scores are percentages from 0 to 100, rounded to one decimal, and times are in minutes, as in Echelio’s CSV export.

participation is one of SUBMITTED, TIMED_OUT, STOPPED, and MISSED.

A quiz question’s outcome is one of CORRECT, WRONG, TO_REVIEW, SKIPPED, and NOT_REACHED; TO_REVIEW waits for a teacher.

Checking the signature

Your administrator finds the signing secret under Organization → Webhook. It starts with whsec_; keep it on your server only. Check each request against its raw body, before parsing the JSON: the signature covers the exact bytes Echelio sent.

With a library

Standard Webhooks offers libraries for many languages, listed on standardwebhooks.com. In JavaScript:

import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.ECHELIO_WEBHOOK_SECRET);

// rawBody is the request's body as received, before any JSON parsing.
// verify() throws when the signature or the timestamp is wrong.
const payload = webhook.verify(rawBody, {
  "webhook-id": request.headers["webhook-id"],
  "webhook-timestamp": request.headers["webhook-timestamp"],
  "webhook-signature": request.headers["webhook-signature"],
});

Without a library

The signature is the HMAC-SHA256 of webhook-id, webhook-timestamp and the body, joined by dots, keyed with the secret’s bytes after whsec_ (base64), and written in base64. In Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, headers, secret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  // Refuse a request more than five minutes old, so an old one cannot be replayed.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
  // The header can hold several signatures, separated by spaces.
  return headers["webhook-signature"].split(" ").some((part) => {
    const [version, signature] = part.split(",");
    const given = Buffer.from(signature ?? "", "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

When an administrator makes a new secret, the old one stops working at once; deliveries that fail meanwhile are tried again, signed with the new one.

Handling deliveries

  • Answer first, then work. Store the body and answer 200, then process it, so slow work never makes a delivery fail.
  • Keep the highest sequence. For each session.id, keep the copy with the highest sequence and ignore a lower one.
  • Skip what you have seen. A delivery can arrive twice, for example when your answer was lost; skip a webhook-id you have already handled.
  • Leave test events out. Requests with "type": "test" carry made-up students.
  • Match students by email, the address they sign in with, or by Echelio’s id.

When delivery fails

Echelio tries a delivery eight times over about two days: at once, then after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. A newer copy of the same test takes the place of one still waiting to be tried again.

If every attempt fails, the school’s administrators get an email, at most one a day. The delivery log under Organization → Webhook shows each delivery, its attempts and your platform’s answer for 30 days. Fix your side, then press Send again.