OutFigure

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.

API endpoints with required scopes
MethodPathScopePurpose
GET/health/liveNoneProcess liveness
POST/v1/eventsevents:writeSend one learner event
POST/v1/events/batchevents:writeSend up to 25 events atomically
GET/v1/jobs/{job_id}events:writeProcessing job status
GET/v1/learners/{learner_id}/statestate:readConcept summaries for a learner
GET/v1/learners/{learner_id}/concepts/{concept_id}state:readOne learner-concept state
GET/v1/learners/{learner_id}/evidenceevidence:readObservations behind the state
POST/v1/learners/{learner_id}/check-planschecks:writeIssue a shadow-mode check plan
GET/v1/check-plans/{plan_id}state:readA check plan with its follow-up

Download the OpenAPI document

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.

POST /v1/events (synthetic example)
{
  "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_id with 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.
Event fields, types and meaning
FieldTypeRequiredMeaning
schema_version1.0Always
event_idstringAlwaysUnique per project. Resending the same event with identical content is acknowledged as a duplicate; different content returns 409.
event_typeresponse | exposure | hint | feedback | interventionAlwaysresponse: 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_idstringAlwaysPseudonymous learner identifier, unique within the project.
occurred_atstringAlwaysISO 8601 instant with an explicit offset (Z or ±hh:mm), at most millisecond precision. Stored and returned in UTC.
sourcestringAlwaysSystem that observed the event.
course_idstringNoOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
session_idstringNoLearning session identifier. Without it, eligibility falls back to attempt grouping and sessions cannot be counted.
item_idstringResponsesOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
item_versionstringResponsesVersion of the item content the learner saw. A content edit requires a new version.
attempt_idstringResponsesOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
attempt_numberinteger 1–10000Responses
response_kindbinary | partial_credit | unscoredResponsesbinary requires correct. partial_credit requires score in [0,1] and rubric_version. unscored carries neither.
correctbooleanBinary responses
scorenumber 0–1Partial-credit responses
rubric_versionstringPartial-credit responses
latency_msinteger 0–3600000NoTime measured by the source. Not a proxy for ability.
hint_countinteger 0–100NoMissing means unknown; zero means explicitly none.
assistancenone | hints | external | unknownNoHelp available or used for this attempt. Omit or send unknown when not known; unknown assistance is never assumed to be unassisted.
confidencenumber 0–1NoOptional learner self-report in [0,1]. Never inferred from timing.
selected_option_idstringNoOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
feedback_seenbooleanNoWhether feedback was visible for the measured attempt.
intervention_idstringInterventionsOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
metadataobjectNoAllowlisted keys only, at most 2 KiB. Free text, answers, names and hidden identifiers are not accepted.

Reading state honestly

GET /v1/learners/{learner_id}/concepts/{concept_id} (abridged)
{
  "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.

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.

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.

HTTP statuses and error codes
StatusCodes
400malformed_json, idempotency_key_required, invalid_query, invalid_cursor, invalid_learner_id, invalid_concept_id
401invalid_credentials
403insufficient_scope, project_suspended
404not_found (also for anything that belongs to another project)
410learner_deleted
409idempotency_key_reused, event_id_conflict, item_version_conflict
413payload_too_large, batch_too_large
415unsupported_media_type
422validation_failed, unsupported_schema_version, duplicate_event_id_in_batch
503commit_outcome_unknown, temporarily_unavailable (retry with the same key)