{
  "module": "caliper",
  "surface": "1edtech",
  "baseUrls": {
    "expression": "https://platform3-andymontgomery-9773s-projects.vercel.app/caliper/1edtech/implementation/api",
    "canonicalTarget": "https://platform3-andymontgomery-9773s-projects.vercel.app/caliper/1edtech/implementation/api",
    "env": "CALIPER_BASE_URL",
    "reviewerTokenEnv": "CALIPER_REVIEWER_JWT",
    "demoTenantId": "00000000-0000-4000-8000-00000000ca12"
  },
  "endpoints": [
    {
      "operationId": "mintDemoToken",
      "kind": "Demo helper",
      "method": "POST",
      "path": "/dev/mint?tenantId={tenantId}",
      "rootPath": "/api/dev/mint?tenantId={tenantId}",
      "statusCodes": [
        "200",
        "400",
        "403"
      ],
      "requestSchema": [
        {
          "name": "tenantId",
          "in": "Query",
          "type": "uuid",
          "required": "Required",
          "description": "Use the seeded demo platform tenant UUID 00000000-0000-4000-8000-00000000ca12. Malformed or missing values return 400; real-tenant tokens are not minted by this helper."
        },
        {
          "name": "Authorization",
          "in": "Header",
          "type": "none",
          "required": "Not allowed",
          "description": "No bearer token is required for public demo token minting. Real-tenant JWTs are minted by operators or by the loop driver."
        }
      ],
      "responseSchema": [
        {
          "name": "token",
          "type": "string",
          "required": "Required",
          "description": "HS256 bearer token signed with the platform secret and scoped to the demo tenant."
        },
        {
          "name": "tenantId",
          "type": "uuid",
          "required": "Required",
          "description": "The seeded demo tenant UUID 00000000-0000-4000-8000-00000000ca12."
        },
        {
          "name": "role",
          "type": "string",
          "required": "Required",
          "description": "Demo role, usually sensor_writer or reviewer."
        },
        {
          "name": "sensorIri",
          "type": "IRI",
          "required": "Required for quickstart",
          "description": "Seeded active sensor IRI accepted by POST /caliper/v1p2/events for the demo tenant."
        },
        {
          "name": "expiresAt",
          "type": "date-time",
          "required": "Required",
          "description": "Expiration time after which the token returns 401."
        }
      ],
      "traces": [
        "itd:citd-006-sensor-registration-tenant-auth"
      ]
    },
    {
      "operationId": "persistCaliperEnvelope",
      "kind": "Caliper Sensor API",
      "method": "POST",
      "path": "/caliper/v1p2/events",
      "rootPath": null,
      "statusCodes": [
        "204",
        "400",
        "401",
        "403",
        "409",
        "415",
        "422",
        "500"
      ],
      "requestSchema": [
        {
          "name": "Authorization",
          "in": "Header",
          "type": "Bearer JWT",
          "required": "Required",
          "description": "Signed, unexpired platform token. tenantId must match X-Timeback-Tenant."
        },
        {
          "name": "X-Timeback-Tenant",
          "in": "Header",
          "type": "tenant uuid",
          "required": "Required",
          "description": "Tenant boundary for the envelope, sensor lookup, and all normalized rows. Use the tenantId returned by demo minting or the tenantId claim from a real-tenant JWT."
        },
        {
          "name": "Content-Type",
          "in": "Header",
          "type": "application/json",
          "required": "Required",
          "description": "Only JSON Sensor API envelopes are accepted by this HTTP surface."
        },
        {
          "name": "Idempotency-Key",
          "in": "Header",
          "type": "string",
          "required": "Optional",
          "description": "Optional platform retry key. Duplicate Caliper delivery is still deduplicated by canonical envelope/event hashes."
        },
        {
          "name": "sensor",
          "in": "Body",
          "type": "IRI or Sensor entity",
          "required": "Required",
          "description": "Caliper envelope sensor. It must resolve to an active caliper.sensor row for the tenant."
        },
        {
          "name": "sendTime",
          "in": "Body",
          "type": "date-time",
          "required": "Required",
          "description": "Caliper envelope sendTime supplied by the sender."
        },
        {
          "name": "dataVersion",
          "in": "Body",
          "type": "IRI",
          "required": "Required",
          "description": "Must be http://purl.imsglobal.org/ctx/caliper/v1p2 for this surface."
        },
        {
          "name": "data[]",
          "in": "Body",
          "type": "array<Event|Entity>",
          "required": "Required",
          "description": "At least one Event or described Entity. Event rows require id, type, actor, action, object, and eventTime."
        }
      ],
      "responseSchema": [
        {
          "name": "body",
          "type": "empty",
          "required": "Always empty on success",
          "description": "204 No Content has no JSON body. Use the read projections for envelope/event evidence."
        },
        {
          "name": "raw_envelope",
          "type": "jsonb",
          "required": "Persisted, not returned",
          "description": "Original JSON-LD envelope is stored as the interchange authority."
        },
        {
          "name": "envelope_hash",
          "type": "sha256",
          "required": "Persisted, not returned",
          "description": "Canonical envelope hash used for duplicate detection and read projection lookup."
        }
      ],
      "traces": [
        "itd:citd-004-jsonld-envelope-authority",
        "itd:citd-007-envelope-first-ingest",
        "itd:citd-008-canonical-hashes-idempotency",
        "itd:citd-011-sensor-api-response-errors"
      ]
    },
    {
      "operationId": "getCaliperEventProjection",
      "kind": "TimeBack read projection",
      "method": "GET",
      "path": "/caliper/v1p2/events?eventIri={eventIri}",
      "rootPath": null,
      "statusCodes": [
        "200",
        "401",
        "403",
        "404",
        "500"
      ],
      "requestSchema": [
        {
          "name": "Authorization",
          "in": "Header",
          "type": "Bearer JWT",
          "required": "Required",
          "description": "Token tenantId must match X-Timeback-Tenant."
        },
        {
          "name": "X-Timeback-Tenant",
          "in": "Header",
          "type": "tenant id",
          "required": "Required",
          "description": "Tenant boundary for the event lookup. Must match the token tenantId claim."
        },
        {
          "name": "eventIri",
          "in": "Query",
          "type": "IRI",
          "required": "Required",
          "description": "Caliper Event id supplied in the original event object."
        }
      ],
      "responseSchema": [
        {
          "name": "eventRowId",
          "type": "uuid",
          "required": "Required",
          "description": "Platform row identifier for the normalized event."
        },
        {
          "name": "envelopeId",
          "type": "uuid",
          "required": "Required",
          "description": "Envelope row that carried the event."
        },
        {
          "name": "eventIri",
          "type": "IRI",
          "required": "Required",
          "description": "Caliper event id."
        },
        {
          "name": "eventType",
          "type": "enum",
          "required": "Required",
          "description": "Caliper Event subclass such as AssessmentItemEvent."
        },
        {
          "name": "profile",
          "type": "enum",
          "required": "Required",
          "description": "Caliper profile. If omitted by the sender, this projection may infer it from event type without changing raw_event."
        },
        {
          "name": "action",
          "type": "enum",
          "required": "Required",
          "description": "Caliper action term."
        },
        {
          "name": "eventTime",
          "type": "date-time",
          "required": "Required",
          "description": "Learning activity timestamp."
        },
        {
          "name": "rawEvent",
          "type": "json",
          "required": "Required",
          "description": "Original Caliper JSON-LD event. Returned only to the authenticated tenant."
        },
        {
          "name": "eventHash",
          "type": "sha256",
          "required": "Required",
          "description": "Canonical event hash used for replay evidence."
        }
      ],
      "traces": [
        "itd:citd-009-event-entity-relational-projection",
        "itd:citd-012-operational-read-projections"
      ]
    },
    {
      "operationId": "getCaliperEnvelopeProjection",
      "kind": "TimeBack read projection",
      "method": "GET",
      "path": "/caliper/v1p2/envelopes?hash={envelopeHash}",
      "rootPath": null,
      "statusCodes": [
        "200",
        "401",
        "403",
        "404",
        "500"
      ],
      "requestSchema": [
        {
          "name": "Authorization",
          "in": "Header",
          "type": "Bearer JWT",
          "required": "Required",
          "description": "Token tenantId must match X-Timeback-Tenant."
        },
        {
          "name": "X-Timeback-Tenant",
          "in": "Header",
          "type": "tenant id",
          "required": "Required",
          "description": "Tenant boundary for envelope lookup. Must match the token tenantId claim."
        },
        {
          "name": "hash",
          "in": "Query",
          "type": "sha256",
          "required": "Required",
          "description": "Canonical envelope hash."
        }
      ],
      "responseSchema": [
        {
          "name": "envelopeId",
          "type": "uuid",
          "required": "Required",
          "description": "Platform identifier for the received envelope."
        },
        {
          "name": "sensorIri",
          "type": "IRI",
          "required": "Required",
          "description": "Caliper envelope sensor IRI."
        },
        {
          "name": "dataVersion",
          "type": "IRI",
          "required": "Required",
          "description": "Accepted Caliper context/data version."
        },
        {
          "name": "sendTime",
          "type": "date-time",
          "required": "Required",
          "description": "Sender supplied envelope sendTime."
        },
        {
          "name": "receivedAt",
          "type": "date-time",
          "required": "Required",
          "description": "Platform receipt timestamp."
        },
        {
          "name": "envelopeStatus",
          "type": "enum",
          "required": "Required",
          "description": "received, processed, rejected, or duplicate."
        },
        {
          "name": "events[]",
          "type": "array",
          "required": "Required",
          "description": "Event summaries normalized from this envelope."
        }
      ],
      "traces": [
        "itd:citd-007-envelope-first-ingest",
        "itd:citd-008-canonical-hashes-idempotency",
        "itd:citd-012-operational-read-projections"
      ]
    }
  ],
  "statusCodes": [
    {
      "code": "200",
      "name": "OK",
      "applies": [
        "getCaliperEventProjection",
        "getCaliperEnvelopeProjection",
        "mintDemoToken"
      ],
      "meaning": "The read or demo helper request succeeded and returned JSON."
    },
    {
      "code": "204",
      "name": "No Content",
      "applies": [
        "persistCaliperEnvelope"
      ],
      "meaning": "The Sensor API envelope was accepted or safely deduplicated; no JSON body is returned."
    },
    {
      "code": "400",
      "name": "Bad Request",
      "applies": [
        "persistCaliperEnvelope",
        "mintDemoToken"
      ],
      "meaning": "Malformed JSON, missing required envelope fields, empty data array, invalid tenantId format, or invalid query syntax."
    },
    {
      "code": "401",
      "name": "Unauthorized",
      "applies": [
        "persistCaliperEnvelope",
        "getCaliperEventProjection",
        "getCaliperEnvelopeProjection"
      ],
      "meaning": "Missing, expired, malformed, or wrongly signed bearer token."
    },
    {
      "code": "403",
      "name": "Forbidden",
      "applies": [
        "persistCaliperEnvelope",
        "getCaliperEventProjection",
        "getCaliperEnvelopeProjection",
        "mintDemoToken"
      ],
      "meaning": "The token is valid but not authorized for the X-Timeback-Tenant value or the envelope sensor, or /dev/mint was called for a tenant that the public demo helper is not allowed to mint."
    },
    {
      "code": "404",
      "name": "Not Found",
      "applies": [
        "getCaliperEventProjection",
        "getCaliperEnvelopeProjection"
      ],
      "meaning": "No event or envelope exists for this tenant and identifier."
    },
    {
      "code": "409",
      "name": "Conflict",
      "applies": [
        "persistCaliperEnvelope"
      ],
      "meaning": "The optional Idempotency-Key was reused with a different payload, method, path, tenant, module, or surface."
    },
    {
      "code": "415",
      "name": "Unsupported Media Type",
      "applies": [
        "persistCaliperEnvelope"
      ],
      "meaning": "Content-Type is not application/json."
    },
    {
      "code": "422",
      "name": "Unprocessable Content",
      "applies": [
        "persistCaliperEnvelope"
      ],
      "meaning": "The JSON shape parses, but dataVersion, event type, action, entity type, timestamp, profile, extension, or privacy validation fails."
    },
    {
      "code": "500",
      "name": "Server Error",
      "applies": [
        "persistCaliperEnvelope",
        "getCaliperEventProjection",
        "getCaliperEnvelopeProjection"
      ],
      "meaning": "Unexpected platform failure. Problem JSON stays redacted and never includes learner payloads or tokens."
    }
  ]
}