{
  "openapi": "3.1.0",
  "info": {
    "title": "Caliper Sensor API",
    "description": "The Caliper API receives Caliper 1.2 Sensor API envelopes and stores them as tenant-scoped learning-activity event facts. Envelope submissions are content-addressed by canonical JSON SHA-256 hashes so repeated submissions dedupe deterministically. Events are immutable append-only Caliper JSON-LD facts keyed by their urn:uuid: event ids. Described entities are upserted from event payloads so downstream surfaces can resolve the opaque JSON-LD graph.",
    "version": "1.0"
  },
  "tags": [
    {
      "name": "Caliper described entity",
      "description": "A described JSON-LD Caliper entity observed inside submitted envelopes. The ingest hook upserts by `(tenantId, entityIri)`: the first sighting sets `firstSeenAt`, later sightings preserve it while refreshing `lastSeenAt`, `rawEntity`, and `canonicalEntity`."
    },
    {
      "name": "Caliper envelope receipt",
      "description": "One received Caliper 1.2 envelope, keyed by the tenant-scoped SHA-256 content address that the ingest hook computes from the canonical envelope. This receipt records the sending sensor when known, the required Caliper context IRI, the send and receipt instants, the processing status, and both raw and canonical JSON payloads so the accepted or rejected envelope can be audited and read back by hash."
    },
    {
      "name": "Caliper event",
      "description": "One accepted Caliper 1.2 event, stored as the immutable tenant-scoped event ledger that downstream Alpha event surfaces project. The row is keyed by the event's own JSON-LD `id` IRI (eventId for append idempotency)."
    },
    {
      "name": "Caliper event entity link",
      "description": "A tenant-scoped junction row recording one Caliper Event-to-Entity relationship at the exact JSON-LD path where the entity appeared. Its identity is the natural tuple `(tenantId, eventIri, entityIri, relation, rawPath)`, so repeated references are distinguished without introducing a surrogate link id."
    },
    {
      "name": "Caliper sensor",
      "description": "A registered sender allowed to submit Caliper 1.2 envelopes for one tenant. The row is keyed by the sensor's stable IRI inside the tenant, so envelope receipts can reference the sending system without a local surrogate id."
    }
  ],
  "paths": {
    "/caliper/v1p2/entities/{entityIri}": {
      "get": {
        "operationId": "getCaliperEntity",
        "summary": "Get a described Caliper entity by IRI",
        "description": "Returns the latest described JSON-LD entity observed by the ingest pipeline. The write path remains the submit-envelope command; this read exists so local GUIDRef links have a dereferenceable target.\n\n> **Requires role:** `reader` · `admin`",
        "tags": [
          "Caliper described entity"
        ],
        "parameters": [
          {
            "name": "entityIri",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaliperEntity"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Problem (caliper_entity_not_found).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/caliper/v1p2/envelopes": {
      "get": {
        "operationId": "getCaliperEnvelope",
        "summary": "Get an envelope receipt by hash",
        "description": "Returns the Caliper ingest receipt whose tenant-scoped `envelopeHash` matches the required `hash` query parameter. This is the plain CRUD read projection for `GET /caliper/v1p2/envelopes?hash=`.\n\n> **Requires role:** `reader` · `admin`",
        "tags": [
          "Caliper envelope receipt"
        ],
        "parameters": [
          {
            "name": "hash",
            "in": "query",
            "required": true,
            "description": "The `sha256:` envelope content address to read back.",
            "schema": {
              "type": "string",
              "pattern": "^sha256:[0-9a-f]{64}$"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaliperEnvelope"
                },
                "example": {
                  "envelopeHash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                  "sensorIri": {
                    "href": "/caliper/v1p2/sensors/https://sensor.example.edu/caliper/sensors/lms-primary",
                    "sourcedId": "https://sensor.example.edu/caliper/sensors/lms-primary",
                    "type": "caliperSensor"
                  },
                  "dataVersion": "http://purl.imsglobal.org/ctx/caliper/v1p2",
                  "sendTime": "2026-07-01T12:00:00.000Z",
                  "receivedAt": "2026-07-01T12:00:01.000Z",
                  "envelopeStatus": "processed",
                  "rawEnvelope": {
                    "sensor": "https://sensor.example.edu/caliper/sensors/lms-primary",
                    "sendTime": "2026-07-01T12:00:00.000Z",
                    "dataVersion": "http://purl.imsglobal.org/ctx/caliper/v1p2",
                    "data": [
                      {
                        "id": "urn:uuid:cccccccc-0000-4000-8000-000000000001",
                        "type": "AssessmentEvent"
                      }
                    ]
                  },
                  "canonicalEnvelope": {
                    "dataVersion": "http://purl.imsglobal.org/ctx/caliper/v1p2",
                    "sendTime": "2026-07-01T12:00:00.000Z",
                    "sensor": "https://sensor.example.edu/caliper/sensors/lms-primary",
                    "data": [
                      {
                        "id": "urn:uuid:cccccccc-0000-4000-8000-000000000001",
                        "type": "AssessmentEvent"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Problem (caliper_envelope_not_found).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/caliper/v1p2/events": {
      "post": {
        "operationId": "submitCaliperEnvelope",
        "summary": "Submit a Caliper envelope",
        "description": "Ingests one Caliper Sensor API envelope. Validates the envelope-level shape and the fixed `dataVersion`; the required `submitCaliperEnvelope` hook performs deep event/entity validation, canonical hashing, duplicate detection, and writes the envelope, events, entities, and links in one transaction.\n\n> **Requires role:** `sensor` · `admin`",
        "tags": [
          "Caliper event"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "The idempotency key (max 128 chars). Optional — when present, the call is replay-protected; a completed replay returns the stored response at 200 with idempotency-replayed: true.",
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sensor": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The sending sensor's IRI from the Caliper envelope."
                  },
                  "sendTime": {
                    "type": "string",
                    "format": "date-time",
                    "description": "The instant the sender says this envelope was sent."
                  },
                  "dataVersion": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The Caliper 1.2 context IRI. Wrong values are rejected by the submitCaliperEnvelope hook with 422 unsupported_data_version (CAL-02); the envelope entity keeps the v1p2 const for storage."
                  },
                  "data": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "A Caliper event or described entity from the envelope `data` array.",
                      "additionalProperties": true
                    },
                    "minItems": 1,
                    "description": "The non-empty heterogeneous array of Caliper events and described entities. Each item is open at the schema boundary and deeply validated by the hook."
                  }
                },
                "required": [
                  "sensor",
                  "sendTime",
                  "dataVersion",
                  "data"
                ],
                "additionalProperties": false
              },
              "example": {
                "sensor": "https://sensor.example.edu/caliper/sensors/lms-primary",
                "sendTime": "2026-07-01T12:00:00.000Z",
                "dataVersion": "http://purl.imsglobal.org/ctx/caliper/v1p2",
                "data": [
                  {
                    "@context": "http://purl.imsglobal.org/ctx/caliper/v1p2",
                    "id": "urn:uuid:cccccccc-0000-4000-8000-000000000001",
                    "type": "AssessmentEvent",
                    "action": "Started",
                    "actor": {
                      "id": "https://example.edu/users/554433",
                      "type": "Person"
                    },
                    "object": {
                      "id": "https://example.edu/terms/201801/courses/7/assess/1",
                      "type": "Assessment"
                    },
                    "eventTime": "2026-07-01T12:00:00.000Z"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK (idempotent replay — the stored first response; idempotency-replayed: true).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "null",
                  "description": "The stored response of a no-content operation is JSON null."
                }
              }
            },
            "headers": {
              "idempotency-replayed": {
                "description": "true when this response is a stored idempotent replay (the hook did not run).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            }
          },
          "204": {
            "description": "No Content.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "No content; the envelope was accepted and the event master rows were written or deduplicated."
                }
              }
            },
            "headers": {
              "caliper-envelope-hash": {
                "schema": {
                  "type": "string",
                  "pattern": "^sha256:[0-9a-f]{64}$",
                  "description": "The SHA-256 content address of the accepted canonical envelope."
                }
              },
              "idempotency-replayed": {
                "description": "true when this response is a stored idempotent replay (the hook did not run).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not Found.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict — an operation with this Idempotency-Key is already in progress (60s lock).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Problem (unsupported_data_version).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required — this operation requires the Idempotency-Key header.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listCaliperEvents",
        "summary": "List Caliper events",
        "description": "Lists immutable Caliper event master rows using exact filters used by read-after-write checks: event IRI and envelope hash. Object IRI is also filterable to support the GradeEvent-to-Attempt hand-off.\n\n> **Requires role:** `reader` · `admin`",
        "tags": [
          "Caliper event"
        ],
        "parameters": [
          {
            "name": "eventIri",
            "in": "query",
            "required": false,
            "description": "Restrict results to one event IRI.",
            "schema": {
              "type": "string",
              "pattern": "^urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
            },
            "x-filter": {
              "column": "eventIri",
              "operator": "eq"
            }
          },
          {
            "name": "envelopeHash",
            "in": "query",
            "required": false,
            "description": "Restrict results to events carried by one accepted envelope.",
            "schema": {
              "type": "string",
              "pattern": "^sha256:[0-9a-f]{64}$"
            },
            "x-filter": {
              "column": "envelopeHash",
              "operator": "eq"
            }
          },
          {
            "name": "objectIri",
            "in": "query",
            "required": false,
            "description": "Restrict results to events whose opaque object IRI matches the supplied value.",
            "schema": {
              "type": "string"
            },
            "x-filter": {
              "column": "objectIri",
              "operator": "eq"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to return on this page (clamped server-side).",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor — pass the previous page's `nextCursor` to fetch the next page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CaliperEvent"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor for the next page, or null when this is the last page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not Found.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/caliper/v1p2/events/{eventIri}": {
      "get": {
        "operationId": "getCaliperEvent",
        "summary": "Get a Caliper event",
        "description": "Returns one stored Caliper event by its tenant-scoped event IRI. This is a read projection over the immutable event master, not a write path.\n\n> **Requires role:** `reader` · `admin`",
        "tags": [
          "Caliper event"
        ],
        "parameters": [
          {
            "name": "eventIri",
            "in": "path",
            "required": true,
            "description": "The `urn:uuid:` event IRI to read.",
            "schema": {
              "type": "string",
              "pattern": "^urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaliperEvent"
                },
                "example": {
                  "id": "urn:uuid:cccccccc-0000-4000-8000-000000000001",
                  "type": "AssessmentEvent",
                  "action": "Started",
                  "profile": "AssessmentProfile",
                  "actorIri": "https://example.edu/users/554433",
                  "objectIri": "https://example.edu/terms/201801/courses/7/assess/1",
                  "eventTime": "2026-07-01T12:00:00.000Z",
                  "envelopeHash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                  "envelopeStatus": "processed",
                  "eventCount": 1,
                  "actor": {
                    "id": "https://example.edu/users/554433",
                    "type": "Person"
                  },
                  "object": {
                    "id": "https://example.edu/terms/201801/courses/7/assess/1",
                    "type": "Assessment"
                  },
                  "rawEvent": {
                    "@context": "http://purl.imsglobal.org/ctx/caliper/v1p2",
                    "id": "urn:uuid:cccccccc-0000-4000-8000-000000000001",
                    "type": "AssessmentEvent"
                  },
                  "canonicalEvent": {
                    "@context": "http://purl.imsglobal.org/ctx/caliper/v1p2",
                    "id": "urn:uuid:cccccccc-0000-4000-8000-000000000001",
                    "type": "AssessmentEvent"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Problem (caliper_event_not_found).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/caliper/v1p2/event-entity-links": {
      "get": {
        "operationId": "listCaliperEventEntityLinks",
        "summary": "List Caliper event/entity links by exact tuple filters",
        "description": "Returns junction rows by exact natural-key filters. Public writes are intentionally absent; links are created by the submit-envelope hook through the typed internal repository.\n\n> **Requires role:** `reader` · `admin`",
        "tags": [
          "Caliper event entity link"
        ],
        "parameters": [
          {
            "name": "eventIri",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "x-filter": {
              "column": "eventIri",
              "operator": "eq"
            }
          },
          {
            "name": "entityIri",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "x-filter": {
              "column": "entityIri",
              "operator": "eq"
            }
          },
          {
            "name": "relation",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "x-filter": {
              "column": "relation",
              "operator": "eq"
            }
          },
          {
            "name": "rawPath",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "x-filter": {
              "column": "rawPath",
              "operator": "eq"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to return on this page (clamped server-side).",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor — pass the previous page's `nextCursor` to fetch the next page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CaliperEventEntityLink"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor for the next page, or null when this is the last page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not Found.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/caliper/v1p2/sensors": {
      "post": {
        "operationId": "createCaliperSensor",
        "summary": "Register a Caliper sensor",
        "description": "Creates the tenant-scoped operational sender record used by Caliper envelope receipts. This is the small admin write surface whose per-resource updates are guarded by If-Match/ETag.\n\n> **Requires role:** `admin`",
        "tags": [
          "Caliper sensor"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCaliperSensorRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaliperSensor"
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "The resource's current entity-tag (RFC 9110, quoted). Send it back in If-Match on the next write (optimistic concurrency).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not Found.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listCaliperSensors",
        "summary": "List registered Caliper sensors.",
        "description": "Lists the tenant's registered sensors (the sensor registry) with cursor pagination.\n\n> **Requires role:** `reader` · `admin`",
        "tags": [
          "Caliper sensor"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of items to return on this page (clamped server-side).",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque pagination cursor — pass the previous page's `nextCursor` to fetch the next page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CaliperSensor"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor for the next page, or null when this is the last page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Not Found.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/caliper/v1p2/sensors/{sensorIri}": {
      "get": {
        "operationId": "getCaliperSensor",
        "summary": "Get a Caliper sensor by IRI",
        "description": "Returns the sender record and emits the resource ETag used for guarded updates.\n\n> **Requires role:** `reader` · `admin`",
        "tags": [
          "Caliper sensor"
        ],
        "parameters": [
          {
            "name": "sensorIri",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaliperSensor"
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "The resource's current entity-tag (RFC 9110, quoted). Send it back in If-Match on the next write (optimistic concurrency).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Problem (caliper_sensor_not_found).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateCaliperSensor",
        "summary": "Update a Caliper sensor (If-Match required)",
        "description": "Updates mutable operational metadata for a registered sensor. Optimistic concurrency requires If-Match and returns a fresh ETag.\n\n> **Requires role:** `admin`",
        "tags": [
          "Caliper sensor"
        ],
        "parameters": [
          {
            "name": "sensorIri",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "description": "The resource's current ETag (from the last GET/write response), quoted per RFC 9110. REQUIRED — absent returns 428; a stale ETag returns 412 (refetch and retry). Send `*` to overwrite unconditionally.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCaliperSensorRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaliperSensor"
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "The resource's current entity-tag (RFC 9110, quoted). Send it back in If-Match on the next write (optimistic concurrency).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Problem (caliper_sensor_not_found).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Conflict.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "412": {
            "description": "Precondition Failed — the If-Match ETag is stale (the resource was modified); refetch and retry.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "428": {
            "description": "Precondition Required — this write requires the If-Match header (send the resource's ETag).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "501": {
            "description": "Not Implemented.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CaliperConformanceRun": {
        "type": "object",
        "properties": {
          "conformanceRunId": {
            "type": "string",
            "format": "uuid",
            "description": "Server-minted UUID for this conformance evidence run, exposed on the wire as `conformanceRunId`.",
            "readOnly": true
          },
          "sourceName": {
            "type": "string",
            "minLength": 1,
            "description": "Name of the conformance source or harness that produced the evidence, such as an IMS Caliper validator, local smoke test, or platform interoperability suite."
          },
          "sourceVersion": {
            "type": "string",
            "minLength": 1,
            "description": "Version of the source, harness, or ruleset used for this run, so later reviews can distinguish evidence produced by different Caliper conformance baselines."
          },
          "result": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "warning"
            ],
            "description": "Overall outcome of the run: `pass` for accepted evidence, `fail` for a conformance break, and `warning` for evidence that completed with non-blocking concerns."
          },
          "evidence": {
            "type": "object",
            "description": "JSONB payload carrying the local conformance evidence, including validator output, checked scenarios, response headers, hashes, or other structured proof needed to interpret the result.",
            "additionalProperties": true
          },
          "startedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Instant when the conformance run started, recorded as a UTC date-time string."
          },
          "finishedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Instant when the conformance run finished, recorded as a UTC date-time string. A row represents completed evidence, so this value is required."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true,
            "description": "Server-managed row-creation timestamp (RFC 3339). Read-only — never accepted on create or update."
          }
        },
        "required": [
          "conformanceRunId",
          "sourceName",
          "sourceVersion",
          "result",
          "evidence",
          "startedAt",
          "finishedAt",
          "createdAt"
        ]
      },
      "CaliperEntity": {
        "type": "object",
        "properties": {
          "entityIri": {
            "type": "string",
            "minLength": 1,
            "description": "The Caliper entity `id` IRI or stable source identifier. Together with the tenant id, this is the natural primary key for the described entity index."
          },
          "entityType": {
            "type": "string",
            "minLength": 1,
            "description": "The Caliper JSON-LD `type` value for the entity. This remains an open string rather than a DDL enum because Caliper entity classes are broad, profile-extensible, and deeply validated by the envelope ingest hook."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "A human-readable label copied from the Caliper entity when the source supplies one."
          },
          "dateCreated": {
            "type": "string",
            "format": "date-time",
            "description": "The instant the source says this entity was created, copied from the Caliper `dateCreated` field when present."
          },
          "dateModified": {
            "type": "string",
            "format": "date-time",
            "description": "The instant the source says this entity last changed, copied from the Caliper `dateModified` field when present."
          },
          "firstSeenAt": {
            "type": "string",
            "format": "date-time",
            "description": "The server-side instant when this tenant first observed the entity. The ingest hook treats this field as immutable during upsert-on-conflict handling."
          },
          "lastSeenAt": {
            "type": "string",
            "format": "date-time",
            "description": "The server-side instant when this tenant most recently observed the entity. The ingest hook refreshes this value whenever a later envelope describes the same `entityIri`."
          },
          "rawEntity": {
            "type": "object",
            "description": "The Caliper entity JSON exactly as received inside the envelope, retained as a JSONB audit payload.",
            "additionalProperties": true
          },
          "canonicalEntity": {
            "type": "object",
            "description": "The normalized canonical JSON entity used by the ingest hook for deterministic comparison and readback.",
            "additionalProperties": true
          }
        },
        "required": [
          "entityIri",
          "entityType",
          "firstSeenAt",
          "lastSeenAt",
          "rawEntity",
          "canonicalEntity"
        ]
      },
      "CaliperEnvelope": {
        "type": "object",
        "properties": {
          "envelopeHash": {
            "type": "string",
            "pattern": "^sha256:[0-9a-f]{64}$",
            "description": "The canonical JSON SHA-256 content address for the envelope. Together with the tenant id, this is the natural primary key and the domain-layer duplicate-detection key."
          },
          "sensorIri": {
            "type": "object",
            "description": "A OneRoster GUIDRef to a caliperSensor — the referenced resource's sourcedId, its type, and a dereferenceable href. On write, either this object (its sourcedId is used) or the bare sourcedId string is accepted.",
            "required": [
              "sourcedId",
              "type"
            ],
            "properties": {
              "href": {
                "type": "string",
                "description": "The dereferenceable URL of the referenced resource."
              },
              "sourcedId": {
                "type": "string",
                "description": "The sourcedId of the referenced resource."
              },
              "type": {
                "type": "string",
                "const": "caliperSensor",
                "description": "The referenced resource type."
              }
            }
          },
          "dataVersion": {
            "type": "string",
            "enum": [
              "http://purl.imsglobal.org/ctx/caliper/v1p2"
            ],
            "const": "http://purl.imsglobal.org/ctx/caliper/v1p2",
            "description": "The Caliper 1.2 context IRI. The Sensor API requires this exact value; the schema const and singleton enum keep the API validator and database check on the same contract."
          },
          "sendTime": {
            "type": "string",
            "format": "date-time",
            "description": "The instant the sender says this envelope was sent, copied from the Caliper envelope `sendTime` field."
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "The server-side instant when this tenant received and recorded the envelope receipt."
          },
          "envelopeStatus": {
            "type": "string",
            "enum": [
              "received",
              "processed",
              "rejected",
              "duplicate"
            ],
            "description": "The receipt state for the envelope: `received` before normalization completes, `processed` after events and entities are written, `rejected` when validation fails, and `duplicate` when the hash matches a previously stored envelope."
          },
          "rejectionReason": {
            "type": "string",
            "description": "Human-readable validation or policy reason recorded when `envelopeStatus` is `rejected`; null for accepted and duplicate receipts."
          },
          "rawEnvelope": {
            "type": "object",
            "description": "The incoming Caliper envelope JSON exactly as received at the API boundary, retained as a JSONB audit payload.",
            "additionalProperties": true
          },
          "canonicalEnvelope": {
            "type": "object",
            "description": "The normalized canonical JSON envelope used for hash computation, duplicate detection, and deterministic readback.",
            "additionalProperties": true
          }
        },
        "required": [
          "envelopeHash",
          "dataVersion",
          "sendTime",
          "receivedAt",
          "envelopeStatus",
          "rawEnvelope",
          "canonicalEnvelope"
        ]
      },
      "CaliperEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$",
            "description": "The event's own JSON-LD `id`, constrained to a `urn:uuid:` URN. Together with the tenant id, this is the natural primary key; no surrogate event id is introduced."
          },
          "type": {
            "type": "string",
            "enum": [
              "AnnotationEvent",
              "AssessmentEvent",
              "AssessmentItemEvent",
              "AssignableEvent",
              "FeedbackEvent",
              "ForumEvent",
              "GradeEvent",
              "MediaEvent",
              "MessageEvent",
              "NavigationEvent",
              "QuestionnaireEvent",
              "QuestionnaireItemEvent",
              "ResourceManagementEvent",
              "SearchEvent",
              "SessionEvent",
              "SurveyEvent",
              "SurveyInvitationEvent",
              "ThreadEvent",
              "ToolLaunchEvent",
              "ToolUseEvent",
              "ViewEvent"
            ],
            "description": "The closed Caliper 1.2 event `type` vocabulary accepted by this base module. The wire spelling is `type`, while storage uses `eventType` so the domain column name stays explicit."
          },
          "action": {
            "type": "string",
            "enum": [
              "Abandoned",
              "Accepted",
              "Activated",
              "Added",
              "Archived",
              "Attached",
              "Bookmarked",
              "ChangedResolution",
              "ChangedSize",
              "ChangedSpeed",
              "ChangedVolume",
              "Classified",
              "ClosedPopout",
              "Commented",
              "Completed",
              "Copied",
              "Created",
              "Deactivated",
              "Declined",
              "Deleted",
              "Described",
              "DisabledClosedCaptioning",
              "Disliked",
              "Downloaded",
              "EnabledClosedCaptioning",
              "Ended",
              "EnteredFullScreen",
              "ExitedFullScreen",
              "ForwardedTo",
              "Graded",
              "Hid",
              "Highlighted",
              "Identified",
              "JumpedTo",
              "Launched",
              "Liked",
              "Linked",
              "LoggedIn",
              "LoggedOut",
              "MarkedAsRead",
              "MarkedAsUnread",
              "Modified",
              "Muted",
              "NavigatedTo",
              "OpenedPopout",
              "OptedIn",
              "OptedOut",
              "Paused",
              "Posted",
              "Printed",
              "Published",
              "Questioned",
              "Ranked",
              "Recommended",
              "Removed",
              "Reset",
              "Restarted",
              "Restored",
              "Resumed",
              "Retrieved",
              "Returned",
              "Reviewed",
              "Rewound",
              "Saved",
              "Searched",
              "Sent",
              "Shared",
              "Showed",
              "Skipped",
              "Started",
              "Submitted",
              "Subscribed",
              "Tagged",
              "TimedOut",
              "Unmuted",
              "Unpublished",
              "Unsubscribed",
              "Uploaded",
              "Used",
              "Viewed"
            ],
            "description": "The closed Caliper Action vocabulary. It is stored as a typed scalar because downstream projections and audits filter by action without inspecting the polymorphic JSON-LD payload."
          },
          "profile": {
            "type": "string",
            "enum": [
              "AnnotationProfile",
              "AssessmentProfile",
              "AssignableProfile",
              "FeedbackProfile",
              "ForumProfile",
              "GradingProfile",
              "MediaProfile",
              "ReadingProfile",
              "ResourceManagementProfile",
              "SearchProfile",
              "SessionProfile",
              "SurveyProfile",
              "ToolLaunchProfile",
              "ToolUseProfile",
              "GeneralProfile"
            ],
            "description": "The optional closed Caliper Profile value. When an event omits it, the ingest hook infers it from `eventType` using the complete Caliper event-to-profile map."
          },
          "actorIri": {
            "type": "string",
            "minLength": 1,
            "description": "The opaque JSON-LD IRI for the actor. This is intentionally not a OneRoster foreign key; resolving a Caliper IRI to a roster identity belongs to downstream projections."
          },
          "objectIri": {
            "type": "string",
            "minLength": 1,
            "description": "The opaque JSON-LD IRI for the object of the event. The field is indexed for GradeEvent-to-Attempt hand-off, but it remains a non-FK external IRI."
          },
          "eventTime": {
            "type": "string",
            "format": "date-time",
            "description": "The instant the event occurred, copied from the Caliper Event `eventTime` field and used as the append-ledger time axis."
          },
          "envelopeHash": {
            "type": "string",
            "pattern": "^sha256:[0-9a-f]{64}$",
            "description": "The SHA-256 content address of the accepted envelope that carried this event. The local tenant-carrying composite FK links each event to its envelope receipt.",
            "x-references": "caliperEnvelope.envelopeHash",
            "x-onDelete": "restrict"
          },
          "actor": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The actor reference from the Caliper JSON-LD payload. Caliper permits compact IRI references as well as expanded objects; storage is JSONB and the ingest hook requires the expanded object form for accepted writes."
          },
          "object": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The object reference from the Caliper JSON-LD payload. Caliper permits compact IRI references as well as expanded objects; storage is JSONB and the ingest hook requires the expanded object form for accepted writes."
          },
          "generated": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              },
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            ],
            "description": "The optional generated JSON-LD value, such as a GradeEvent score result. Caliper profiles can supply one object, a compact IRI, or a collection, so this is declared as a value union instead of hidden in ad-hoc code."
          },
          "target": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The optional target JSON-LD reference from event types that name a target resource."
          },
          "edApp": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The optional educational application JSON-LD reference associated with the event."
          },
          "group": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The optional group JSON-LD reference associated with the event context."
          },
          "membership": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The optional membership JSON-LD reference associated with the event context."
          },
          "referrer": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The optional referrer JSON-LD reference captured from the event."
          },
          "federatedSession": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "description": "The optional federated session JSON-LD reference captured from the event."
          },
          "eventExtensions": {
            "type": "object",
            "description": "Vendor or profile-specific event extension properties that are not modeled as typed scalar columns.",
            "additionalProperties": true
          },
          "envelopeStatus": {
            "type": "string",
            "enum": [
              "received",
              "processed",
              "rejected",
              "duplicate"
            ],
            "description": "Required internal ledger state, denormalized from the envelope and set by the ingest hook in the same transaction as the event write. Read-only on the public request surface. Its non-null storage invariant makes downstream processed-only projections fail closed instead of admitting legacy or malformed NULL rows.",
            "readOnly": true
          },
          "eventCount": {
            "type": "integer",
            "const": 1,
            "description": "Always 1. Additive ledger measure so foldAsOf(actorIri, asOf) returns the accepted event count for that actor as-of (analytics compute-on-read).",
            "default": 1
          },
          "alphaKind": {
            "type": "string",
            "enum": [
              "app_opened",
              "content_viewed",
              "video_started",
              "video_scrubbed",
              "hint_requested",
              "question_answered",
              "assessment_started",
              "assessment_submitted",
              "lesson_finished",
              "search_run",
              "feedback_viewed",
              "session_joined"
            ],
            "description": "The governed Alpha activity kind, computed at write time from the Caliper (type, profile, action) tuple via the kind_map policy. Owned by the events module; set once at ingest, never mutated.",
            "x-owned-by": "events"
          },
          "studentId": {
            "type": "string",
            "minLength": 1,
            "description": "Typed student relationship ref, set at ingest from the resolved actor identity. Semantically references oneroster:user.sourcedId (tenant-carrying composite FK validated by the events ingest hook). Owned by the events module.",
            "x-owned-by": "events"
          },
          "contentId": {
            "type": "string",
            "minLength": 1,
            "description": "Typed content relationship ref, set at ingest. Hook-validated loose ref to content:contentItem.contentId — not a database foreign key. Owned by the events module.",
            "x-owned-by": "events"
          },
          "orgId": {
            "type": "string",
            "minLength": 1,
            "description": "Typed org relationship ref, set at ingest from the resolved org identity. Semantically references oneroster:org.sourcedId (tenant-carrying composite FK validated by the events ingest hook). Owned by the events module.",
            "x-owned-by": "events"
          },
          "rawEvent": {
            "type": "object",
            "description": "The incoming event JSON exactly as received at the API boundary, retained as a JSONB audit payload.",
            "additionalProperties": true
          },
          "canonicalEvent": {
            "type": "object",
            "description": "The normalized canonical JSON event written by the ingest hook for deterministic readback and downstream projection.",
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true,
            "description": "Server-managed row-creation timestamp (RFC 3339). Read-only — never accepted on create or update."
          }
        },
        "required": [
          "id",
          "type",
          "action",
          "actorIri",
          "objectIri",
          "eventTime",
          "envelopeHash",
          "eventCount",
          "actor",
          "object",
          "rawEvent",
          "canonicalEvent",
          "envelopeStatus",
          "createdAt"
        ]
      },
      "CaliperEventEntityLink": {
        "type": "object",
        "properties": {
          "eventIri": {
            "type": "object",
            "description": "A OneRoster GUIDRef to a caliperEvent — the referenced resource's sourcedId, its type, and a dereferenceable href. On write, either this object (its sourcedId is used) or the bare sourcedId string is accepted.",
            "required": [
              "sourcedId",
              "type"
            ],
            "properties": {
              "href": {
                "type": "string",
                "description": "The dereferenceable URL of the referenced resource."
              },
              "sourcedId": {
                "type": "string",
                "description": "The sourcedId of the referenced resource."
              },
              "type": {
                "type": "string",
                "const": "caliperEvent",
                "description": "The referenced resource type."
              }
            }
          },
          "entityIri": {
            "type": "object",
            "description": "A OneRoster GUIDRef to a caliperEntity — the referenced resource's sourcedId, its type, and a dereferenceable href. On write, either this object (its sourcedId is used) or the bare sourcedId string is accepted.",
            "required": [
              "sourcedId",
              "type"
            ],
            "properties": {
              "href": {
                "type": "string",
                "description": "The dereferenceable URL of the referenced resource."
              },
              "sourcedId": {
                "type": "string",
                "description": "The sourcedId of the referenced resource."
              },
              "type": {
                "type": "string",
                "const": "caliperEntity",
                "description": "The referenced resource type."
              }
            }
          },
          "relation": {
            "type": "string",
            "enum": [
              "actor",
              "object",
              "generated",
              "target",
              "edApp",
              "group",
              "membership",
              "referrer",
              "federatedSession",
              "member",
              "item"
            ],
            "description": "The closed Caliper relationship vocabulary naming how the entity participates in the event."
          },
          "rawPath": {
            "type": "string",
            "minLength": 1,
            "description": "The raw JSON-LD path inside the event payload where this entity reference was found. It is part of the natural key so two references to the same entity through different payload paths remain distinct."
          },
          "ordinal": {
            "type": "integer",
            "minimum": 0,
            "description": "The zero-based position of this entity reference within its relation/path collection when the Caliper payload contains repeated linked entities."
          }
        },
        "required": [
          "eventIri",
          "entityIri",
          "relation",
          "rawPath",
          "ordinal"
        ]
      },
      "CaliperSensor": {
        "type": "object",
        "properties": {
          "sensorIri": {
            "type": "string",
            "minLength": 1,
            "description": "The stable IRI that identifies the sending Caliper sensor within the tenant. Together with the tenant id, this is the natural primary key used by incoming envelopes."
          },
          "displayName": {
            "type": "string",
            "minLength": 1,
            "description": "Human-readable name for the registered sensor, used by operators to recognize the sender."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "retired"
            ],
            "description": "Operational status for the sender: `active` sensors may submit envelopes, `paused` sensors are temporarily disabled, and `retired` sensors are retained for historical envelope attribution.",
            "default": "active"
          },
          "credentialRef": {
            "type": "string",
            "minLength": 1,
            "description": "Reference to the managed credential or secret material used to authenticate this sensor. The credential value itself is not stored in the Caliper schema."
          },
          "metadata": {
            "type": "object",
            "description": "Optional implementation metadata about the sensor, such as vendor, deployment, or contact hints that do not change Caliper event identity.",
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true,
            "description": "Server-managed row-creation timestamp (RFC 3339). Read-only — never accepted on create or update."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true,
            "description": "Server-managed last-update timestamp (RFC 3339). Read-only — never accepted on create or update."
          }
        },
        "required": [
          "sensorIri",
          "displayName",
          "status",
          "createdAt",
          "updatedAt"
        ]
      },
      "CreateCaliperSensorRequest": {
        "type": "object",
        "description": "Create-CaliperSensor request body (readOnly/server-managed fields excluded).",
        "required": [
          "sensorIri",
          "displayName",
          "status"
        ],
        "additionalProperties": false,
        "properties": {
          "sensorIri": {
            "type": "string",
            "minLength": 1,
            "description": "The stable IRI that identifies the sending Caliper sensor within the tenant. Together with the tenant id, this is the natural primary key used by incoming envelopes."
          },
          "displayName": {
            "type": "string",
            "minLength": 1,
            "description": "Human-readable name for the registered sensor, used by operators to recognize the sender."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "retired"
            ],
            "description": "Operational status for the sender: `active` sensors may submit envelopes, `paused` sensors are temporarily disabled, and `retired` sensors are retained for historical envelope attribution.",
            "default": "active"
          },
          "credentialRef": {
            "type": "string",
            "minLength": 1,
            "description": "Reference to the managed credential or secret material used to authenticate this sensor. The credential value itself is not stored in the Caliper schema."
          },
          "metadata": {
            "type": "object",
            "description": "Optional implementation metadata about the sensor, such as vendor, deployment, or contact hints that do not change Caliper event identity.",
            "additionalProperties": true
          }
        }
      },
      "UpdateCaliperSensorRequest": {
        "type": "object",
        "description": "Update-CaliperSensor request body (partial — all fields optional; readOnly + immutable fields excluded; null clears a nullable column).",
        "additionalProperties": false,
        "properties": {
          "displayName": {
            "type": "string",
            "minLength": 1,
            "description": "Human-readable name for the registered sensor, used by operators to recognize the sender."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "retired"
            ],
            "description": "Operational status for the sender: `active` sensors may submit envelopes, `paused` sensors are temporarily disabled, and `retired` sensors are retained for historical envelope attribution.",
            "default": "active"
          },
          "credentialRef": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "description": "Reference to the managed credential or secret material used to authenticate this sensor. The credential value itself is not stored in the Caliper schema."
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "description": "Optional implementation metadata about the sensor, such as vendor, deployment, or contact hints that do not change Caliper event identity.",
            "additionalProperties": true
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 7807 problem details.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A URI identifying the problem type."
          },
          "title": {
            "type": "string",
            "description": "A short, human-readable summary of the problem."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code."
          },
          "detail": {
            "type": "string",
            "description": "A human-readable explanation specific to this occurrence."
          },
          "instance": {
            "type": "string",
            "format": "uri",
            "description": "A URI identifying this occurrence."
          }
        },
        "required": [
          "title",
          "status"
        ]
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "servers": [
    {
      "url": "https://caliper.platform4.alphaschool.dev"
    }
  ]
}
