# Canonical Events Alpha Surface

Use this file after the skill activates when you need surface details. Always prefer the hosted customer website and data dictionary if they differ from this file.

## Hosted Sources

- Customer website: https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/customer_website/
- Data dictionary: https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/data_dictionary/
- Architecture: https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/architecture/
- Surface QC: https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/surface_qc/
- Integration app: https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/integration/
- Live API base: https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/implementation/api
- People & Orgs customer website: https://platform3-andymontgomery-9773s-projects.vercel.app/people_and_orgs/alpha/customer_website/
- People & Orgs live API base: https://platform3-andymontgomery-9773s-projects.vercel.app/people_and_orgs/alpha/implementation/api

Events Alpha is a plain-language alias over the approved Caliper 1EdTech persistence. The approved Events Alpha docs win over older Caliper source material for this surface.

## Credentials

Demo tenant:

- Tenant id: `demo`
- Demo mint: `POST $EVENTS_BASE_URL/dev/mint?tenantId=demo`
- Demo activity source id returned by mint: `2fb3d3d9-a9f0-49ac-9ef7-5d2d1d0b1001`
- Demo token scope: tenant-scoped demo read/write. The mint response includes the Events identity boundary contract.
- The public Events demo uses `student-ada-001` as the seeded Events student id and `content-fractions-video-01` as the seeded content id for deterministic examples. Use those values for the bundled demo reads, writes, source imports, attendance reads, and discipline reads unless an operator gives you different ids.
- Do not use `person_ada_lovelace` in Events demo calls; that id belongs to the People & Orgs demo roster, not the Events seeded fixtures.

Real or reviewer tenant:

- Use the operator-supplied bearer token, usually `EVENTS_REVIEWER_JWT` in platform3 loop contexts.
- Use the supplied tenant id or the JWT `tenantId` / `tenant_id` claim as `X-Timeback-Tenant`.
- For teacher-facing reports that show student names, use a token that can read both `events:read` and `people_orgs:read`, or use separately supplied tokens for each surface. Resolve the Events `studentId` in People & Orgs only when the tenant uses People & Orgs `person_id` values as Events student ids. Do not store either token in the skill pack.
- Do not call `/dev/mint` for real tenants.

## Learning-App Identity Boundary

- Write activity moments with the same `studentId` and `contentId` used by the Content/Results lesson attempt. Do not substitute Events-only demo identities or carry the real content id only in `extensions`.
- A token without `studentIds`, `orgIds`/`schoolIds`, or `contentIds` claims is tenant-scoped. A token carrying any of those claims is boundary-scoped and Events enforces the supplied values on reads and writes.
- A valid token used outside an explicit boundary returns HTTP `403`, code `events:boundary_rejected`, and a field error naming the rejected boundary. Authentication, tenant mismatch, and clean absence remain separate failures.

## Endpoints

| Operation | Method and path | Success | Required proof |
| --- | --- | --- | --- |
| Demo token mint | `POST /dev/mint?tenantId=demo` | `200` JSON with token, tenant id, and default activity source id | The tenant id is exactly `demo`. |
| Import source rows | `POST /source-imports` | `200` JSON with `acceptedCount`, `materializedCount`, readable rows or links | Records are source-shaped, server-normalized, and accepted rows are immediately readable through public list/detail views. |
| Create events | `POST /events` | `202` JSON with accepted event projections | Capture returned event ids; do not compute ids or dedupe locally. |
| List events | `GET /events` | `200` JSON activity stream | Use documented filters and cursor values returned by the surface. |
| Get event | `GET /events/{eventId}` | `200` JSON detail | Use an event id returned by `POST /events` or `GET /events`. |
| Register activity source | `POST /activity-sources` | `201` on create; `200` with the same UUID on identical replay | Requires `events:sources:write` or an approved source/operator role. Send only `sourceIri` and `name`; the platform generates `id`. |
| List activity sources | `GET /activity-sources` | `200` JSON list | Optional `status`, `cursor`, and `pageSize` query parameters only. |
| Get activity source | `GET /activity-sources/{sourceId}` | `200` JSON detail | Use a source id returned by mint, event, or activity-source list. |
| List attendance moments | `GET /attendance-events` | `200` JSON list | Read-only Ed-Fi canonical attendance view; use documented filters and returned cursors. |
| Get attendance moment | `GET /attendance-events/{attendanceEventId}` | `200` JSON detail | Use an id returned by the attendance list; do not write attendance through Events Alpha. |
| List discipline moments | `GET /discipline-events` | `200` JSON list | Read-only Ed-Fi canonical discipline view; raw narratives and drafts are not public. |
| Get discipline moment | `GET /discipline-events/{disciplineEventId}` | `200` JSON detail | Use an id returned by the discipline list; do not write discipline through Events Alpha. |
| Resolve student name | `GET /people/{personId}` on People & Orgs Alpha | `200` JSON person detail | Use `event.studentId` as the People & Orgs `personId`; do not parse names from event labels. |

Headers for writes and reads:

- `Authorization: Bearer <token>`
- `X-Timeback-Tenant: <tenant id>`
- `Content-Type: application/json` for `POST /events`
- `Content-Type: application/json` for `POST /activity-sources`
- Optional `Idempotency-Key` for retry safety on `POST /events`
- `Content-Type: application/json` and `Idempotency-Key` for retry safety on `POST /source-imports`

## Activity Source Registration

Use the Events registry before a new producer sends its first event. Registration is tenant-scoped and privilege-gated; the public demo token cannot create sources.

```bash
REGISTERED=$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS \
  -X POST "$EVENTS_BASE_URL/activity-sources" \
  -H "Authorization: Bearer $EVENTS_SOURCE_ADMIN_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID" \
  -H "Content-Type: application/json" \
  --data '{"sourceIri":"https://producer.example.edu","name":"Producer Web"}')
ACTIVITY_SOURCE_ID=$(printf '%s' "$REGISTERED" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>process.stdout.write(JSON.parse(s).data.id))')
```

The platform generates `activitySource.id`; apps must store the returned UUID and send it as `activitySourceId`. `(tenantId, sourceIri)` is the idempotent natural key: repeating the same registration returns `200`, header `Idempotency-Replayed: true`, and the original UUID. Reusing an IRI with a different name returns `409 resource_conflict`. The request rejects caller-supplied `id`, `credentialRef`, `metadata`, tokens, and any undocumented field.

## Event Write Shape

Use the public Alpha field names documented in the data dictionary. The current implementation accepts a flat school-language event with `kind`, `studentId`, `contentId`, `activitySourceId`, and `happenedAt` / `occurredAt`; the server derives Caliper `eventType`, `profile`, `action`, actor/object links, and relationship projections when they are omitted. A compatibility request may also supply:

- `sourceEventId`
- `eventType`
- `profile`
- `action`
- `actorRef`
- `objectRef`
- optional governed `extensions`

Allowed values and field meanings live in the hosted data dictionary. Do not copy those lists into app code or this skill. Use the docs when composing a request, and let Problem JSON explain rejected values.
Raw event payloads remain cut from the public Alpha contract and response.

The first-party player namespace is governed. Keys under
`https://platform3.timeback.com/player/*` must be from
`alpha.policy.events.player_extension_registry.v2026-07-18.issue-1128`; current governed
keys include `renderer_ref`, `interaction_id`, `component_id`,
`component_type`, `question_id`, `rendered_content_id`, `response_identifier`,
`response_value`, `response_label`, `first_attempt`, `attempt_ordinal`,
`attempt_count`, `effort_signals`, `response_time_ms`, `hint_usage`,
`occurred_at`, and `idempotency_key`. Unknown player keys are rejected instead
of being accepted opaquely. Use `response_time_ms`, `attempt_count` /
`attempt_ordinal`, and `hint_usage` exactly; do not invent local aliases such
as `response_latency`.

## Source Import Contract

Use `POST /source-imports` only for migration/reconciliation jobs that send raw TimeBack Production or Horizons SIS records. It is a surface call, not a client-side normalizer.

Request fields:

- `sourceSystem`: `timeback_production` or `horizons_sis`.
- `adapter`: `timeback_learning_event_v1`, `horizons_attendance_event_v1`, or `horizons_discipline_event_v1`.
- `importId`: optional retry/correlation label for the source cut.
- `records`: 1..1000 source-shaped JSON rows exactly as the producer emits them.
- `dryRun`: optional boolean; `false` by default.

For TimeBack Production learning activity, a valid source-shaped row can be a processed-facts record with producer fields such as `id`, `user_id`, `date`, `subject`, `app`, `course_id`, `activity_id`, `total_questions`, `correct_questions`, `xp_earned`, `mastered_units`, `active_seconds`, `source_system`, `score_type`, `score_given`, and `max_score`. Do not add caller-derived fields such as `activity_kind`, `kind`, `eventType`, `profile`, `action`, `actorRef`, `objectRef`, or source-course aliases; the Events source-adapter policy owns that classification.

Success rule:

- Non-dry-run success is HTTP `200` only when `acceptedCount == materializedCount` and accepted rows are immediately readable through `GET /events`, `GET /attendance-events`, or `GET /discipline-events`.
- `dryRun: true` may return `materializedCount: 0` because it intentionally writes nothing.
- `events:validation_failed` returns HTTP `400`.
- `events:adapter_rejected` returns HTTP `422`.
- A `200` response containing a Problem body, or accepted rows that do not materialize into public read rows, is a surface failure.

Client rules:

- Do not rewrite source rows into Alpha event objects before calling the endpoint.
- Do not compute Caliper tuple fields, Alpha kind, People & Orgs joins, Content joins, descriptor codes, soft-delete state, dedupe hashes, minute buckets, score/mastery facts, or event/result boundaries.
- Do not copy source-field synonym logic into the pack or an app. The hosted data dictionary / Events source-adapter policy names the adapter contract; implementation only executes that policy.
- After success, verify materialization through the public list/detail endpoint for the object family.

## Query Parameters

`GET /events` supports documented filters for:

- `studentId`
- `contentId`
- `orgId`
- `activitySourceId`
- `kind`
- `action`
- `eventType`
- `happenedAtFrom`
- `happenedAtTo`
- `modifiedSince`
- `cursor`
- `pageSize`

Clients must not parse cursor values. Use the cursor returned by the surface as an opaque string.

## Poll Cadence And Backoff

Events Alpha is poll-only today. There is no outbound webhook, SSE stream, push stream, or Platform notifications module for Events. Use `modifiedSince`, returned opaque cursors, and typed filters to keep activity views fresh.

- Foreground activity feeds and same-screen progress views: poll every 15 seconds while visible.
- Background tabs, freshness badges, and non-critical dashboards: poll every 60 seconds.
- Idle clients or server jobs with no visible user waiting: poll every 5 minutes.
- After this client writes events or imports: read immediately, then retry after 2, 5, and 10 seconds. Stop after 30 seconds and show pending or unknown state.

Share one poller per tenant and filter set. Add 10–20 percent random jitter. Keep the last successful `modifiedSince` high-watermark, drain every returned cursor before advancing it, and de-duplicate replayed rows by returned event id.

Retry network failures, `429`, and `5xx` with exponential backoff at 15 seconds, 30 seconds, 60 seconds, 2 minutes, then a 5-minute cap. Reset after one successful page drain. Do not loop on `400`, `401`, `403`, `404`, `409`, `412`, `415`, or `422`; surface the Problem JSON and fix the request.

`GET /attendance-events` supports documented filters for `studentId`, `schoolId`, `classId`, `category`, `from`, `to`, `cursor`, and `pageSize`.

`GET /discipline-events` supports documented filters for `studentId`, `schoolId`, `behavior`, `participationCode`, `location`, `from`, `to`, `cursor`, and `pageSize`.

For both administrative moment lists, `from` and `to` are date filters at the surface-owned event date grain. Do not translate them into current-enrollment filters or raw Ed-Fi update timestamps.

## Attendance And Discipline Reads

Attendance and discipline belong in Events Alpha only as moments that happened. They are read-only school-language projections over the Ed-Fi 1EdTech base surface:

- Attendance reads use `attendanceEvent` rows sourced from Ed-Fi canonical attendance records.
- Discipline reads use `disciplineEvent` rows sourced from Ed-Fi canonical discipline records.
- Ed-Fi owns writes, drafts, canonical acknowledgements, descriptors, soft-delete state, and source keys.
- Events Alpha exposes safe list/detail reads, redacts raw payloads and discipline narratives, and documents the raw/API convergence rules.

Agents must not ship local attendance-category tables, behavior/location/participation descriptor tables, draft recovery logic, soft-delete dedup logic, platform-id reconciliation, or raw Ed-Fi payload parsing. If a report needs one of those operations, use the Ed-Fi base surface or report a surface gap instead of adding hidden code to the app.

## Cross-Module Student Names

Events owns the activity moment and returns `studentId` when the surface resolved or accepted a student relationship. People & Orgs owns the roster name. A teacher report that says "what apps did Mary use today?" should therefore resolve the returned Events `studentId` through People & Orgs when the tenant's identity boundary is aligned.

In the public Events demo, the seeded Events student id is `student-ada-001`. The demo proof prints that id as the student label and does not require a People & Orgs lookup. In real or reviewer tenants, prefer the People & Orgs `person_id` as the Events `studentId` so reports can resolve names cleanly.

1. Ask Events for activity: `GET /events?studentId={studentId}&happenedAtFrom={startOfDayIso}&pageSize=50`.
2. If the tenant uses People & Orgs person ids for Events students, ask People & Orgs for the name: `GET /people/{studentId}`.
3. Join those two returned records in presentation only.

Do not infer a student from `actorRef.id`, `objectRef.name`, an app URL, a source-specific label, or free text. If the teacher gives only a name, use the People & Orgs roster API under the teacher's scope to choose a `person_id`; if there are multiple matches, ask for the intended student row.

## Status and Error Semantics

- `200`: mint or read succeeded.
- `201`: a new activity source was registered.
- `200`: source import succeeded only when non-dry-run accepted rows are materialized and readable.
- `202`: event batch accepted or safely replayed.
- `400`: malformed request, invalid query, invalid cursor, invalid JSON, or demo mint shape error.
- `401`: missing, expired, malformed, or wrongly signed bearer token.
- `403`: token/tenant mismatch or insufficient authority.
- `404`: no tenant-owned event or activity source for the supplied id.
- `409`: optional `Idempotency-Key` reused with different request content, or an activity-source IRI conflicts with different registration fields.
- `412`: mutable-resource precondition sent to immutable event write.
- `415`: POST content type is not JSON.
- `422`: parsed JSON violates documented field values, kinds, timestamps, relationship constraints, or a named source adapter rejects one or more source rows.

Errors use Problem JSON with stable top-level `code`, `status`, `title`, `detail`, `requestId`, `traceId`, and `fieldErrors`.

## Deferred or Unshipped Behavior

Do not promise these:

- No event updates or deletes.
- No local event-kind inference, local source-trust rules, or local relationship resolver.
- No source-import pre-normalization, source-field synonym maps, descriptor mapping, or adapter workaround logic.
- No raw Caliper payload, canonical JSON, hash, credential, or secret exposure.
- No attendance or discipline writes through Events Alpha.
- No Ed-Fi draft rows, descriptor tables, soft-delete reconciliation, raw payloads, or discipline narratives in public Events outputs.
- No Results, score, mastery, report-card, content-effectiveness, advancement, or minutes math.
- No outbound webhook or push stream. Use `modifiedSince` polling.
- No direct list endpoint for `eventThing`, `eventLink`, `eventBatch`, or `trustEvidence`.
- No official 1EdTech certification claim from the Alpha surface.

When a user asks for one of these, report that the surface has not committed to it and link to the relevant architecture decision in the hosted architecture page.
