Developers
Partner API, version 1
Send events from your backend; read state and the evidence behind it. This reference is generated from the API contract (OpenAPI 1.0.0-m1). Local preview Hosted sandbox access is in preparation.
§ 1 Routes
Endpoints and scopes
Authenticate every /v1 request from your server with a project API key:
Authorization: Bearer lsk_test_…. A key belongs to exactly one project; nothing
in a request can choose another. Never ship a key to a browser or app.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | / | None | Process liveness |
| POST | / | events:write | Send one learner event |
| POST | / | events:write | Send up to 25 events atomically |
| GET | / | events:write | Processing job status |
| GET | / | state:read | Concept summaries for a learner |
| GET | / | state:read | One learner-concept state |
| GET | / | evidence:read | Observations behind the state |
| POST | / | checks:write | Issue a shadow-mode check plan |
| GET | / | state:read | A check plan with its follow-up |
§ 2 Events
The event contract
One observed learning event. Required for all types: schema_version, event_id, event_type, learner_id, occurred_at, source. Responses additionally require item_id, item_version, attempt_id, attempt_number, response_kind and the outcome that matches the kind. Unknown fields, and fields that try to name a project or tenant, are rejected rather than ignored.
{
"schema_version": "1.0",
"event_id": "evt_0001",
"event_type": "response",
"learner_id": "learner_demo_01",
"occurred_at": "2026-09-14T12:40:31Z",
"source": "partner_backend",
"session_id": "session_01",
"item_id": "question_012",
"item_version": "1",
"attempt_id": "attempt_01",
"attempt_number": 1,
"response_kind": "binary",
"correct": true,
"assistance": "none",
"hint_count": 0,
"feedback_seen": false
}
Limits and delivery
- At most 25 events and 64 KiB per request; larger requests are rejected before anything is stored.
- Batches are atomic: one invalid event rejects the whole batch, with every failing index listed.
-
Every write needs an
Idempotency-Key. Retrying with the same key and body returns the original receipt; request records are kept at least 7 days. -
An existing
event_idwith identical content is acknowledged as a duplicate and never counted twice. Different content returns 409. - Times need an offset. Events more than 5 minutes in the future, or older than 90 days, are rejected on this route.
- A 202 means stored, not yet reflected in state. Poll the job it returns.
| Field | Type | Required | Meaning |
|---|---|---|---|
schema_version | 1.0 | Always | |
event_id | string | Always | Unique per project. Resending the same event with identical content is acknowledged as a duplicate; different content returns 409. |
event_type | response | exposure | hint | feedback | intervention | Always | response: the learner answered an item. exposure: content was shown (not evidence of mastery). hint/feedback: assistance delivered. intervention: an action actually delivered, not merely recommended. |
learner_id | string | Always | Pseudonymous learner identifier, unique within the project. |
occurred_at | string | Always | ISO 8601 instant with an explicit offset (Z or ±hh:mm), at most millisecond precision. Stored and returned in UTC. |
source | string | Always | System that observed the event. |
course_id | string | No | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
session_id | string | No | Learning session identifier. Without it, eligibility falls back to attempt grouping and sessions cannot be counted. |
item_id | string | Responses | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
item_version | string | Responses | Version of the item content the learner saw. A content edit requires a new version. |
attempt_id | string | Responses | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
attempt_number | integer 1–10000 | Responses | |
response_kind | binary | partial_credit | unscored | Responses | binary requires correct. partial_credit requires score in [0,1] and rubric_version. unscored carries neither. |
correct | boolean | Binary responses | |
score | number 0–1 | Partial-credit responses | |
rubric_version | string | Partial-credit responses | |
latency_ms | integer 0–3600000 | No | Time measured by the source. Not a proxy for ability. |
hint_count | integer 0–100 | No | Missing means unknown; zero means explicitly none. |
assistance | none | hints | external | unknown | No | Help available or used for this attempt. Omit or send unknown when not known; unknown assistance is never assumed to be unassisted. |
confidence | number 0–1 | No | Optional learner self-report in [0,1]. Never inferred from timing. |
selected_option_id | string | No | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
feedback_seen | boolean | No | Whether feedback was visible for the measured attempt. |
intervention_id | string | Interventions | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
metadata | object | No | Allowlisted keys only, at most 2 KiB. Free text, answers, names and hidden identifiers are not accepted. |
§ 3 State
Reading state honestly
{
"learner_id": "learner_demo_01",
"concept_id": "networking.dns",
"status": "insufficient_evidence",
"summary": {
"window": "latest_200_eligible_events",
"eligible_attempts": 1,
"correct_attempts": 1,
"observed_accuracy": 1,
"distinct_sessions": 1
},
"forecast": null,
"uncertainty": { "kind": "not_estimated", "interval": null, "calibration_status": "unvalidated" },
"flags": ["sparse_history", "insufficient_sessions"],
"model_version": "observed-v0",
"eligibility_version": "eligibility-v1"
}
Statuses
insufficient_evidence- Fewer than 5 counted responses, or fewer than 2 sessions.
stale_evidence- The latest counted response is more than 30 days old. Nothing is decayed.
observed_only- Enough recent observations to summarize. Still no forecast.
forecast is always null in
observed-v0. observed_accuracy describes past
observations; it is not a probability.
Why a response does not count (eligibility-v1)
partial_credit- Scored on a rubric, not right or wrong.
unscored- No outcome was recorded.
assistance_unknown- Assistance was not reported.
assisted- Hints or outside help were used.
feedback_seen- Feedback was visible while answering.
hint_before_response- A hint was shown before the answer.
feedback_before_response- Feedback was shown before the answer.
repeat_in_session- The item was already answered in this session.
repeat_in_attempt- The item was already answered in this attempt.
§ 4 Checks
Check plans, in shadow mode
A check plan lists where one independent response (explicitly unassisted, scored, first in
its session) would most improve the evidence about a learner, with the reason and the
evidence behind each check. The rule is next-check-v0: documented, versioned
and Unvalidated. Plans are shadow mode: nothing is delivered to a
learner, and your platform decides whether to show anything.
Reasons, in the order they are ranked
no_independent_evidence- Responses exist on the concept, but none counted as independent.
assisted_since_independent_evidence- Assisted or unknown-assistance work happened after the latest independent response.
insufficient_independent_evidence- Fewer than 5 counted responses, or fewer than 2 sessions.
persistence_check_due- The latest independent response is older than the declared delay (default 7 days).
stale_independent_evidence- The latest independent response is more than 30 days old.
Follow-up
- A plan never changes after it is issued. Follow-up is derived when you read it.
-
observed: an independent response on the concept arrived inside the window, with its delay and whether the item was new to the learner. -
not_observed: the window closed without one. That is missing follow-up, neither failure nor retention. - Assisted, unknown-assistance and pre-issue responses never count, but they are reported as learner time spent.
- A correct answer on an item the learner had seen before may be a remembered answer, so repeats are labeled.
§ 5 Errors
Errors and retries
Errors share one shape:
{"error": {"code", "message", "request_id", "details"}}. Messages never repeat
submitted values. Retry network failures and 429, 502, 503 and 504 with the same idempotency
key; do not retry other 4xx errors unchanged.
| Status | Codes |
|---|---|
| 400 | malformed_json, idempotency_key_required, invalid_query, invalid_cursor, invalid_learner_id, invalid_concept_id |
| 401 | invalid_credentials |
| 403 | insufficient_scope, project_suspended |
| 404 | not_found (also for anything that belongs to another project) |
| 410 | learner_deleted |
| 409 | idempotency_key_reused, event_id_conflict, item_version_conflict |
| 413 | payload_too_large, batch_too_large |
| 415 | unsupported_media_type |
| 422 | validation_failed, unsupported_schema_version, duplicate_event_id_in_batch |
| 503 | commit_outcome_unknown, temporarily_unavailable (retry with the same key) |