REST · v1

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.

Request token
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"]
  }'
Token response
{
  "accessToken": "ttt_at_8c1d…",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "role": "teacher",
  "scopes": ["curriculum.read", "progress.read"]
}
Authenticated request
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.

4 endpoints
GET/v1/year-groupscurriculum.read

List year groups available to the caller

200 response
{
  "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 }
}
GET/v1/topicscurriculum.read

List topics, filtered by year group, subject or status

ParameterTypeDescription
yearGroupIdstringRestrict to one year group
subjectIdstringRestrict to one subject
statusenumdraft | in_review | published | archived
pageintegerDefaults to 1, 25 items per page
200 response
{
  "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 }
}
POST/v1/topicscurriculum.write

Create a topic (admin only)

Request body
{
  "name": "Ratio and proportion",
  "subjectId": "s-maths",
  "yearGroupId": "yg-6",
  "description": "Compare quantities and scale recipes.",
  "status": "draft"
}
200 response
{
  "data": {
    "id": "t-ratio-y6",
    "name": "Ratio and proportion",
    "slug": "ratio-and-proportion",
    "status": "draft",
    "createdAt": "2026-08-13T08:12:04.000Z"
  }
}
PATCH/v1/lessons/{lessonId}content.write

Update a lesson or move it through the review workflow

Request body
{ "status": "published", "minutes": 12 }
200 response
{
  "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.

4 endpoints
GET/v1/practice-sets/{setId}practice.read

Fetch a practice set with its questions

200 response
{
  "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
      }
    ]
  }
}
POST/v1/attemptspractice.write

Submit an answer and receive instant marking

Request body
{
  "childId": "ch-charlie",
  "questionId": "q-frac-014",
  "response": "1/2",
  "secondsTaken": 34
}
200 response
{
  "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 }
  }
}
POST/v1/assignmentspractice.write

Assign a practice set to a child

Request body
{
  "practiceSetId": "ps-fractions-1",
  "childId": "ch-charlie",
  "dueOn": "2026-08-18"
}
200 response
{
  "data": {
    "id": "as-71bd93",
    "practiceSetId": "ps-fractions-1",
    "childId": "ch-charlie",
    "dueOn": "2026-08-18T00:00:00.000Z",
    "completed": false
  }
}
DELETE/v1/assignments/{assignmentId}practice.write

Withdraw an assignment

200 response
{ "data": { "id": "as-71bd93", "deleted": true } }

Progress and insights

Derived reporting: subject scores, topic mastery, activity series and paper history.

3 endpoints
GET/v1/children/{childId}/progressprogress.read

Overall progress with subject breakdown

ParameterTypeDescription
rangeintegerDays of activity to include (7, 14, 30, 90)
200 response
{
  "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
  }
}
GET/v1/children/{childId}/masteryprogress.read

Topic-level mastery, weakest first

200 response
{
  "data": [
    { "topicId": "t-fractions-y5", "mastery": 55, "answered": 24 },
    { "topicId": "t-decimals-y5", "mastery": 82, "answered": 18 }
  ]
}
POST/v1/children/{childId}/paper-attemptsprogress.write

Record a paper score

Request body
{ "paperId": "p-2024-maths-arith", "score": 32, "outOf": 40 }
200 response
{
  "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.

3 endpoints
POST/v1/childrenfamily.write

Add a child profile to the family account

Request body
{
  "firstName": "Ada",
  "lastName": "Whitfield",
  "yearGroupId": "yg-4",
  "schoolName": "Elmfield Primary"
}
200 response
{
  "data": { "id": "ch-ada", "seatsUsed": 3, "seatsAvailable": 4 }
}
GET/v1/subscriptionbilling.read

Current plan, seats and renewal date

200 response
{
  "data": {
    "id": "sub-whitfield",
    "plan": "family",
    "status": "active",
    "seats": 4,
    "pricePence": 2499,
    "renewsOn": "2026-09-04T00:00:00.000Z"
  }
}
PATCH/v1/subscriptionbilling.write

Change plan or cancel at period end

Request body
{ "plan": "annual" }
200 response
{
  "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.

2 endpoints
POST/v1/assistant/messagesassistant.write

Ask a question and receive an explanation plus suggested practice

Request body
{
  "childId": "ch-charlie",
  "prompt": "Charlie keeps getting fractions wrong. What should we do?"
}
200 response
{
  "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" }
    ]
  }
}
POST/v1/assistant/practice-setsassistant.write

Save generated questions as a practice set

Request body
{
  "title": "Fractions booster",
  "topicId": "t-fractions-y5",
  "questionIds": ["q-gen-01", "q-gen-02", "q-gen-03"]
}
200 response
{ "data": { "id": "ps-gen-4kd8", "status": "draft", "questionCount": 3 } }

Errors

Every failure returns the same envelope

Error envelope
{
  "error": {
    "status": 409,
    "code": "seat_limit_reached",
    "message": "The Monthly plan allows 1 child profile.",
    "details": { "seats": 1, "seatsUsed": 1 },
    "requestId": "req_7fd21a"
  }
}
StatusCodeWhen it happens
400invalid_requestA field is missing or the wrong shape. The response lists each field.
401unauthenticatedNo token, an expired token, or a token for a different environment.
403forbidden_scopeThe token is valid but lacks the scope for this route, or the role cannot reach it.
404not_foundThe resource does not exist, or is not visible to this role.
409seat_limit_reachedAdding a child profile would exceed the seats on the current plan.
429rate_limited600 requests per minute per token. Retry after the header interval.

Webhooks

Signed events, delivered with exponential backoff for 24 hours

attempt.recorded

Fires after each answer is marked, with the topic mastery delta.

assignment.completed

Fires when a child finishes every question in an assigned set.

content.published

Fires when a lesson, question, video or paper goes live.

subscription.updated

Plan, seat or status change on a family account.

paper.scored

A SATs paper score has been recorded against a child.

Delivery payload
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.

Prefer to see it working?

Open the platform and follow the same flows these endpoints power.