Authentication
Every request carries an API key in the Authorization header: Authorization: Bearer eco_xxxxxxxx_… . A school admin creates the key and chooses what it may read (its scopes). The key is shown only once; Ecolia keeps only a hash of it.
A key reads only its own school. It can be given an end date and revoked at any time — it stops working at once. Keep keys on your server, never in a web page or an app.
Base address
All endpoints are under /api/v1 of your Ecolia address. Answers are JSON (UTF-8). Dates are YYYY-MM-DD, times of day HH:MM in the school’s time zone, timestamps ISO 8601 in UTC. Ids are stable UUIDs. https://ecolia.eu/api/v1
Endpoints
All endpoints are GET. Unknown parameters are refused, so typos show up at once.
GET /api/v1
Which school the key belongs to and its scopes.
- Scope
—- Parameters
- none
GET /api/v1/students
Current students: id, class, level, names, student number.
- Scope
read:students- Parameters
class_id— only this classinclude_left— true: also students who leftlimit— 1–200, default 50cursor— next page
GET /api/v1/classes
Classes of the current school year, with their year and class teacher id.
- Scope
read:classes- Parameters
school_year_id— a given yearall_years— true: every yearlimit— 1–200cursor— next page
GET /api/v1/timetable
Weekly lessons: weekday (1 = Monday … 7 = Sunday), start, end, subject, teacher id, room.
- Scope
read:timetable- Parameters
class_id— only this classlimit— 1–200cursor— next page
GET /api/v1/attendance
Absences and late arrivals: status absent or late, minutes late, excused, reported by a parent.
- Scope
read:attendance- Parameters
from, to— required, at most 366 daysclass_id— only this classstudent_id— only this studentlimit— 1–200cursor— next page
GET /api/v1/grades
Published grades only (what parents see): test, subject, date, term, score, maximum, weight.
- Scope
read:grades- Parameters
from, to— dates of the teststerm— 1–4class_id— only this classstudent_id— only this studentlimit— 1–200cursor— next page
Examples
curl https://ecolia.eu/api/v1/students?limit=200 \
-H "Authorization: Bearer $ECOLIA_KEY"
# next page
curl "https://ecolia.eu/api/v1/students?limit=200&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer $ECOLIA_KEY"
# absences of October
curl "https://ecolia.eu/api/v1/attendance?from=2026-10-01&to=2026-10-31" \
-H "Authorization: Bearer $ECOLIA_KEY"{
"data": [
{ "id": "6b1f…", "class_id": "a3c2…", "level_id": null, "first_name": "Sami",
"middle_name": null, "last_name": "Example", "last_initial": "E",
"student_number": "2026-014", "left_at": null, "created_at": "2026-09-01T07:12:44.000Z" }
],
"next_cursor": "NmIxZi4uLg"
}Pages
Lists answer { "data": [ … ], "next_cursor": "…" }. While next_cursor is not null, ask again with cursor=<next_cursor> (and the same filters) to get the next page.
The order is stable (by id, attendance by date then id), so no row is skipped or repeated between pages.
Errors
Errors answer { "error": { "code": "…", "message": "…" } } with a matching HTTP status.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A parameter is missing, unknown or not valid. |
| 401 | unauthorized | No key, an unknown key, or a revoked or expired one. |
| 403 | insufficient_scope | The key may not read this. |
| 403 | module_off | The school does not use this part of Ecolia. |
| 404 | not_found | No such endpoint. |
| 429 | rate_limited | Too many requests; wait for the time in Retry-After. |
| 500 | server_error | Something went wrong on our side; try again later. |
Rate limits
Each key may make 60 requests per minute and 10,000 per day. Above that, the answer is 429 with a Retry-After header. Ask for 200 rows per page to need fewer requests.
Webhooks
A school admin registers an https address and chooses events. When one happens, Ecolia sends a POST with a JSON body to that address. Private, internal and loopback addresses are refused.
The body holds only ids and a few fields — never names, notes or health data. Read the details with the API.
Events
student.created- A student was added (student_id, class_id, level_id).
student.updated- A student’s name, class, level, number or leaving date changed.
attendance.recorded- An absence or late arrival was recorded or changed (attendance_id, student_id, date, status, excused).
grades.published- A test’s grades were published to parents (assessment_id, class_id, term, date).
ping- Sent by the “Send a test” button.
{
"id": "0d8e…",
"type": "attendance.recorded",
"school_id": "5f20…",
"created_at": "2026-10-31T08:15:02.120Z",
"data": { "attendance_id": "91ab…", "student_id": "6b1f…", "date": "2026-10-31", "status": "absent", "excused": false }
}Headers
Ecolia-Webhook-Id- the event id (the same for every retry: use it to ignore duplicates)
Ecolia-Event- the event type
Ecolia-Timestamp- when it was sent, in seconds since 1970 (UTC)
Ecolia-Signature- v1= followed by the hex HMAC-SHA256 of "<timestamp>.<body>" with your signing secret
Ecolia-Attempt- 1 for the first try, then 2, 3 …
Answers and retries
Answer with any 2xx status within 10 seconds. Otherwise Ecolia tries again after 1 minute, 5 minutes, 30 minutes, 2, 6 and 12 hours (7 attempts in all). Redirects are not followed.
After 15 failed attempts in a row the webhook is turned off; an admin can turn it back on. The last 50 deliveries are shown in the settings.
Checking the signature
The signing secret (whsec_…) is shown once when the webhook is created. Compute the HMAC over the raw body exactly as received, compare in constant time, and refuse timestamps older than 5 minutes (against replays).
import { createHmac, timingSafeEqual } from 'node:crypto';
// body: the raw request body as a string (before JSON.parse)
export function isFromEcolia(secret, body, signature, timestamp) {
const ts = Number(timestamp);
if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = 'v1=' + createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(signature ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}
// Express: app.post('/ecolia', express.text({ type: 'application/json' }), (req, res) => {
// if (!isFromEcolia(process.env.ECOLIA_WEBHOOK_SECRET, req.body,
// req.get('Ecolia-Signature'), req.get('Ecolia-Timestamp'))) return res.sendStatus(400);
// const event = JSON.parse(req.body); /* … */ res.sendStatus(204);
// });import hashlib, hmac, time
def is_from_ecolia(secret: str, body: bytes, signature: str, timestamp: str) -> bool:
try:
ts = int(timestamp)
except (TypeError, ValueError):
return False
if abs(time.time() - ts) > 300:
return False
expected = "v1=" + hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
# Flask:
# @app.post("/ecolia")
# def ecolia():
# if not is_from_ecolia(SECRET, request.get_data(), request.headers.get("Ecolia-Signature"),
# request.headers.get("Ecolia-Timestamp")):
# abort(400)
# event = request.get_json(); ...; return "", 204Data protection
The API is read-only. Health information, support plans, private notes, parents’ notes on absences and grade comments are never returned. Every key and webhook change is written to the school’s activity log (ids only).
The school remains the controller of its data: share keys only with processors it has a contract with.