OutFigure Get in touch

Partner API, version 1

Send what your tutor did and what the learner did next, read the receipts that follow, run experiments, and get a signed notice when a receipt changes. This page is generated from the API’s own machine-readable description (version 1.0.0-m1), so it can’t fall out of step with the service.

Endpoints and scopes

Authenticate every /v1 request from your server with a project API key: Authorization: Bearer lsk_live_…. 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
POST/v1/events/correctionsevents:writeCorrect a stored event
POST/v1/experiments/assignmentsexperiments:writeRecord experiment assignments
GET/v1/learners/{learner_id}/assignmentsevidence:readA learner's experiment assignments
POST/v1/marking-sessionsmarking:writeStart a marking session
GET/v1/marking-sessionsmarking:readList marking sessions
GET/v1/marking-sessions/{session_id}marking:readRead a marking session's report
POST/v1/mark-schemesmarking:writeAdd a mark scheme, or a new version of one
GET/v1/mark-schemesmarking:readList mark schemes
GET/v1/mark-schemes/{scheme_id}marking:readRead a mark scheme's latest version
GET/v1/mark-schemes/{scheme_id}/versions/{version}marking:readRead one version of a mark scheme
POST/v1/marking-sessions/{session_id}/questions/{question_id}/mark-schememarking:writeMark a session's question against a mark scheme version
POST/v1/marking-sessions/{session_id}/scriptsmarking:writeSend scripts with a second reading
POST/v1/marking-sessions/{session_id}/decisionsmarking:writeRecord a senior marker's decision
GET/v1/marking-sessions/{session_id}/recordmarking:readRead a marking session's record
GET/v1/marking-sessions/{session_id}/markers/{marker_id}marking:readRead one marker's page
GET/v1/marking-sessions/{session_id}/markers/{marker_id}/feedbackmarking:readRead feedback for one marker
GET/v1/marking-sessions/{session_id}/final-marks.csvdata:exportDownload the final marks
GET/v1/experiments/{experiment_id}/recordevaluations:readHow one experiment was assigned, delivered and followed
GET/v1/evaluationsevaluations:readEvaluation runs over the predictions this project issued
GET/v1/evaluations/{evaluation_id}evaluations:readOne evaluation run, with its manifest
GET/v1/experiments/{experiment_id}/record/reportevaluations:readThe record set out as a document to share
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}/concepts/{concept_id}/revisionsstate:readHow a learner-concept state changed
GET/v1/learners/{learner_id}/evidenceevidence:readObservations behind the state
POST/v1/catalog/coursescatalog:writeRegister a course
POST/v1/catalog/conceptscatalog:writeRegister a concept
POST/v1/catalog/itemscatalog:writeRegister an item version
POST/v1/catalog/mappingscatalog:writeApprove a concept mapping
POST/v1/learners/{learner_id}/check-planschecks:writeIssue a shadow-mode check plan
GET/v1/learners/{learner_id}/next-stepsuggestions:readSuggest the next step (shadow mode)
GET/v1/check-plans/{plan_id}state:readA check plan with its follow-up
POST/v1/predictionspredictions:writeIssue a prospective prediction
POST/v1/outcomesoutcomes:writeLink an observed response to a prediction
POST/v1/learners/{learner_id}/exportsdata:exportExport a learner's records
DELETE/v1/learners/{learner_id}data:deleteErase a learner
GET/v1/data-requests/{data_request_id}data:export or data:deleteAn export or erasure request
GET/v1/data-requests/{data_request_id}/downloaddata:exportDownload an export
POST/v1/interventionsevents:writeRecord an intervention
POST/v1/interventions/batchevents:writeRecord several interventions at once
POST/v1/evidenceevents:writeRecord evidence
POST/v1/evidence/batchevents:writeRecord several pieces of evidence at once
POST/v1/statesevents:writeRecord a state observation
POST/v1/states/batchevents:writeRecord several state observations at once
POST/v1/outcome-contractsoutcomes:writeOpen an Outcome Contract
GET/v1/outcome-contracts/{contract_id}evaluations:readAn Outcome Contract
GET/v1/outcome-receiptsevaluations:readOutcome Receipts
GET/v1/outcome-receipts/{receipt_id}evaluations:readOne Outcome Receipt version
POST/v1/experimentsexperiments:writeDeclare an experiment
GET/v1/experiments/{experiment_id}evaluations:readAn experiment
POST/v1/experiments/{experiment_id}/startexperiments:writeStart an experiment
POST/v1/experiments/{experiment_id}/stopexperiments:writeStop an experiment
POST/v1/experiments/{experiment_id}/assignexperiments:writeAssign a learner
GET/v1/experiments/{experiment_id}/resultsevaluations:readExperiment results
GET/v1/experiments/{experiment_id}/release-checkevaluations:readRelease check
GET/v1/experiments/{experiment_id}/reportevaluations:readExperiment evidence report
GET/v1/outcome-summaryevaluations:readA project's outcome summary
GET/v1/cohortsevaluations:readOutcomes by cohort
GET/v1/capabilitiesevaluations:readA project's capabilities
GET/v1/data-qualityevaluations:readData-quality report
POST/v1/webhook-endpointswebhooks:writeRegister a webhook endpoint
GET/v1/webhook-endpointswebhooks:writeList webhook endpoints
POST/v1/webhook-endpoints/{endpoint_id}/rotate-secretwebhooks:writeRotate an endpoint secret
POST/v1/webhook-endpoints/{endpoint_id}/disablewebhooks:writeDisable an endpoint
GET/v1/webhook-deliverieswebhooks:writeList webhook deliveries
POST/v1/webhook-deliveries/{delivery_id}/replaywebhooks:writeReplay a delivery
POST/v1/importsevents:writeImport a CSV of interventions or evidence
GET/v1/importsevents:writeList imports
GET/v1/imports/{import_id}events:writeRead an import
GET/v1/imports/{import_id}/errorsevents:writeDownload the problems in an import

Download the API description: one machine-readable file listing every route, field and error, in the standard format that API tools such as Swagger UI and Postman import. It describes HTTP endpoints only; this service calls no model provider.

Clients for TypeScript and Python

Both clients are published as outfigure under the MIT licence. They use the API’s own field names, retry network failures and 429, 502, 503 and 504 with the same idempotency key, and send nothing but your requests.

The npm package also brings the tools that audit an export of your logs on your own computer, npx outfigure outcome-audit and npx outfigure readiness. They read and write local files and open no network connection.

Install
npm install outfigure
pip install outfigure

outfigure on npm · outfigure on PyPI

Interventions and evidence

Record each intervention when it is delivered, with contract.template to open an Outcome Contract, and each later check as evidence. Both are append-only: the same record sent again is stored once, and different content under the same ID returns 409. Batches of up to 100 are atomic, and every write needs an Idempotency-Key. Receipts are issued by the job drain, never inside the request.

One intervention, from the demo
{
  "schema_version": "1.0",
  "intervention_id": "dt_int_88",
  "learner_id": "dt_learner_0089",
  "capability": "geometry.area_rectangles",
  "class": "contrast_case",
  "delivered_at": "2026-09-02T08:32:00.000Z",
  "source": "demotutor",
  "session_id": "dt_session_88",
  "agent_id": "demotutor_math",
  "model_provider": "synthetic",
  "model_name": "demotutor-model",
  "prompt_policy_version": "demotutor-2026.09.1",
  "turn_count": 6,
  "contract": {
    "template": "durable_capability_gain"
  }
}

What makes evidence count

  • assistance: only unassisted is independent evidence, and unknown is never read as unassisted.
  • is_unseen_item: a delayed result counts toward transfer only on an item the learner had never seen. Missing means unknown.
  • evidence_type and scorer.type: engagement, behavior, model estimates and model-scored answers are proxies, which never go past MD1.
Intervention fields: fields, types and meaning
FieldTypeRequiredMeaning
schema_version1.0Always
intervention_idstringAlwaysUnique per project. Resending identical content is a duplicate; different content under the same ID returns 409.
learner_idstringAlwaysOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
capabilitystringAlwaysThe capability the intervention was meant to change, as your catalog names it. The same identifier a concept is registered under.
classsocratic_question | hint | worked_example | contrast_case | error_explanation | concept_explanation | retrieval_prompt | practice_item_selection | difficulty_adjustment | prerequisite_review | metacognitive_prompt | feedback_corrective | feedback_elaborative | solution_reveal | content_recommendation | human_escalation | otherAlwaysThe teaching move, from a stable taxonomy. Your own finer labels go in strategy_tags. other when none fits; it is compared only with itself.
delivered_atstringAlwaysWhen the learner received it. Only delivered interventions are recorded; a proposal that never reached the learner is not an intervention.
sourcestringAlways
session_idstringNoOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
agent_idstringNoWhich of your agents acted.
model_providerstringNoOptional. Some teams do not disclose their model stack, and need not.
model_namestringNo
prompt_policy_versionstringNoOpaque, immutable ID of the prompt or policy. Never the prompt text.
content_hashstringNoHash of the delivered content, sha256: and 64 lowercase hex digits. Lets two deliveries be compared without the content being sent.
content_refstringNoWhere the content lives in your system. OutFigure stores the reference, never the content.
turn_countinteger 0–10000No
duration_msinteger 0–86400000No
assistance_levelstringNoHow much the intervention did for the learner, in your own vocabulary.
strategy_tagsarrayNo
experimentobjectNoThe experiment and arm this delivery belonged to. This is the exposure record: an assignment alone is never read as exposure.
contractobjectNoOpen an Outcome Contract for this intervention with a predefined evidence plan, in the same request.
Evidence fields: fields, types and meaning
FieldTypeRequiredMeaning
schema_version1.0Always
evidence_idstringAlwaysOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
learner_idstringAlwaysOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
capabilitystringAlwaysOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
observed_atstringAlwaysISO 8601 instant with an explicit offset (Z or ±hh:mm), at most millisecond precision. Stored and returned in UTC.
evidence_typeassessment_item | constructed_response | skill_demonstration | simulation | external_assessment | state_model | behavioral | engagement | model_judgedAlwaysWhat was observed. Direct performance: assessment_item, constructed_response, skill_demonstration, simulation, external_assessment. Proxy, which can never show more than MD1: state_model (an inference, not an observation), behavioral, engagement, model_judged.
scorenumberAlways
scalearrayAlwaysThe score range as [minimum, maximum].
assistanceunassisted | hinted | solution_revealed | collaborative | external | unknownAlwaysHelp the learner had on this observation. Only unassisted is independent evidence. unknown is never read as unassisted.
is_unseen_itembooleanNoTrue when the learner had never seen this item. Missing means unknown, which cannot support a transfer claim.
is_transferbooleanNoTrue when the task applies the capability in a new form or context. Missing means unknown.
item_refstringNoOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
scorerobjectAlwaysThe scoring method and its version.
reliabilityobjectNo
session_idstringNoOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
sourcestringAlways

Reading a receipt

One receipt, abridged
{
  "status": "evaluated",
  "measurement_depth": "MD4",
  "causal_strength": "CS1",
  "observed_outcome": {
    "direction": "positive",
    "baseline_metric": 0,
    "baseline_source": "prior_evidence",
    "baseline_n": 1,
    "post_metric": 1,
    "post_role": "delayed_transfer",
    "delta": 1
  },
  "durability": {
    "immediate": 0,
    "immediate_n": 1,
    "delayed": 1,
    "delayed_n": 1,
    "median_delay_seconds": 619200
  },
  "evidence": [
    {
      "evidence_id": "dt_ev_88_baseline",
      "role": null,
      "reason": "before_delivery",
      "offset_seconds": -172800
    },
    {
      "evidence_id": "dt_ev_88_immediate",
      "role": "immediate",
      "reason": "eligible",
      "offset_seconds": 780
    }
  ],
  "warnings": [
    "ASSESSMENT_RELIABILITY_UNKNOWN",
    "TEMPORAL_ASSOCIATION_ONLY",
    "INDIVIDUAL_CHANGE_NOT_ATTRIBUTABLE"
  ]
}

Statuses

provisional
A role has its evidence while a later window is still open.
evaluated
Every role has its evidence, or its window has closed.
insufficient_evidence
Every window closed and no role found what it needed. Missing doesn’t mean failed.

measurement_depth and causal_strength are separate axes. A receipt is never edited: a new version supersedes it and names the one it replaced.

Signed notices

Register an https endpoint for outcome.receipt.created, outcome.receipt.updated, outcome.contract.expired, experiment.data_ready, experiment.sample_ratio_mismatch, experiment.guardrail_triggered, experiment.release_advice_changed and data_quality.degraded. Each notice carries identifiers and statuses only; read the receipt with your key for the rest.

  • OutFigure-Signature is t=<unix seconds>,v1=<hex>: HMAC-SHA256 of <t>.<raw body> under your endpoint's secret, one v1 per valid secret while a rotation is under way. Refuse a timestamp more than 5 minutes from your clock.
  • OutFigure-Webhook-Id stays the same on every retry and replay: record the ones you have handled. Delivery is at least once.
  • Anything but a 2xx answer within ten seconds is retried, up to 8 attempts over about two days. Redirects are never followed.

Learner-state events

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.

One event
{
  "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 | unscoredResponses
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 | unknownNo
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.
answer_revealedbooleanNoWhether the correct answer was shown to the learner before or during this attempt. Missing means unknown and is never read as false.
collaboratedbooleanNoWhether the learner worked on this attempt with another person. Missing means unknown and is never read as false.
assistance_sourceplatform_telemetry | learner_report | proctored | unknownNo
construct_typepractice | formative_check | immediate_post_test | delayed_check | near_transfer | far_transfer | external_assessmentNo
assessment_idstringNoThe assessment this response belongs to, when it is part of one.
assessment_versionstringNoVersion of that assessment. Required with assessment_id, and only with it.
intended_delay_secondsinteger 0–7776000NoThe delay the product planned between teaching and this measurement. Missing means unknown, never zero.
independent_of_teachingbooleanNoWhether this measurement was produced independently of the teaching being evaluated: different content, or an assessment the teaching side did not choose. Missing means unknown.
intervention_idstringInterventionsOpaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.
provenanceobjectNoOnly on intervention events: what was delivered and by which system and release (PROV-01).
metadataobjectNo

Reading a state

One concept, 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-v2"
}

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-v2)

partial_credit
Scored on a rubric, not right or wrong.
unscored
No outcome was recorded.
answer_revealed
The answer was shown before the attempt.
collaborated
Worked on with someone else.
assistance_unknown
No record of whether help was used.
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

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, and its version is carried on every plan. A plan is returned to your product, which decides what to do with it; OutFigure never contacts a learner.

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)
409idempotency_key_reused, event_id_conflict, item_version_conflict, mapping_revision_stale, correction_conflict, outcome_already_recorded
410learner_deleted, export_unavailable
413payload_too_large, batch_too_large, export_too_large
415unsupported_media_type
422validation_failed, unsupported_schema_version, duplicate_event_id_in_batch, correction_mismatch, insufficient_evidence, outcome_does_not_match
429rate_limited, quota_exceeded (retry after the Retry-After interval with the same key)
503commit_outcome_unknown, temporarily_unavailable, erasure_not_recorded (retry with the same key); recovery_replay_pending, recovery_check_unavailable (while a restored database is missing an erasure)