External Graders
Register your own grading endpoint with the TimeBack Platform. Once registered, content items can bind to it by id, and the platform proxies grade-time calls to your endpoint — you never receive raw traffic from the browser, and the platform is the only party that ever talks to your grader.
This page covers the registration and management surface for external graders. The grade-time proxy that actually invokes them, and the item-to-grader binding that decides when they run, ship in follow-up tickets — those sections are marked below.
Quick Start
- Authenticate — obtain an access token for your M2M client via the OAuth client credentials flow. Your client needs the
content.writescope to register / update / revoke, andcontent.readto list / get. - Stand up your grader endpoint — an HTTPS endpoint that accepts the platform-to-grader payload (see Grader-side contract below) and returns a JSON body with
scoreGiven. - Register the grader —
POST /content/1.0/external-graderswith your URL,authenticationType, and (if non-none)authenticationCredential. - Store the grader's
id— content items reference the grader by this id when the binding endpoint ships.
Prerequisites
- An OAuth M2M client with the required content scopes (see Authentication).
- An HTTPS endpoint reachable from the public internet (HTTP is rejected; raw IP addresses,
localhost, private ranges,.local/.internal/.example/.test/.invalidsuffixes, and cloud-metadata hosts are rejected — use a domain name).
Concepts
External grader
An external grader is a first-class, id-addressable entity stored on the platform. It carries:
| Field | Type | Description |
|---|---|---|
id |
UUID | Platform-assigned. Content items bind to this. Stable across URL / credential rotations. |
url |
string | HTTPS endpoint of your grader. Globally unique across the platform — no two graders may share a URL, even across owners. |
authenticationType |
enum | none or api_key. Controls the auth header the platform sends on every outbound call. See Authenticating to your grader. |
authenticationCredential |
string | The API key value. Write-only — accepted on register/update, never returned in any response. Stored encrypted at rest via AWS KMS. null when authenticationType is none. |
status |
enum | active — usable. revoked — soft-deleted; the id stays resolvable so items still bound to it don't dangle. |
dateCreated |
ISO 8601 | When the grader was registered. |
dateLastModified |
ISO 8601 | When the grader was last updated. |
Ownership is tied to the caller's auth_client_id — the M2M client that registered the grader is the only party that can list, get, update, or revoke it.
Live probe
Every registration and every URL / auth change triggers a live probe: the platform sends a canary payload to your grader with the exact envelope shape a real grade call would use, then requires a JSON response with a coercible scoreGiven. If the probe fails, the change is rejected — the grader never enters the active state until it has proven it can produce a usable result.
Probe behavior:
- Timeout: 30 seconds. A slower response is rejected.
- Redirects: disabled. A
3xxresponse is treated as an error (Grader returned HTTP 30x). - Retries: none. A failing grader is rejected outright — you fix and re-register.
- SSRF gate: the URL is validated against the block list before any request is sent.
Probes run on:
- Registration (
POST). - Any
PUTwhose resultingstatusisactive— the platform re-verifies the grader before returning success.
Probes skip on:
PUTwhose resultingstatusisrevoked— the platform is decommissioning it and doesn't need it to be reachable.
Authenticating to Your Grader
The platform sends one of two authentication headers on every probe and grade call, decided by the grader's authenticationType:
authenticationType |
Header sent | When to use |
|---|---|---|
none |
(no auth header) | Your endpoint is protected by network ACLs, mTLS, or is genuinely open. authenticationCredential must be null. |
api_key |
X-Api-Key: <credential> |
A shared key your grader knows. |
The credential you supply is encrypted with a platform-owned KMS key before it ever hits storage. The plaintext is never persisted, never logged, and never returned by any API. To rotate, PUT the grader with a new credential value; the previous one is overwritten.
Registering a Grader
POST /content/1.0/external-graders
Required OAuth scope: https://timeback-platform.trilogy.com/content/scope/content.write
Request body:
{
"url": "https://grader.your-service.com/grade",
"authenticationType": "api_key",
"authenticationCredential": "sk-live-..."
}
Response (201):
{
"id": "b6c8e0e0-6f0d-4a3b-9c1e-4b2f0f1a1a2a",
"url": "https://grader.your-service.com/grade",
"authenticationType": "api_key",
"status": "active",
"dateCreated": "2026-07-14T20:14:31.542Z",
"dateLastModified": "2026-07-14T20:14:31.542Z"
}
Validation & flow:
- Body is Zod-parsed. If
authenticationTypeisnone,authenticationCredentialmust benull. Ifapi_key,authenticationCredentialmust be a non-empty string. Mismatch →400. - URL is validated (HTTPS, public domain — see Prerequisites). Failure →
400. - A pre-flight lookup checks whether the URL is already registered anywhere. If so →
409. - The platform sends a probe to your grader with the configured auth header. Failure →
400with a specific reason (Grader did not respond within 30 seconds,Grader returned HTTP 500,Grader unreachable (ENOTFOUND),Grader response missing required field "scoreGiven", etc.). - The credential is KMS-encrypted, the row is inserted, and the response DTO is returned. The credential is not in the response.
Managing Graders
List
GET /content/1.0/external-graders
Required scope: content.read. Returns only the caller's own graders.
Response (200):
{
"graders": [
{
"id": "b6c8e0e0-6f0d-4a3b-9c1e-4b2f0f1a1a2a",
"url": "https://grader.your-service.com/grade",
"authenticationType": "api_key",
"status": "active",
"dateCreated": "2026-07-14T20:14:31.542Z",
"dateLastModified": "2026-07-14T20:14:31.542Z"
}
]
}
Get
GET /content/1.0/external-graders/{graderId}
Required scope: content.read. Returns 404 if the grader belongs to a different auth_client — existence is not leaked across owners.
Update (full replace)
PUT /content/1.0/external-graders/{graderId}
Required scope: content.write. Full-replace semantics — every field must be supplied on every call, including the credential.
Request body:
{
"url": "https://grader.your-service.com/grade",
"status": "active",
"authenticationType": "api_key",
"authenticationCredential": "sk-live-...new..."
}
Because the credential must be resupplied on every PUT, the platform always has plaintext available to re-probe the grader — no decryption of the stored value is needed. If the request's resulting status is active, the platform runs a fresh probe with the incoming URL + auth before persisting. If the URL differs from the stored value, a duplicate-URL check runs first; a collision → 409.
Rotation flows:
- Rotate the API key — same URL, same type, new credential. The re-probe verifies the new credential works.
- Switch auth type — flip
authenticationType(e.g.api_key→none); the new credential must match the new type per the schema rules. - Drop auth entirely — set
authenticationTypetononeandauthenticationCredentialtonull. The stored ciphertext is nulled. - Change the URL — supply the new URL; the probe hits the new address.
Revoke (soft-delete)
DELETE /content/1.0/external-graders/{graderId}
Required scope: content.write. Flips status to revoked and returns 204. The id stays resolvable so items already bound to it don't lose their reference — bindings on revoked graders will be surfaced at grade time when the proxy ships. Delete is idempotent; a second DELETE on an already-revoked grader is a no-op.
There is no hard-delete surface. If you want a revoked grader out of your list view, work with platform support.
Grader-Side Contract
Your grader implements a single HTTPS POST endpoint. The platform sends this envelope on both probes and real grade calls:
{
"itemIdentifier": "timeback-platform-canary",
"rawXml": "<qti-assessment-item …>…</qti-assessment-item>",
"resolvedItemXml": "<optional post-templating XML>",
"responses": { "RESPONSE": "the student's answer text" },
"context": {
"questionText": "The stimulus / prompt text extracted from the item.",
"rubric": "Rubric text (or null).",
"answerInterpretation": "The @interpretation from the QTI response declaration, or null.",
"maxScore": 1,
"modelAnswer": null
}
}
Your grader responds with:
{
"scoreGiven": 0.75,
"comment": "Optional feedback string."
}
Rules the platform enforces on the response:
- Content-Type must be JSON.
scoreGivenmust be a finite, non-negative number. Missing,null, non-numeric, or negative → the probe fails (or, at real grade time, the platform fails closed).commentis optional. It flows through to the caller as the grade result's feedback field.
Note on questionText / rubric / answerInterpretation / maxScore / modelAnswer: these are pre-extracted from the item's QTI by the platform, so your grader doesn't have to parse rawXml itself. The extractor ships with the grade-time proxy in the follow-up ticket — until then, your grader is only ever hit with the canary payload during registration, where those fields are hard-coded platform-side.
Security Posture
- HTTPS-only, with a public domain. All internal, link-local, loopback, cloud-metadata, and reserved-TLD hosts are rejected at registration and on every URL change. Numeric-encoded loopback (
https://2130706433/…) and hex-encoded (https://0x7f000001/…) are blocked as well. - No redirects.
axiosis configured withmaxRedirects: 0; a3xxresponse is treated as an error to prevent a public URL from redirecting the platform to an internal target. - Credentials are write-only.
authenticationCredentialis accepted inPOST/PUTbodies and encrypted with AWS KMS before storage. It is never included in any list / get / update / delete response, never logged, and cannot be read back through the API — even by the owner. - Ownership isolation. Every grader is owned by the
auth_client_idthat registered it. Cross-owner reads or writes return404, not403— existence is not leaked. - Global URL uniqueness. A URL registered by one client cannot be registered by another. If you need to move a grader between clients, contact platform support.
Errors
| Status | When |
|---|---|
400 |
Body validation failure (URL, auth combination, scoring response coercion). The error message names the specific failure reason. |
401 |
Missing or invalid token, or no client identity on the request. |
404 |
Grader id does not exist under the caller's auth_client_id. |
409 |
The URL is already registered on another grader (or on the same grader by a concurrent write). |
500 |
Unexpected platform error. Retry with the same body; if it recurs, contact platform support. |
Coming Later
- Item-to-grader binding — the endpoint that ties a content item's
sourcedIdto a registered grader'sid. Ships in a follow-up ticket. - Grade-time proxy — the runtime that actually invokes your grader when a student attempt reaches
POST /content/1.0/grade, resolves the item's binding, forwards the payload, applies the injection safety net, coerces the response, and returns a normalized grade to the caller. - Platform-managed grading (Tier 1) — for consumers who don't want to host their own grader, the platform will offer author-written prompts run against a platform-owned model + schema. Registered external graders remain the "bring-your-own" tier.
Each of these will get its own section (or dedicated page) as they ship. For now, this page reflects only what's live: registration and management of external graders.
