Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: ClassQuill Public Operations API
description: 'REST API for ClassQuill tutoring organisations. Pipe your sessions, invoices, payments, tutor earnings, students, and more into your own tools. Most endpoints are reads; a focused set of writes (each gated by a *:write scope) create or update data, including creating student/tutor/parent accounts.
Authenticate every request with an org API key:
Authorization: Token token=cq_live_...
Mint keys in the ClassQuill app under Settings → Developers.'
version: 1.0.0
security:
- ApiKeyAuth: []
tags:
- name: Operations
paths:
/v1/mileage:
get:
tags:
- Operations
summary: List Mileage
description: 'List the org''s mileage trips (most recent first) — for reimbursement export.
Scoped directly by `organization_id`. Filter by tutor, status, or trip-date
range. `amount_cents`/`rate_cents` are the snapshots taken at approval.'
operationId: list_mileage_v1_mileage_get
parameters:
- name: tutor_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Filter by tutor UUID
title: Tutor Id
description: Filter by tutor UUID
- name: status
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: logged, approved, rejected, reimbursed
title: Status
description: logged, approved, rejected, reimbursed
- name: from
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: trip_date >= ISO date
title: From
description: trip_date >= ISO date
- name: to
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: trip_date <= ISO date
title: To
description: trip_date <= ISO date
- name: cursor
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
title: Cursor
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
- name: limit
in: query
required: false
schema:
type: integer
maximum: 100
minimum: 1
description: Records per page (default 20, max 100)
default: 20
title: Limit
description: Records per page (default 20, max 100)
- name: offset
in: query
required: false
schema:
type: integer
minimum: 0
description: Pagination offset (ignored when cursor is set)
default: 0
title: Offset
description: Pagination offset (ignored when cursor is set)
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Page_MileageTrip_'
'422':
description: Validation error — `error.details.errors` lists the fields.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Missing, malformed, revoked, or expired credentials.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: The credential lacks the scope this endpoint requires.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/expenses:
get:
tags:
- Operations
summary: List Expenses
description: 'List the org''s expense claims (most recent first) — for reimbursement export.
Scoped directly by `organization_id`. Filter by tutor, status, category, or
expense-date range.'
operationId: list_expenses_v1_expenses_get
parameters:
- name: tutor_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Filter by tutor UUID
title: Tutor Id
description: Filter by tutor UUID
- name: status
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: logged, approved, rejected, reimbursed
title: Status
description: logged, approved, rejected, reimbursed
- name: category
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: parking, tolls, materials, training, other
title: Category
description: parking, tolls, materials, training, other
- name: from
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: expense_date >= ISO date
title: From
description: expense_date >= ISO date
- name: to
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: expense_date <= ISO date
title: To
description: expense_date <= ISO date
- name: cursor
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
title: Cursor
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
- name: limit
in: query
required: false
schema:
type: integer
maximum: 100
minimum: 1
description: Records per page (default 20, max 100)
default: 20
title: Limit
description: Records per page (default 20, max 100)
- name: offset
in: query
required: false
schema:
type: integer
minimum: 0
description: Pagination offset (ignored when cursor is set)
default: 0
title: Offset
description: Pagination offset (ignored when cursor is set)
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Page_ExpenseClaim_'
'422':
description: Validation error — `error.details.errors` lists the fields.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Missing, malformed, revoked, or expired credentials.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: The credential lacks the scope this endpoint requires.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/adjustments:
get:
tags:
- Operations
summary: List Adjustments
description: 'List the org''s payroll adjustments (most recent first) — manual credits/debits
folded into payroll. A negative `amount_cents` is a deduction.
Scoped directly by `organization_id`. Voided adjustments are excluded unless
`include_voided=true`.'
operationId: list_adjustments_v1_adjustments_get
parameters:
- name: tutor_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Filter by tutor UUID
title: Tutor Id
description: Filter by tutor UUID
- name: include_voided
in: query
required: false
schema:
type: boolean
description: Include voided adjustments (default false)
default: false
title: Include Voided
description: Include voided adjustments (default false)
- name: from
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: period_start >= ISO date
title: From
description: period_start >= ISO date
- name: to
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: period_start <= ISO date
title: To
description: period_start <= ISO date
- name: cursor
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
title: Cursor
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
- name: limit
in: query
required: false
schema:
type: integer
maximum: 100
minimum: 1
description: Records per page (default 20, max 100)
default: 20
title: Limit
description: Records per page (default 20, max 100)
- name: offset
in: query
required: false
schema:
type: integer
minimum: 0
description: Pagination offset (ignored when cursor is set)
default: 0
title: Offset
description: Pagination offset (ignored when cursor is set)
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Page_Adjustment_'
'422':
description: Validation error — `error.details.errors` lists the fields.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Missing, malformed, revoked, or expired credentials.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: The credential lacks the scope this endpoint requires.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
post:
tags:
- Operations
summary: Create Adjustment
description: 'Record a payroll adjustment (manual credit/debit) for one of the org''s tutors.
A negative `amount_cents` is a deduction. `tutor_id` must be an accepted member
of your org (404 otherwise); `period_end` must be on or after `period_start`.
`organization_id`, `created_by`, and `created_at` are server-set.
Send an `Idempotency-Key` header to make retries safe — a repeat with the same
body replays the original response; a repeat with a different body is a 409.'
operationId: create_adjustment_v1_adjustments_post
parameters:
- name: Idempotency-Key
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Idempotency-Key
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AdjustmentCreate'
responses:
'201':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Adjustment'
'422':
description: Validation error — `error.details.errors` lists the fields.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Missing, malformed, revoked, or expired credentials.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: The credential lacks the scope this endpoint requires.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/locations:
get:
tags:
- Operations
summary: List Locations
description: 'List the saved locations owned by the org, its tutors, and its students.
`locations` has three owner columns (org_id / tutor_id / student_id), exactly
one of which is set; `owner_type` is resolved in the response. Scoped to the
union of this org, its accepted members, and its students.'
operationId: list_locations_v1_locations_get
parameters:
- name: owner_type
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: 'Filter by owner type: org, tutor, student'
title: Owner Type
description: 'Filter by owner type: org, tutor, student'
- name: tutor_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Filter by owning tutor UUID
title: Tutor Id
description: Filter by owning tutor UUID
- name: student_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Filter by owning student UUID
title: Student Id
description: Filter by owning student UUID
- name: cursor
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
title: Cursor
description: Opaque cursor from a previous response's meta.next_cursor, or the literal 'start' to begin cursor paging from the newest record. When set, offset is ignored and results page by a stable (created_at, id) keyset — recommended for large or incremental syncs.
- name: limit
in: query
required: false
schema:
type: integer
maximum: 100
minimum: 1
description: Records per page (default 20, max 100)
default: 20
title: Limit
description: Records per page (default 20, max 100)
- name: offset
in: query
required: false
schema:
type: integer
minimum: 0
description: Pagination offset (ignored when cursor is set)
default: 0
title: Offset
description: Pagination offset (ignored when cursor is set)
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Page_Location_'
'422':
description: Validation error — `error.details.errors` lists the fields.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Missing, malformed, revoked, or expired credentials.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: The credential lacks the scope this endpoint requires.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/locations/{location_id}:
get:
tags:
- Operations
summary: Get Location
description: Fetch a single location by ID. 404 unless owned by this org, a member, or a student of it.
operationId: get_location_v1_locations__location_id__get
parameters:
- name: location_id
in: path
required: true
schema:
type: string
description: Location UUID
title: Location Id
description: Location UUID
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Location'
'422':
description: Validation error — `error.details.errors` lists the fields.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Missing, malformed, revoked, or expired credentials.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: The credential lacks the scope this endpoint requires.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Not found — the record doesn't exist or belongs to another organisation.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
components:
schemas:
Page_MileageTrip_:
properties:
data:
items:
$ref: '#/components/schemas/MileageTrip'
type: array
title: Data
meta:
$ref: '#/components/schemas/PageMeta'
type: object
required:
- data
- meta
title: Page[MileageTrip]
Page_ExpenseClaim_:
properties:
data:
items:
$ref: '#/components/schemas/ExpenseClaim'
type: array
title: Data
meta:
$ref: '#/components/schemas/PageMeta'
type: object
required:
- data
- meta
title: Page[ExpenseClaim]
ApiErrorBody:
description: The `error` object every /v1 error response carries.
properties:
code:
description: 'Stable machine-readable error code: bad_request, unauthorized, forbidden, not_found, conflict, validation_error, rate_limited, or internal_error.'
examples:
- not_found
title: Code
type: string
message:
description: Human-readable explanation, safe to surface to end users.
examples:
- Session not found
title: Message
type: string
status:
description: The HTTP status code, repeated in the body.
examples:
- 404
title: Status
type: integer
details:
additionalProperties: true
description: Error-specific context. 422 carries the field errors under `errors`; 429 carries `retry_after_seconds`.
title: Details
type: object
required:
- code
- message
- status
title: ApiErrorBody
type: object
ApiError:
description: Canonical error envelope for every /v1 error response (4xx/5xx).
properties:
error:
$ref: '#/components/schemas/ApiErrorBody'
required:
- error
title: ApiError
type: object
Page_Location_:
properties:
data:
items:
$ref: '#/components/schemas/Location'
type: array
title: Data
meta:
$ref: '#/components/schemas/PageMeta'
type: object
required:
- data
- meta
title: Page[Location]
Page_Adjustment_:
properties:
data:
items:
$ref: '#/components/schemas/Adjustment'
type: array
title: Data
meta:
$ref: '#/components/schemas/PageMeta'
type: object
required:
- data
- meta
title: Page[Adjustment]
ExpenseClaim:
properties:
id:
type: string
title: Id
tutor_id:
anyOf:
- type: string
- type: 'null'
title: Tutor Id
expense_date:
anyOf:
- type: string
- type: 'null'
title: Expense Date
category:
anyOf:
- type: string
- type: 'null'
title: Category
vendor:
anyOf:
- type: string
- type: 'null'
title: Vendor
amount_cents:
anyOf:
- type: integer
- type: 'null'
title: Amount Cents
approved_amount_cents:
anyOf:
- type: integer
- type: 'null'
title: Approved Amount Cents
status:
anyOf:
- type: string
- type: 'null'
title: Status
receipt_photo_url:
anyOf:
- type: string
- type: 'null'
title: Receipt Photo Url
session_id:
anyOf:
- type: string
- type: 'null'
title: Session Id
approved_at:
anyOf:
- type: string
- type: 'null'
title: Approved At
created_at:
anyOf:
- type: string
- type: 'null'
title: Created At
type: object
required:
- id
title: ExpenseClaim
MileageTrip:
properties:
id:
type: string
title: Id
tutor_id:
anyOf:
- type: string
- type: 'null'
title: Tutor Id
trip_date:
anyOf:
- type: string
- type: 'null'
title: Trip Date
destination:
anyOf:
- type: string
- type: 'null'
title: Destination
distance_km:
anyOf:
- type: number
- type: 'null'
title: Distance Km
duration_minutes:
anyOf:
- type: integer
- type: 'null'
title: Duration Minutes
vehicle:
anyOf:
- type: string
- type: 'null'
title: Vehicle
tracking_method:
anyOf:
- type: string
- type: 'null'
title: Tracking Method
status:
anyOf:
- type: string
- type: 'null'
title: Status
rate_cents:
anyOf:
- type: integer
- type: 'null'
title: Rate Cents
amount_cents:
anyOf:
- type: integer
- type: 'null'
title: Amount Cents
session_id:
anyOf:
- type: string
- type: 'null'
title: Session Id
approved_at:
anyOf:
- type: string
- type: 'null'
title: Approved At
created_at:
anyOf:
- type: string
- type: 'null'
title: Created At
type: object
required:
- id
title: MileageTrip
PageMeta:
properties:
limit:
type: integer
title: Limit
offset:
anyOf:
- type: integer
- type: 'null'
title: Offset
count:
anyOf:
- type: integer
- type: 'null'
title: Count
total:
anyOf:
- type: integer
- type: 'null'
title: Total
next_cursor:
anyOf:
- type: string
- type: 'null'
title: Next Cursor
description: 'Cursor-mode only: pass as ?cursor= to fetch the next page; null when this is the last page.'
type: object
required:
- limit
title: PageMeta
description: 'Pagination envelope metadata. `count` is the size of the returned page;
`total` (when present) is the full result-set size for offset paging.
In cursor mode `offset`/`total` are absent and `next_cursor` carries the
opaque token for the next page (null on the last page).'
AdjustmentCreate:
properties:
tutor_id:
type: string
title: Tutor Id
period_start:
type: string
title: Period Start
description: ISO date, e.g. 2026-06-01
period_end:
type: string
title: Period End
description: ISO date; must be >= period_start
amount_cents:
type: integer
title: Amount Cents
description: Cents. Negative = deduction.
note:
type: string
minLength: 1
title: Note
description: Non-empty reason, e.g. 'Training bonus'.
additionalProperties: false
type: object
required:
- tutor_id
- period_start
- period_end
- amount_cents
- note
title: AdjustmentCreate
description: 'POST /v1/adjustments → org_payroll_adjustments. A negative `amount_cents`
is a deduction. `tutor_id` must be an accepted member of the key''s org.'
Location:
properties:
id:
type: string
title: Id
owner_type:
anyOf:
- type: string
- type: 'null'
title: Owner Type
owner_id:
anyOf:
- type: string
- type: 'null'
title: Owner Id
label:
anyOf:
- type: string
- type: 'null'
title: Label
suburb:
anyOf:
- type: string
- type: 'null'
title: Suburb
state:
anyOf:
- type: string
- type: 'null'
title: State
postcode:
anyOf:
- type: string
- type: 'null'
title: Postcode
is_default:
anyOf:
- type: boolean
- type: 'null'
title: Is Default
created_at:
anyOf:
- type: string
- type: 'null'
title: Created At
type: object
required:
- id
title: Location
description: 'A saved location owned by the org, one of its tutors, or one of its
students. `owner_type` is resolved from whichever owner column is set.'
Adjustment:
properties:
id:
type: string
title: Id
tutor_id:
anyOf:
- type: string
- type: 'null'
title: Tutor Id
period_start:
anyOf:
- type: string
- type: 'null'
title: Period Start
period_end:
anyOf:
- type: string
- type: 'null'
title: Period End
amount_cents:
anyOf:
- type: integer
- type: 'null'
title: Amount Cents
note:
anyOf:
- type: string
- type: 'null'
title: Note
voided:
type: boolean
title: Voided
default: false
created_at:
anyOf:
- type: string
- type: 'null'
title: Created At
type: object
required:
- id
title: Adjustment
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: Authorization
description: Org API key as `Token token=cq_live_...`