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.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | / | None | Process liveness |
| POST | / | events:write | Send one learner event |
| POST | / | events:write | Send up to 25 events atomically |
| POST | / | events:write | Correct a stored event |
| POST | / | experiments:write | Record experiment assignments |
| GET | / | evidence:read | A learner's experiment assignments |
| POST | / | marking:write | Start a marking session |
| GET | / | marking:read | List marking sessions |
| GET | / | marking:read | Read a marking session's report |
| POST | / | marking:write | Add a mark scheme, or a new version of one |
| GET | / | marking:read | List mark schemes |
| GET | / | marking:read | Read a mark scheme's latest version |
| GET | / | marking:read | Read one version of a mark scheme |
| POST | / | marking:write | Mark a session's question against a mark scheme version |
| POST | / | marking:write | Send scripts with a second reading |
| POST | / | marking:write | Record a senior marker's decision |
| GET | / | marking:read | Read a marking session's record |
| GET | / | marking:read | Read one marker's page |
| GET | / | marking:read | Read feedback for one marker |
| GET | / | data:export | Download the final marks |
| GET | / | evaluations:read | How one experiment was assigned, delivered and followed |
| GET | / | evaluations:read | Evaluation runs over the predictions this project issued |
| GET | / | evaluations:read | One evaluation run, with its manifest |
| GET | / | evaluations:read | The record set out as a document to share |
| GET | / | events:write | Processing job status |
| GET | / | state:read | Concept summaries for a learner |
| GET | / | state:read | One learner-concept state |
| GET | / | state:read | How a learner-concept state changed |
| GET | / | evidence:read | Observations behind the state |
| POST | / | catalog:write | Register a course |
| POST | / | catalog:write | Register a concept |
| POST | / | catalog:write | Register an item version |
| POST | / | catalog:write | Approve a concept mapping |
| POST | / | checks:write | Issue a shadow-mode check plan |
| GET | / | suggestions:read | Suggest the next step (shadow mode) |
| GET | / | state:read | A check plan with its follow-up |
| POST | / | predictions:write | Issue a prospective prediction |
| POST | / | outcomes:write | Link an observed response to a prediction |
| POST | / | data:export | Export a learner's records |
| DELETE | / | data:delete | Erase a learner |
| GET | / | data:export or data:delete | An export or erasure request |
| GET | / | data:export | Download an export |
| POST | / | events:write | Record an intervention |
| POST | / | events:write | Record several interventions at once |
| POST | / | events:write | Record evidence |
| POST | / | events:write | Record several pieces of evidence at once |
| POST | / | events:write | Record a state observation |
| POST | / | events:write | Record several state observations at once |
| POST | / | outcomes:write | Open an Outcome Contract |
| GET | / | evaluations:read | An Outcome Contract |
| GET | / | evaluations:read | Outcome Receipts |
| GET | / | evaluations:read | One Outcome Receipt version |
| POST | / | experiments:write | Declare an experiment |
| GET | / | evaluations:read | An experiment |
| POST | / | experiments:write | Start an experiment |
| POST | / | experiments:write | Stop an experiment |
| POST | / | experiments:write | Assign a learner |
| GET | / | evaluations:read | Experiment results |
| GET | / | evaluations:read | Release check |
| GET | / | evaluations:read | Experiment evidence report |
| GET | / | evaluations:read | A project's outcome summary |
| GET | / | evaluations:read | Outcomes by cohort |
| GET | / | evaluations:read | A project's capabilities |
| GET | / | evaluations:read | Data-quality report |
| POST | / | webhooks:write | Register a webhook endpoint |
| GET | / | webhooks:write | List webhook endpoints |
| POST | / | webhooks:write | Rotate an endpoint secret |
| POST | / | webhooks:write | Disable an endpoint |
| GET | / | webhooks:write | List webhook deliveries |
| POST | / | webhooks:write | Replay a delivery |
| POST | / | events:write | Import a CSV of interventions or evidence |
| GET | / | events:write | List imports |
| GET | / | events:write | Read an import |
| GET | / | events:write | Download 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.
npm install outfigure
pip install outfigure
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.
{
"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: onlyunassistedis independent evidence, andunknownis 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_typeandscorer.type: engagement, behavior, model estimates and model-scored answers are proxies, which never go past MD1.
| Field | Type | Required | Meaning |
|---|---|---|---|
schema_version | 1.0 | Always | |
intervention_id | string | Always | Unique per project. Resending identical content is a duplicate; different content under the same ID returns 409. |
learner_id | string | Always | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
capability | string | Always | The capability the intervention was meant to change, as your catalog names it. The same identifier a concept is registered under. |
class | socratic_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 | other | Always | The 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_at | string | Always | When the learner received it. Only delivered interventions are recorded; a proposal that never reached the learner is not an intervention. |
source | string | Always | |
session_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. |
agent_id | string | No | Which of your agents acted. |
model_provider | string | No | Optional. Some teams do not disclose their model stack, and need not. |
model_name | string | No | |
prompt_policy_version | string | No | Opaque, immutable ID of the prompt or policy. Never the prompt text. |
content_hash | string | No | Hash of the delivered content, sha256: and 64 lowercase hex digits. Lets two deliveries be compared without the content being sent. |
content_ref | string | No | Where the content lives in your system. OutFigure stores the reference, never the content. |
turn_count | integer 0–10000 | No | |
duration_ms | integer 0–86400000 | No | |
assistance_level | string | No | How much the intervention did for the learner, in your own vocabulary. |
strategy_tags | array | No | |
experiment | object | No | The experiment and arm this delivery belonged to. This is the exposure record: an assignment alone is never read as exposure. |
contract | object | No | Open an Outcome Contract for this intervention with a predefined evidence plan, in the same request. |
| Field | Type | Required | Meaning |
|---|---|---|---|
schema_version | 1.0 | Always | |
evidence_id | string | Always | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
learner_id | string | Always | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
capability | string | Always | Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address. |
observed_at | string | Always | ISO 8601 instant with an explicit offset (Z or ±hh:mm), at most millisecond precision. Stored and returned in UTC. |
evidence_type | assessment_item | constructed_response | skill_demonstration | simulation | external_assessment | state_model | behavioral | engagement | model_judged | Always | What 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. |
score | number | Always | |
scale | array | Always | The score range as [minimum, maximum]. |
assistance | unassisted | hinted | solution_revealed | collaborative | external | unknown | Always | Help the learner had on this observation. Only unassisted is independent evidence. unknown is never read as unassisted. |
is_unseen_item | boolean | No | True when the learner had never seen this item. Missing means unknown, which cannot support a transfer claim. |
is_transfer | boolean | No | True when the task applies the capability in a new form or context. Missing means unknown. |
item_ref | 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. |
scorer | object | Always | The scoring method and its version. |
reliability | object | No | |
session_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. |
source | string | Always |
Reading a receipt
{
"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-Signatureist=<unix seconds>,v1=<hex>: HMAC-SHA256 of<t>.<raw body>under your endpoint's secret, onev1per valid secret while a rotation is under way. Refuse a timestamp more than 5 minutes from your clock. -
OutFigure-Webhook-Idstays 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.
{
"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 | |
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 | |
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. |
answer_revealed | boolean | No | Whether the correct answer was shown to the learner before or during this attempt. Missing means unknown and is never read as false. |
collaborated | boolean | No | Whether the learner worked on this attempt with another person. Missing means unknown and is never read as false. |
assistance_source | platform_telemetry | learner_report | proctored | unknown | No | |
construct_type | practice | formative_check | immediate_post_test | delayed_check | near_transfer | far_transfer | external_assessment | No | |
assessment_id | string | No | The assessment this response belongs to, when it is part of one. |
assessment_version | string | No | Version of that assessment. Required with assessment_id, and only with it. |
intended_delay_seconds | integer 0–7776000 | No | The delay the product planned between teaching and this measurement. Missing means unknown, never zero. |
independent_of_teaching | boolean | No | Whether 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_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. |
provenance | object | No | Only on intervention events: what was delivered and by which system and release (PROV-01). |
metadata | object | No |
Reading a state
{
"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.
| 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) |
| 409 | idempotency_key_reused, event_id_conflict, item_version_conflict, mapping_revision_stale, correction_conflict, outcome_already_recorded |
| 410 | learner_deleted, export_unavailable |
| 413 | payload_too_large, batch_too_large, export_too_large |
| 415 | unsupported_media_type |
| 422 | validation_failed, unsupported_schema_version, duplicate_event_id_in_batch, correction_mismatch, insufficient_evidence, outcome_does_not_match |
| 429 | rate_limited, quota_exceeded (retry after the Retry-After interval with the same key) |
| 503 | commit_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) |
