API reference
Everything the platform does is available over a JSON API: curriculum content, practice and marking, progress reporting, family accounts and the Ask TTT assistant. All requests are scoped to a role, and every write is recorded in the audit log.
Base URL
https://api.teachersthattutor.co.uk
Version
v1 (date-based, additive changes)
Format
application/json · UTF-8
Authentication
Bearer tokens issued per integration, scoped to a role
Exchange an integration key for a short-lived access token, then send it as a bearer token on every request. Tokens inherit the role of the integration: a pupil token can never read billing, and a teacher token can never change a subscription.
curl -X POST https://api.teachersthattutor.co.uk/v1/auth/token \
-H "Content-Type: application/json" \
-d '{
"clientId": "ttt_live_9f2a",
"clientSecret": "••••••••",
"scopes": ["curriculum.read", "progress.read"]
}'{
"accessToken": "ttt_at_8c1d…",
"tokenType": "Bearer",
"expiresIn": 3600,
"role": "teacher",
"scopes": ["curriculum.read", "progress.read"]
}curl https://api.teachersthattutor.co.uk/v1/topics?yearGroupId=yg-6 \
-H "Authorization: Bearer ttt_at_8c1d…" \
-H "TTT-Version: 2026-08-01"Scoped tokens
Request only the scopes an integration needs. Scope failures return 403 with the missing scope named.
Versioning
Send TTT-Version to pin behaviour. Without it, requests use the oldest supported version.
Rate limits
600 requests per minute per token, with burst headroom of 60. Limits appear in response headers.
Curriculum
Year groups, subjects, topics and lessons. The spine every other resource hangs from.
/v1/year-groupscurriculum.readList year groups available to the caller
{
"data": [
{ "id": "yg-5", "name": "Year 5", "keyStage": "Upper KS2", "order": 5 },
{ "id": "yg-6", "name": "Year 6", "keyStage": "Upper KS2", "order": 6 }
],
"meta": { "total": 4 }
}/v1/topicscurriculum.readList topics, filtered by year group, subject or status
| Parameter | Type | Description |
|---|---|---|
| yearGroupId | string | Restrict to one year group |
| subjectId | string | Restrict to one subject |
| status | enum | draft | in_review | published | archived |
| page | integer | Defaults to 1, 25 items per page |
{
"data": [
{
"id": "t-fractions-y5",
"name": "Fractions",
"slug": "fractions",
"subjectId": "s-maths",
"yearGroupId": "yg-5",
"status": "published",
"counts": { "lessons": 4, "questions": 32, "videos": 3 }
}
],
"meta": { "page": 1, "perPage": 25, "total": 48 }
}/v1/topicscurriculum.writeCreate a topic (admin only)
{
"name": "Ratio and proportion",
"subjectId": "s-maths",
"yearGroupId": "yg-6",
"description": "Compare quantities and scale recipes.",
"status": "draft"
}{
"data": {
"id": "t-ratio-y6",
"name": "Ratio and proportion",
"slug": "ratio-and-proportion",
"status": "draft",
"createdAt": "2026-08-13T08:12:04.000Z"
}
}/v1/lessons/{lessonId}content.writeUpdate a lesson or move it through the review workflow
{ "status": "published", "minutes": 12 }{
"data": {
"id": "l-fractions-add",
"title": "Adding fractions with different denominators",
"status": "published",
"updatedAt": "2026-08-13T08:20:11.000Z"
},
"audit": { "actorId": "u-admin", "action": "published" }
}Practice and marking
Questions, practice sets and the marking endpoint that returns an explanation with every result.
/v1/practice-sets/{setId}practice.readFetch a practice set with its questions
{
"data": {
"id": "ps-fractions-1",
"title": "Fractions: adding and subtracting",
"topicId": "t-fractions-y5",
"minutes": 12,
"questions": [
{
"id": "q-frac-014",
"type": "short_answer",
"difficulty": "secure",
"prompt": "Calculate 1/3 + 1/6. Give your answer in its simplest form.",
"marks": 1
}
]
}
}/v1/attemptspractice.writeSubmit an answer and receive instant marking
{
"childId": "ch-charlie",
"questionId": "q-frac-014",
"response": "1/2",
"secondsTaken": 34
}{
"data": {
"id": "at-9f3ka2",
"correct": true,
"marksAwarded": 1,
"explanation": "Convert to sixths: 1/3 = 2/6, so 2/6 + 1/6 = 3/6 = 1/2.",
"topicMastery": { "topicId": "t-fractions-y5", "before": 61, "after": 65 }
}
}/v1/assignmentspractice.writeAssign a practice set to a child
{
"practiceSetId": "ps-fractions-1",
"childId": "ch-charlie",
"dueOn": "2026-08-18"
}{
"data": {
"id": "as-71bd93",
"practiceSetId": "ps-fractions-1",
"childId": "ch-charlie",
"dueOn": "2026-08-18T00:00:00.000Z",
"completed": false
}
}/v1/assignments/{assignmentId}practice.writeWithdraw an assignment
{ "data": { "id": "as-71bd93", "deleted": true } }Progress and insights
Derived reporting: subject scores, topic mastery, activity series and paper history.
/v1/children/{childId}/progressprogress.readOverall progress with subject breakdown
| Parameter | Type | Description |
|---|---|---|
| range | integer | Days of activity to include (7, 14, 30, 90) |
{
"data": {
"childId": "ch-charlie",
"overall": { "score": 77, "trend": 6 },
"subjects": [
{ "subjectId": "s-maths", "score": 78, "trend": 4, "label": "Improving" },
{ "subjectId": "s-english", "score": 71, "trend": 0, "label": "Steady" }
],
"streakDays": 12,
"minutesPractised": 486
}
}/v1/children/{childId}/masteryprogress.readTopic-level mastery, weakest first
{
"data": [
{ "topicId": "t-fractions-y5", "mastery": 55, "answered": 24 },
{ "topicId": "t-decimals-y5", "mastery": 82, "answered": 18 }
]
}/v1/children/{childId}/paper-attemptsprogress.writeRecord a paper score
{ "paperId": "p-2024-maths-arith", "score": 32, "outOf": 40 }{
"data": {
"id": "pa-4ka91",
"percentage": 80,
"paperAverage": 76,
"readiness": "On track for the expected standard"
}
}Accounts and subscriptions
Families, child profiles, plans and invoices. Seat limits are enforced on write.
/v1/childrenfamily.writeAdd a child profile to the family account
{
"firstName": "Ada",
"lastName": "Whitfield",
"yearGroupId": "yg-4",
"schoolName": "Elmfield Primary"
}{
"data": { "id": "ch-ada", "seatsUsed": 3, "seatsAvailable": 4 }
}/v1/subscriptionbilling.readCurrent plan, seats and renewal date
{
"data": {
"id": "sub-whitfield",
"plan": "family",
"status": "active",
"seats": 4,
"pricePence": 2499,
"renewsOn": "2026-09-04T00:00:00.000Z"
}
}/v1/subscriptionbilling.writeChange plan or cancel at period end
{ "plan": "annual" }{
"data": {
"plan": "annual",
"pricePence": 12900,
"seats": 2,
"effectiveFrom": "2026-09-04T00:00:00.000Z"
}
}Ask TTT
Grounded explanations and generated practice, always tied to published curriculum content.
/v1/assistant/messagesassistant.writeAsk a question and receive an explanation plus suggested practice
{
"childId": "ch-charlie",
"prompt": "Charlie keeps getting fractions wrong. What should we do?"
}{
"data": {
"reply": "Charlie is at 55% on Fractions in Year 5. The misses are all on unlike denominators…",
"citations": [
{ "type": "lesson", "id": "l-fractions-add" },
{ "type": "video", "id": "v-equivalent-fractions" }
],
"generatedQuestions": [
{ "prompt": "Work out 2/5 + 1/10", "answer": "1/2", "difficulty": "secure" }
]
}
}/v1/assistant/practice-setsassistant.writeSave generated questions as a practice set
{
"title": "Fractions booster",
"topicId": "t-fractions-y5",
"questionIds": ["q-gen-01", "q-gen-02", "q-gen-03"]
}{ "data": { "id": "ps-gen-4kd8", "status": "draft", "questionCount": 3 } }Errors
Every failure returns the same envelope
{
"error": {
"status": 409,
"code": "seat_limit_reached",
"message": "The Monthly plan allows 1 child profile.",
"details": { "seats": 1, "seatsUsed": 1 },
"requestId": "req_7fd21a"
}
}| Status | Code | When it happens |
|---|---|---|
| 400 | invalid_request | A field is missing or the wrong shape. The response lists each field. |
| 401 | unauthenticated | No token, an expired token, or a token for a different environment. |
| 403 | forbidden_scope | The token is valid but lacks the scope for this route, or the role cannot reach it. |
| 404 | not_found | The resource does not exist, or is not visible to this role. |
| 409 | seat_limit_reached | Adding a child profile would exceed the seats on the current plan. |
| 429 | rate_limited | 600 requests per minute per token. Retry after the header interval. |
Webhooks
Signed events, delivered with exponential backoff for 24 hours
attempt.recordedFires after each answer is marked, with the topic mastery delta.
assignment.completedFires when a child finishes every question in an assigned set.
content.publishedFires when a lesson, question, video or paper goes live.
subscription.updatedPlan, seat or status change on a family account.
paper.scoredA SATs paper score has been recorded against a child.
POST https://your-app.example/hooks/ttt
TTT-Signature: t=1786000000,v1=6b3c…
{
"id": "evt_2p8fa1",
"type": "attempt.recorded",
"createdAt": "2026-08-13T08:41:02.000Z",
"data": {
"childId": "ch-charlie",
"questionId": "q-frac-014",
"correct": true,
"topicId": "t-fractions-y5",
"masteryAfter": 65
}
}Verify the signature with your endpoint secret before trusting a payload, and treat delivery as at-least-once: use the event id to make handlers idempotent.