{
  "openapi": "3.1.0",
  "info": {
    "title": "OutFigure API",
    "version": "1.0.0-m1",
    "description": "Integration API for learner events, observed learner-concept state and supporting evidence. observed-v0 summarizes observed attempts; it does not forecast or estimate mastery. All examples are synthetic."
  },
  "servers": [
    {
      "url": "http://127.0.0.1:8787",
      "description": "Local development server"
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Project-scoped partner API key: `Authorization: Bearer lsk_test_…`. Server-side use only."
      }
    },
    "schemas": {
      "IngestionReceipt": {
        "type": "object",
        "properties": {
          "receipt_id": {
            "type": "string",
            "format": "uuid",
            "example": "11111111-1111-4111-8111-111111111111"
          },
          "accepted_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Events stored for the first time by this request."
          },
          "duplicate_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Events whose event_id and content were already stored. They are not counted again."
          },
          "job_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "example": "11111111-1111-4111-8111-111111111111"
            },
            "description": "Processing jobs for the events in this request, including earlier jobs of duplicates."
          },
          "processing_status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ],
            "description": "Acknowledgement means stored, not yet reflected in state. Poll GET /v1/jobs/{id}."
          },
          "request_id": {
            "type": "string",
            "description": "Server-generated request identifier. Quote it when reporting a problem.",
            "example": "req_01j9demo0000000000000000"
          }
        },
        "required": [
          "receipt_id",
          "accepted_count",
          "duplicate_count",
          "job_ids",
          "processing_status",
          "request_id"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "event_id_conflict"
              },
              "message": {
                "type": "string",
                "example": "This event ID already exists with different content."
              },
              "request_id": {
                "type": "string",
                "description": "Server-generated request identifier. Quote it when reporting a problem.",
                "example": "req_01j9demo0000000000000000"
              },
              "details": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ErrorDetail"
                }
              },
              "retryable": {
                "type": "boolean",
                "description": "True when retrying the identical request (same Idempotency-Key) may succeed."
              }
            },
            "required": [
              "code",
              "message",
              "request_id"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "ErrorDetail": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "minimum": 0,
            "description": "Zero-based event index within a batch."
          },
          "field": {
            "type": "string",
            "description": "Field path, without the submitted value."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable reason."
          }
        },
        "required": [
          "code"
        ]
      },
      "LearningEvent": {
        "type": "object",
        "properties": {
          "schema_version": {
            "type": "string",
            "enum": [
              "1.0"
            ]
          },
          "event_id": {
            "type": "string",
            "description": "Unique per project. Resending the same event with identical content is acknowledged as a duplicate; different content returns 409.",
            "example": "evt_demo_0001"
          },
          "event_type": {
            "$ref": "#/components/schemas/EventType"
          },
          "learner_id": {
            "type": "string",
            "description": "Pseudonymous learner identifier, unique within the project.",
            "example": "learner_demo_01"
          },
          "occurred_at": {
            "type": "string",
            "description": "ISO 8601 instant with an explicit offset (`Z` or `±hh:mm`), at most millisecond precision. Stored and returned in UTC.",
            "example": "2026-09-13T09:00:00Z"
          },
          "source": {
            "type": "string",
            "maxLength": 64,
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]*$",
            "description": "System that observed the event.",
            "example": "partner_backend"
          },
          "course_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "session_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Learning session identifier. Without it, eligibility falls back to attempt grouping and sessions cannot be counted.",
            "example": "learner_demo_01"
          },
          "item_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "item_version": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]*$",
            "description": "Version of the item content the learner saw. A content edit requires a new version.",
            "example": "1"
          },
          "attempt_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "attempt_number": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10000
          },
          "response_kind": {
            "$ref": "#/components/schemas/ResponseKind"
          },
          "correct": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "rubric_version": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]*$"
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 3600000,
            "description": "Time measured by the source. Not a proxy for ability."
          },
          "hint_count": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "Missing means unknown; zero means explicitly none."
          },
          "assistance": {
            "$ref": "#/components/schemas/Assistance"
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Optional learner self-report in [0,1]. Never inferred from timing."
          },
          "selected_option_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "feedback_seen": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether feedback was visible for the measured attempt."
          },
          "intervention_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "metadata": {
            "$ref": "#/components/schemas/EventMetadata"
          }
        },
        "required": [
          "schema_version",
          "event_id",
          "event_type",
          "learner_id",
          "occurred_at",
          "source"
        ],
        "additionalProperties": false,
        "description": "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."
      },
      "EventType": {
        "type": "string",
        "enum": [
          "response",
          "exposure",
          "hint",
          "feedback",
          "intervention"
        ],
        "description": "`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."
      },
      "ResponseKind": {
        "type": [
          "string",
          "null"
        ],
        "enum": [
          "binary",
          "partial_credit",
          "unscored",
          null
        ],
        "description": "`binary` requires `correct`. `partial_credit` requires `score` in [0,1] and `rubric_version`. `unscored` carries neither."
      },
      "Assistance": {
        "type": [
          "string",
          "null"
        ],
        "enum": [
          "none",
          "hints",
          "external",
          "unknown",
          null
        ],
        "description": "Help available or used for this attempt. Omit or send `unknown` when not known; unknown assistance is never assumed to be unassisted."
      },
      "EventMetadata": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "locale": {
            "type": "string",
            "maxLength": 35,
            "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$",
            "description": "BCP 47 language tag of the content shown.",
            "example": "en"
          },
          "client_platform": {
            "type": "string",
            "enum": [
              "web",
              "ios",
              "android",
              "desktop",
              "other"
            ],
            "description": "Surface on which the learner interacted."
          },
          "app_version": {
            "type": "string",
            "maxLength": 32,
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]*$",
            "description": "Version of the integrating application."
          },
          "delivery_mode": {
            "type": "string",
            "enum": [
              "practice",
              "quiz",
              "exam_simulation",
              "review",
              "diagnostic",
              "other"
            ],
            "description": "Context in which the item was delivered."
          }
        },
        "additionalProperties": false,
        "description": "Allowlisted keys only, at most 2 KiB. Free text, answers, names and hidden identifiers are not accepted."
      },
      "JobStatus": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "example": "11111111-1111-4111-8111-111111111111"
          },
          "kind": {
            "type": "string",
            "enum": [
              "apply_event",
              "rebuild_learner_concept"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/ProcessingStatus"
          },
          "attempts": {
            "type": "integer",
            "minimum": 0
          },
          "max_attempts": {
            "type": "integer",
            "minimum": 1
          },
          "error_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Safe, stable failure code from the most recent attempt. Never contains learner data."
          },
          "retryable": {
            "type": "boolean",
            "description": "True while the job will be retried automatically. False once completed or failed."
          },
          "event_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "learner_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "concept_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "outcome": {
            "type": "string",
            "enum": [
              "state_updated",
              "no_concept_mapping",
              "not_yet_run"
            ],
            "description": "What the job did when it completed."
          },
          "state_revision": {
            "type": [
              "integer",
              "null"
            ],
            "description": "State revision written by this job."
          },
          "created_at": {
            "type": "string",
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "updated_at": {
            "type": "string",
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          }
        },
        "required": [
          "job_id",
          "kind",
          "status",
          "attempts",
          "max_attempts",
          "error_code",
          "retryable",
          "event_id",
          "learner_id",
          "concept_id",
          "outcome",
          "state_revision",
          "created_at",
          "updated_at",
          "completed_at"
        ]
      },
      "ProcessingStatus": {
        "type": "string",
        "enum": [
          "pending",
          "processing",
          "completed",
          "failed"
        ]
      },
      "LearnerStateList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LearnerConceptState"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page, or null when there are no more results."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "LearnerConceptState": {
        "type": "object",
        "properties": {
          "learner_id": {
            "type": "string",
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "concept_id": {
            "type": "string",
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "concept_label": {
            "type": "string"
          },
          "state_revision": {
            "type": "integer",
            "minimum": 0,
            "description": "Monotonic revision of the stored state. 0 means no stored state yet."
          },
          "computed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "as_of": {
            "type": "string",
            "description": "Instant at which `status` and staleness were evaluated (the time of this response).",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "status": {
            "$ref": "#/components/schemas/StateStatus"
          },
          "pending_updates": {
            "type": "boolean",
            "description": "True when accepted events for this learner-concept have not been applied yet."
          },
          "summary": {
            "$ref": "#/components/schemas/ObservedSummary"
          },
          "intake": {
            "$ref": "#/components/schemas/IntakeCounts"
          },
          "forecast": {
            "type": "null",
            "description": "Always null for observed-v0."
          },
          "uncertainty": {
            "$ref": "#/components/schemas/Uncertainty"
          },
          "flags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StateFlag"
            }
          },
          "model_version": {
            "type": "string",
            "enum": [
              "observed-v0"
            ]
          },
          "eligibility_version": {
            "type": "string",
            "enum": [
              "eligibility-v1"
            ]
          },
          "mapping_revision": {
            "type": [
              "string",
              "null"
            ],
            "description": "Fingerprint of the item-concept mapping revisions used by this state revision."
          },
          "evidence_url": {
            "type": "string"
          }
        },
        "required": [
          "learner_id",
          "concept_id",
          "concept_label",
          "state_revision",
          "computed_at",
          "as_of",
          "status",
          "pending_updates",
          "summary",
          "intake",
          "forecast",
          "uncertainty",
          "flags",
          "model_version",
          "eligibility_version",
          "mapping_revision",
          "evidence_url"
        ]
      },
      "StateStatus": {
        "type": "string",
        "enum": [
          "insufficient_evidence",
          "observed_only",
          "stale_evidence"
        ],
        "description": "`insufficient_evidence`: fewer than 5 eligible attempts or fewer than 2 distinct sessions (a disclosure threshold, not a validation threshold). `stale_evidence`: the latest eligible attempt is more than 30 days old; the summary is not decayed. `observed_only`: enough recent observations to summarize; still no forecast."
      },
      "ObservedSummary": {
        "type": "object",
        "properties": {
          "window": {
            "type": "string",
            "enum": [
              "latest_200_eligible_events"
            ]
          },
          "window_size": {
            "type": "number",
            "enum": [
              200
            ]
          },
          "eligible_attempts": {
            "type": "integer",
            "minimum": 0,
            "description": "Eligible attempts inside the window."
          },
          "correct_attempts": {
            "type": "integer",
            "minimum": 0
          },
          "incorrect_attempts": {
            "type": "integer",
            "minimum": 0
          },
          "observed_accuracy": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "correct_attempts / eligible_attempts within the window, rounded to 6 decimals for transport. Null when there are no eligible attempts. A description of past observations, not a probability."
          },
          "distinct_sessions": {
            "type": "integer",
            "minimum": 0,
            "description": "Distinct session IDs among window attempts. Attempts without a session ID do not count."
          },
          "distinct_items": {
            "type": "integer",
            "minimum": 0
          },
          "first_observed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "last_observed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "history_truncated": {
            "type": "boolean"
          },
          "eligible_attempts_outside_window": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "window",
          "window_size",
          "eligible_attempts",
          "correct_attempts",
          "incorrect_attempts",
          "observed_accuracy",
          "distinct_sessions",
          "distinct_items",
          "first_observed_at",
          "last_observed_at",
          "history_truncated",
          "eligible_attempts_outside_window"
        ]
      },
      "IntakeCounts": {
        "type": "object",
        "properties": {
          "scanned_events": {
            "type": "integer",
            "minimum": 0,
            "description": "Stored events mapped to this concept that the computation read (all types)."
          },
          "response_events": {
            "type": "integer",
            "minimum": 0
          },
          "eligible_responses": {
            "type": "integer",
            "minimum": 0,
            "description": "All eligible responses read, including those outside the window."
          },
          "excluded_responses": {
            "type": "integer",
            "minimum": 0
          },
          "exclusions": {
            "type": "object",
            "properties": {
              "partial_credit": {
                "type": "integer",
                "minimum": 0
              },
              "unscored": {
                "type": "integer",
                "minimum": 0
              },
              "assistance_unknown": {
                "type": "integer",
                "minimum": 0
              },
              "assisted": {
                "type": "integer",
                "minimum": 0
              },
              "feedback_seen": {
                "type": "integer",
                "minimum": 0
              },
              "hint_before_response": {
                "type": "integer",
                "minimum": 0
              },
              "feedback_before_response": {
                "type": "integer",
                "minimum": 0
              },
              "repeat_in_session": {
                "type": "integer",
                "minimum": 0
              },
              "repeat_in_attempt": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "partial_credit",
              "unscored",
              "assistance_unknown",
              "assisted",
              "feedback_seen",
              "hint_before_response",
              "feedback_before_response",
              "repeat_in_session",
              "repeat_in_attempt"
            ]
          },
          "exposure_events": {
            "type": "integer",
            "minimum": 0
          },
          "hint_events": {
            "type": "integer",
            "minimum": 0
          },
          "feedback_events": {
            "type": "integer",
            "minimum": 0
          },
          "intervention_events": {
            "type": "integer",
            "minimum": 0
          },
          "superseded_events": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "scanned_events",
          "response_events",
          "eligible_responses",
          "excluded_responses",
          "exclusions",
          "exposure_events",
          "hint_events",
          "feedback_events",
          "intervention_events",
          "superseded_events"
        ],
        "description": "Counts over every stored event read for this learner-concept. Deliberately separate from the windowed summary so numerators and denominators are never mixed."
      },
      "Uncertainty": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "not_estimated"
            ]
          },
          "interval": {
            "type": "null"
          },
          "calibration_status": {
            "type": "string",
            "enum": [
              "unvalidated"
            ]
          }
        },
        "required": [
          "kind",
          "interval",
          "calibration_status"
        ],
        "description": "observed-v0 does not estimate uncertainty. No interval is invented."
      },
      "StateFlag": {
        "type": "string",
        "enum": [
          "no_eligible_evidence",
          "sparse_history",
          "insufficient_sessions",
          "stale_evidence",
          "session_missing",
          "history_truncated",
          "assistance_unreported",
          "scan_limit_reached"
        ],
        "description": "`no_eligible_evidence`: nothing counted yet. `sparse_history`: fewer than 5 eligible attempts. `insufficient_sessions`: fewer than 2 distinct sessions with a session ID. `stale_evidence`: latest eligible attempt older than 30 days. `session_missing`: some counted attempts had no session ID (reduced comparability). `history_truncated`: older eligible attempts fall outside the 200-attempt window. `assistance_unreported`: some responses were excluded because assistance was not reported. `scan_limit_reached`: the recomputation read its maximum number of stored events."
      },
      "EvidenceList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EvidenceItem"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page, or null when there are no more results."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "EvidenceItem": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "event_type": {
            "$ref": "#/components/schemas/EventType"
          },
          "occurred_at": {
            "type": "string",
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "received_at": {
            "type": "string",
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "source": {
            "type": "string"
          },
          "course_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "item_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "item_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "attempt_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "attempt_number": {
            "type": [
              "integer",
              "null"
            ]
          },
          "response_kind": {
            "$ref": "#/components/schemas/ResponseKind"
          },
          "correct": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "score": {
            "type": [
              "number",
              "null"
            ]
          },
          "assistance": {
            "$ref": "#/components/schemas/Assistance"
          },
          "hint_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "feedback_seen": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ]
          },
          "concept": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "concept_id": {
                "type": "string"
              },
              "concept_label": {
                "type": "string"
              },
              "role": {
                "type": "string",
                "enum": [
                  "primary",
                  "secondary"
                ]
              },
              "mapping_revision": {
                "type": "integer"
              }
            },
            "required": [
              "concept_id",
              "concept_label",
              "role",
              "mapping_revision"
            ],
            "description": "Approved primary concept mapping applied to this event, or null if unmapped."
          },
          "flags": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "unmapped_item",
                "superseded",
                "session_missing"
              ]
            }
          },
          "eligibility": {
            "$ref": "#/components/schemas/EvidenceEligibility"
          }
        },
        "required": [
          "event_id",
          "event_type",
          "occurred_at",
          "received_at",
          "source",
          "course_id",
          "session_id",
          "item_id",
          "item_version",
          "attempt_id",
          "attempt_number",
          "response_kind",
          "correct",
          "score",
          "assistance",
          "hint_count",
          "feedback_seen",
          "latency_ms",
          "confidence",
          "concept",
          "flags",
          "eligibility"
        ]
      },
      "EvidenceEligibility": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "eligible",
              "excluded",
              "pending",
              "not_applicable"
            ],
            "description": "`eligible`: counted as baseline evidence. `excluded`: a response that did not count (see reason). `pending`: not yet processed. `not_applicable`: not a response, or the item has no approved concept mapping."
          },
          "reason": {
            "$ref": "#/components/schemas/ExclusionReason"
          },
          "in_window": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether this eligible attempt is inside the latest-200 window of its state revision."
          },
          "eligibility_version": {
            "type": [
              "string",
              "null"
            ]
          },
          "state_revision": {
            "type": [
              "integer",
              "null"
            ],
            "description": "State revision that last evaluated this observation."
          }
        },
        "required": [
          "status",
          "reason",
          "in_window",
          "eligibility_version",
          "state_revision"
        ]
      },
      "ExclusionReason": {
        "type": [
          "string",
          "null"
        ],
        "enum": [
          "partial_credit",
          "unscored",
          "assistance_unknown",
          "assisted",
          "feedback_seen",
          "hint_before_response",
          "feedback_before_response",
          "repeat_in_session",
          "repeat_in_attempt",
          null
        ],
        "description": "`partial_credit`/`unscored`: not a binary outcome. `assistance_unknown`: assistance not reported or reported as unknown. `assisted`: hints or external help were used. `feedback_seen`: feedback was visible for the attempt. `hint_before_response`/`feedback_before_response`: a linked hint or feedback event preceded the response in the same attempt. `repeat_in_session`: not the first response to this item in its session. `repeat_in_attempt`: not the first response to this item in its attempt; this is the rule that applies when no session ID is sent."
      },
      "CheckPlan": {
        "type": "object",
        "properties": {
          "rule_version": {
            "type": "string",
            "enum": [
              "next-check-v0"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "shadow"
            ]
          },
          "validation_status": {
            "type": "string",
            "enum": [
              "unvalidated"
            ]
          },
          "parameters": {
            "type": "object",
            "properties": {
              "max_checks": {
                "type": "integer"
              },
              "persistence_delay_days": {
                "type": "integer"
              },
              "follow_up_days": {
                "type": "integer"
              }
            },
            "required": [
              "max_checks",
              "persistence_delay_days",
              "follow_up_days"
            ]
          },
          "pending_updates": {
            "type": "boolean",
            "description": "True when accepted events for this learner were not yet applied when the plan was issued."
          },
          "concepts_considered": {
            "type": "integer",
            "minimum": 0
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "position": {
                  "type": "integer",
                  "minimum": 1
                },
                "concept_id": {
                  "type": "string",
                  "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
                  "example": "learner_demo_01"
                },
                "concept_label": {
                  "type": "string"
                },
                "reason": {
                  "$ref": "#/components/schemas/CheckReason"
                },
                "evidence": {
                  "$ref": "#/components/schemas/CheckEvidence"
                },
                "candidate_items": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CandidateItem"
                  },
                  "maxItems": 3,
                  "description": "Up to three binary item versions mapped to the concept: items new to the learner first, then the least recently answered. Your platform chooses what to show."
                },
                "responses_requested": {
                  "type": "number",
                  "enum": [
                    1
                  ]
                },
                "follow_up": {
                  "$ref": "#/components/schemas/CheckFollowUp"
                }
              },
              "required": [
                "position",
                "concept_id",
                "concept_label",
                "reason",
                "evidence",
                "candidate_items",
                "responses_requested",
                "follow_up"
              ]
            }
          },
          "not_scheduled": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "reason": {
                  "$ref": "#/components/schemas/CheckReason"
                },
                "concepts": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "required": [
                "reason",
                "concepts"
              ]
            },
            "description": "Gaps left out by the burden budget, counted by reason."
          },
          "no_check_needed": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "concept_id": {
                  "type": "string",
                  "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
                  "example": "learner_demo_01"
                },
                "concept_label": {
                  "type": "string"
                },
                "last_independent_at": {
                  "type": "string",
                  "description": "UTC instant in ISO 8601 format with millisecond precision.",
                  "example": "2026-09-13T09:00:00.000Z"
                },
                "check_due_after": {
                  "type": "string",
                  "description": "UTC instant in ISO 8601 format with millisecond precision.",
                  "example": "2026-09-13T09:00:00.000Z"
                }
              },
              "required": [
                "concept_id",
                "concept_label",
                "last_independent_at",
                "check_due_after"
              ]
            }
          },
          "data_gaps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "concept_id": {
                  "type": "string",
                  "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
                  "example": "learner_demo_01"
                },
                "concept_label": {
                  "type": "string"
                },
                "gap": {
                  "$ref": "#/components/schemas/CheckDataGap"
                }
              },
              "required": [
                "concept_id",
                "concept_label",
                "gap"
              ]
            }
          },
          "limitations": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "plan_id": {
            "type": "string",
            "format": "uuid",
            "example": "11111111-1111-4111-8111-111111111111"
          },
          "learner_id": {
            "type": "string",
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "issued_at": {
            "type": "string",
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "evidence_watermark": {
            "type": "string",
            "description": "Commit-order position of the newest stored event for this learner at issue time."
          },
          "follow_up_as_of": {
            "type": "string",
            "description": "When the follow-up was derived. The plan itself never changes after issue.",
            "example": "2026-09-13T09:00:00.000Z"
          }
        },
        "required": [
          "rule_version",
          "mode",
          "validation_status",
          "parameters",
          "pending_updates",
          "concepts_considered",
          "checks",
          "not_scheduled",
          "no_check_needed",
          "data_gaps",
          "limitations",
          "plan_id",
          "learner_id",
          "issued_at",
          "evidence_watermark",
          "follow_up_as_of"
        ]
      },
      "CheckReason": {
        "type": "string",
        "enum": [
          "no_independent_evidence",
          "assisted_since_independent_evidence",
          "insufficient_independent_evidence",
          "persistence_check_due",
          "stale_independent_evidence"
        ],
        "description": "Why an independent check is suggested, in the order the rule ranks them. `no_independent_evidence`: responses or exposure exist but none counted as independent. `assisted_since_independent_evidence`: assisted, unknown-assistance or feedback-affected responses occurred after the latest independent one. `insufficient_independent_evidence`: fewer than 5 counted attempts or fewer than 2 sessions. `persistence_check_due`: enough recent independent evidence, but the latest is older than the declared persistence delay. `stale_independent_evidence`: the latest independent evidence is more than 30 days old."
      },
      "CheckEvidence": {
        "type": "object",
        "properties": {
          "state_revision": {
            "type": "integer",
            "minimum": 0
          },
          "status": {
            "$ref": "#/components/schemas/StateStatus"
          },
          "independent_responses": {
            "type": "integer",
            "minimum": 0,
            "description": "Every counted (eligible) response read for this concept, inside or outside the window."
          },
          "counted_in_window": {
            "type": "integer",
            "minimum": 0
          },
          "distinct_sessions": {
            "type": "integer",
            "minimum": 0
          },
          "last_independent_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "not_independent_responses": {
            "type": "integer",
            "minimum": 0,
            "description": "Responses excluded because assistance was used, unreported or unknown, or feedback or a hint came first."
          },
          "last_not_independent_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          }
        },
        "required": [
          "state_revision",
          "status",
          "independent_responses",
          "counted_in_window",
          "distinct_sessions",
          "last_independent_at",
          "not_independent_responses",
          "last_not_independent_at"
        ]
      },
      "CandidateItem": {
        "type": "object",
        "properties": {
          "item_id": {
            "type": "string",
            "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
            "example": "learner_demo_01"
          },
          "item_version": {
            "type": "string"
          },
          "novelty": {
            "$ref": "#/components/schemas/ItemNovelty"
          },
          "last_answered_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          }
        },
        "required": [
          "item_id",
          "item_version",
          "novelty",
          "last_answered_at"
        ]
      },
      "ItemNovelty": {
        "type": "string",
        "enum": [
          "new_to_learner",
          "previously_answered"
        ],
        "description": "Whether the learner had responded to this item version before. A correct response on a repeated item may reflect a remembered answer rather than the skill."
      },
      "CheckFollowUp": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "observed",
              "awaiting",
              "not_observed"
            ],
            "description": "`observed`: an independent response on the concept occurred in the window. `awaiting`: the window is open, or responses in it are not yet evaluated. `not_observed`: the window closed without one; this is missing follow-up, neither failure nor success."
          },
          "window_ends_at": {
            "type": "string",
            "description": "UTC instant in ISO 8601 format with millisecond precision.",
            "example": "2026-09-13T09:00:00.000Z"
          },
          "observed": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "event_id": {
                "type": "string",
                "description": "Opaque, case-sensitive identifier chosen by the integrating platform. 1–128 UTF-8 bytes, no control characters. Never a name or email address.",
                "example": "learner_demo_01"
              },
              "item_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "item_version": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "occurred_at": {
                "type": "string",
                "description": "UTC instant in ISO 8601 format with millisecond precision.",
                "example": "2026-09-13T09:00:00.000Z"
              },
              "correct": {
                "type": "boolean"
              },
              "delay_hours": {
                "type": "number",
                "minimum": 0
              },
              "item_novelty": {
                "$ref": "#/components/schemas/ItemNovelty"
              },
              "candidate_item": {
                "type": "boolean"
              }
            },
            "required": [
              "event_id",
              "item_id",
              "item_version",
              "occurred_at",
              "correct",
              "delay_hours",
              "item_novelty",
              "candidate_item"
            ]
          },
          "responses_in_window": {
            "type": "integer",
            "minimum": 0,
            "description": "Every response on the concept in the window, counted or not: the learner time the follow-up used."
          },
          "not_independent_responses_in_window": {
            "type": "integer",
            "minimum": 0
          },
          "responses_awaiting_evaluation": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "status",
          "window_ends_at",
          "observed",
          "responses_in_window",
          "not_independent_responses_in_window",
          "responses_awaiting_evaluation"
        ]
      },
      "CheckDataGap": {
        "type": "string",
        "enum": [
          "sessions_not_reported",
          "no_scoreable_items"
        ],
        "description": "A gap no check can close. `sessions_not_reported`: enough counted attempts exist, but events carry no session IDs, so the two-session disclosure rule can never be met. `no_scoreable_items`: no binary item version is mapped to the concept, so a response could not count under eligibility-v1."
      },
      "CreateCheckPlanRequest": {
        "type": "object",
        "properties": {
          "max_checks": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "description": "Burden budget: the most checks to schedule, each one scored response. Default 5."
          },
          "persistence_delay_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 29,
            "description": "Days after the latest independent evidence at which a persistence check becomes due. Default 7. A declared choice, not a validated interval."
          },
          "follow_up_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60,
            "description": "Days after issue during which an independent response counts as follow-up. Default 7."
          }
        },
        "additionalProperties": false
      }
    },
    "parameters": {}
  },
  "paths": {
    "/health/live": {
      "get": {
        "summary": "Process liveness",
        "description": "Reports only that the process can respond. No infrastructure details.",
        "responses": {
          "200": {
            "description": "Alive",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "status"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "post": {
        "summary": "Send one learner event",
        "description": "Stores one event durably and schedules processing. 202 means stored, not yet reflected in state. Exact duplicates are acknowledged and not counted again. Requires scope `events:write`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Required for every mutation. Reuse the same key when retrying the same request; a different body with the same key returns 409.",
              "example": "demo-send-0001"
            },
            "required": true,
            "description": "Required for every mutation. Reuse the same key when retrying the same request; a different body with the same key returns 409.",
            "name": "Idempotency-Key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "schema_version": "1.0",
                "event_id": "evt_demo_0001",
                "event_type": "response",
                "learner_id": "learner_demo_01",
                "occurred_at": "2026-09-13T09:00:00Z",
                "source": "partner_backend",
                "course_id": "cloud_foundations",
                "session_id": "session_demo_01",
                "item_id": "question_012",
                "item_version": "1",
                "attempt_id": "attempt_demo_01",
                "attempt_number": 1,
                "response_kind": "binary",
                "correct": true,
                "latency_ms": 18400,
                "hint_count": 0,
                "assistance": "none",
                "confidence": null,
                "feedback_seen": false
              },
              "schema": {
                "$ref": "#/components/schemas/LearningEvent"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Stored. Poll the returned jobs for processing status.",
            "content": {
              "application/json": {
                "example": {
                  "receipt_id": "11111111-1111-4111-8111-111111111111",
                  "accepted_count": 1,
                  "duplicate_count": 0,
                  "job_ids": [
                    "22222222-2222-4222-8222-222222222222"
                  ],
                  "processing_status": "pending",
                  "request_id": "req_demo_01"
                },
                "schema": {
                  "$ref": "#/components/schemas/IngestionReceipt"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, missing Idempotency-Key, invalid query or cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key reused with a different body, event_id reused with different content, or item version conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The learner was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Body over 64 KiB or more than 25 events. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "Body is not application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Semantically invalid input. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable or commit outcome unknown. Retry with the same Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/events/batch": {
      "post": {
        "summary": "Send up to 25 events atomically",
        "description": "The whole batch is validated before anything is stored. Any invalid event rejects the batch with indexed details; duplicate event IDs within one batch are rejected. Requires scope `events:write`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Required for every mutation. Reuse the same key when retrying the same request; a different body with the same key returns 409.",
              "example": "demo-send-0001"
            },
            "required": true,
            "description": "Required for every mutation. Reuse the same key when retrying the same request; a different body with the same key returns 409.",
            "name": "Idempotency-Key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "events": [
                  {
                    "schema_version": "1.0",
                    "event_id": "evt_demo_0001",
                    "event_type": "response",
                    "learner_id": "learner_demo_01",
                    "occurred_at": "2026-09-13T09:00:00Z",
                    "source": "partner_backend",
                    "course_id": "cloud_foundations",
                    "session_id": "session_demo_01",
                    "item_id": "question_012",
                    "item_version": "1",
                    "attempt_id": "attempt_demo_01",
                    "attempt_number": 1,
                    "response_kind": "binary",
                    "correct": true,
                    "latency_ms": 18400,
                    "hint_count": 0,
                    "assistance": "none",
                    "confidence": null,
                    "feedback_seen": false
                  },
                  {
                    "schema_version": "1.0",
                    "event_id": "evt_demo_0002",
                    "event_type": "hint",
                    "learner_id": "learner_demo_01",
                    "occurred_at": "2026-09-13T09:02:10Z",
                    "source": "partner_backend",
                    "session_id": "session_demo_01",
                    "item_id": "question_013",
                    "item_version": "1",
                    "attempt_id": "attempt_demo_02"
                  }
                ]
              },
              "schema": {
                "type": "object",
                "properties": {
                  "events": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/LearningEvent"
                    },
                    "minItems": 1,
                    "maxItems": 25
                  }
                },
                "required": [
                  "events"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Stored atomically.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestionReceipt"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, missing Idempotency-Key, invalid query or cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key reused with a different body, event_id reused with different content, or item version conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The learner was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Body over 64 KiB or more than 25 events. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "Body is not application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Semantically invalid input. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable or commit outcome unknown. Retry with the same Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/jobs/{job_id}": {
      "get": {
        "summary": "Processing job status",
        "description": "Status of a processing job created by an ingestion request in the same project. Requires scope `events:write`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "job_id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Job status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown resource, or a resource in another project (indistinguishable by design).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/learners/{learner_id}/state": {
      "get": {
        "summary": "Concept summaries for a learner",
        "description": "observed-v0 summaries for every concept with mapped evidence for this learner, ordered by concept ID. Side-effect free. Requires scope `state:read`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "External learner ID, URL-encoded.",
              "example": "learner_demo_01"
            },
            "required": true,
            "description": "External learner ID, URL-encoded.",
            "name": "learner_id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "description": "Default 50, maximum 100."
            },
            "required": false,
            "description": "Default 50, maximum 100.",
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated states",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LearnerStateList"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, missing Idempotency-Key, invalid query or cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown resource, or a resource in another project (indistinguishable by design).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The learner was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/learners/{learner_id}/concepts/{concept_id}": {
      "get": {
        "summary": "One learner-concept state",
        "description": "The latest completed state revision with `pending_updates`. A registered learner and concept without evidence return an insufficient_evidence projection (revision 0). Requires scope `state:read`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "External learner ID, URL-encoded.",
              "example": "learner_demo_01"
            },
            "required": true,
            "description": "External learner ID, URL-encoded.",
            "name": "learner_id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "example": "networking.dns"
            },
            "required": true,
            "name": "concept_id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "State",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LearnerConceptState"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, missing Idempotency-Key, invalid query or cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown resource, or a resource in another project (indistinguishable by design).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The learner was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/learners/{learner_id}/evidence": {
      "get": {
        "summary": "Observations behind the state",
        "description": "Stored observations for a learner, newest first (occurred_at, then event_id), with concept provenance and eligibility. Unknown filters return 400. Requires scope `evidence:read`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "External learner ID, URL-encoded.",
              "example": "learner_demo_01"
            },
            "required": true,
            "description": "External learner ID, URL-encoded.",
            "name": "learner_id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "concept_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "response",
                "exposure",
                "hint",
                "feedback",
                "intervention"
              ]
            },
            "required": false,
            "name": "event_type",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "required": false,
            "name": "include_superseded",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "required": false,
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated evidence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvidenceList"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, missing Idempotency-Key, invalid query or cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown resource, or a resource in another project (indistinguishable by design).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The learner was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/learners/{learner_id}/check-plans": {
      "post": {
        "summary": "Issue a shadow-mode check plan",
        "description": "Applies the documented next-check-v0 rule to the learner's current state and stores the result, unchanged from then on. Each check asks for one independent response on a concept, with a reason and the evidence behind it. Shadow mode: nothing is delivered to the learner, and the rule is unvalidated. Send `{}` for the defaults. Requires scope `checks:write`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "External learner ID, URL-encoded.",
              "example": "learner_demo_01"
            },
            "required": true,
            "description": "External learner ID, URL-encoded.",
            "name": "learner_id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Required for every mutation. Reuse the same key when retrying the same request; a different body with the same key returns 409.",
              "example": "demo-send-0001"
            },
            "required": true,
            "description": "Required for every mutation. Reuse the same key when retrying the same request; a different body with the same key returns 409.",
            "name": "Idempotency-Key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "max_checks": 3
              },
              "schema": {
                "$ref": "#/components/schemas/CreateCheckPlanRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored. Follow-up is derived from evidence stored after issue; a replay with the same Idempotency-Key and body returns the stored response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckPlan"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, missing Idempotency-Key, invalid query or cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown resource, or a resource in another project (indistinguishable by design).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key reused with a different body, event_id reused with different content, or item version conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The learner was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Body over 4 KiB. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "Body is not application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Semantically invalid input. Nothing was stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable or commit outcome unknown. Retry with the same Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/check-plans/{plan_id}": {
      "get": {
        "summary": "A check plan with its follow-up",
        "description": "The plan exactly as issued, with each check's follow-up derived now: `observed` (an independent response on the concept in the window), `awaiting` or `not_observed`. Missing follow-up is never treated as failure. Side-effect free. Requires scope `state:read`.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "plan_id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Check plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckPlan"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The API key lacks the required scope, or the project is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown resource, or a resource in another project (indistinguishable by design).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The learner was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {}
}
