---
name: qti-1edtech-surface
description: Use when building apps, reports, QA probes, or deliverables against the platform3 QTI 1EdTech live API. The skill guides agents through demo and real-tenant credentials, QTI package ingest, delivery and authoring JSON reads, XML export, delivery sessions, attempt submission, candidate runtime-data review/deletion, and conformance runs without reimplementing QTI parsing, scoring, schema validation, deduplication, or business rules.
---

# QTI 1EdTech Surface

Use this skill to build on the platform3 QTI 1EdTech API as an outside integrator. The served audience is a developer building a Pearson-Assessment-shaped assessment authoring and delivery product on top of the QTI 1EdTech surface alone.

## Canonical Sources

Always ground behavior in the live docs before acting:

- Customer website: <https://platform3-andymontgomery-9773s-projects.vercel.app/qti/1edtech/customer_website/#quickstart>
- API contracts: <https://platform3-andymontgomery-9773s-projects.vercel.app/qti/1edtech/customer_website/#api-contracts>
- Data dictionary: <https://platform3-andymontgomery-9773s-projects.vercel.app/qti/1edtech/data_dictionary/>
- Implementation root: <https://platform3-andymontgomery-9773s-projects.vercel.app/qti/1edtech/implementation/api>

If these docs and this skill disagree, the canonical docs win. Refresh the API descriptor with:

```sh
curl -fsSL "$QTI_BASE_URL" | jq '.quickstart.operationOrder, .endpoints, .links'
```

## Install Targets

Use the same `SKILL.md` on all supported agents. The hosted pack also includes platform-specific operator notes:

- Claude Code: read `agents/claude-code.md`; install under `~/.claude/skills/qti-1edtech-surface/`.
- Codex CLI: read `agents/codex-cli.md`; install under `${CODEX_HOME:-$HOME/.codex}/skills/qti-1edtech-surface/`.
- Perplexity Computer: read `agents/perplexity-computer.md`; upload `SKILL.md` directly or upload a zip with `SKILL.md` at the root.

When producing a customer-facing output, load the relevant example template:

- `examples/assessment-app-spec.md` for an item-bank, delivery-session, results, privacy-cleanup, and conformance evidence app.
- `examples/qa-report-template.md` for an evidence report that cites canonical docs and redacts tokens.
- `checks/qti-surface-check.sh` for the runnable proof that this pack can reproduce the platform's own demo answers by surface calls only.

## Leak Guard

This skill is instructions only. Do not add or rely on:

- A QTI XML parser, schema parser, response-processing engine, scoring engine, hash/dedup implementation, retry ledger, enum/norm table, or database schema copy.
- Local business rules for `RESPONSE`, `ChoiceA`, variable cardinality, QTI processing, candidate privacy, idempotency replay, ETag conflict handling, or conformance.
- Generic search or paging behavior beyond the live descriptor. Use only the documented `listPackages`, `listArtifacts`, and `listArtifactVersions` endpoints; do not invent filters, joins, or client-side query semantics.

If a task requires one of those capabilities outside the API, stop and report a surface leak. The earliest likely upstream owners are architecture for an uncommitted capability, data dictionary for missing field meaning, or implementation for a published behavior that the live API does not perform.

## Credentials

Set the canonical base URL. Do not use a Vercel deploy-hash URL.

```sh
export QTI_BASE_URL="https://platform3-andymontgomery-9773s-projects.vercel.app/qti/1edtech/implementation/api"
export BASE_URL="$QTI_BASE_URL"
```

For a cold demo, mint a public demo token:

```sh
DEMO_TOKEN_RESPONSE=$(curl -fsSL -X POST "$BASE_URL/dev/mint?tenantId=demo")
export TOKEN="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.token')"
export TENANT_ID="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.tenantId')"
export CANDIDATE_REF="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.demoCandidateRef')"
export DEMO_PACKAGE_ZIP_URL="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.demoPackageZipUrl')"
```

For a real tenant, use the operator-minted JWT supplied to you:

```sh
export TOKEN="${QTI_REVIEWER_JWT:?operator must provide a scoped QTI token}"
export TENANT_ID="${QTI_REVIEWER_TENANT_ID:?operator must provide the tenant id}"
export CANDIDATE_REF="${QTI_CANDIDATE_REF:?operator must provide a pseudonymous candidate ref}"
```

`TENANT_ID` may be the canonical `platform.tenant.tenant_id` UUID or the tenant's `tenant_key`. The path value and JWT tenant claim must resolve to the same tenant.

Credential safety rules:

- Never print, commit, screenshot, or report raw JWTs. Show only `tokenType`, `tenantId`, expiry, and the first six characters of a token hash if an audit needs a correlation handle.
- Treat demo tokens as public-demo only; never reuse the demo token path for real tenants.
- Keep real-tenant tokens in environment variables or the agent's secret store, not in generated source files.
- If a token is exposed, stop the task and ask the operator to rotate it before continuing against that tenant.

## Workflow Rules

Use the API operations in the documented order:

1. `mintDemoToken` only for the public demo tenant.
2. `ingestContentPackage` with `Idempotency-Key`, `Content-Type: application/zip`, and `X-QTI-Profile: qti-3.0`.
3. `listPackages`, `listArtifacts`, and `listArtifactVersions` only when a customer workflow needs the documented collection views.
4. `getDeliveryJson` for delivery-safe rendering.
5. `getAuthoringJson` for the lossless read-before-write envelope and `ETag`.
6. `saveAuthoringJson` with the exact `If-Match` value. For smoke runs, writing back the authoring response unchanged is valid and avoids local JSON transformations.
7. `exportXml` from the saved immutable artifact version.
8. `startDeliverySession` with a pseudonymous UUID candidate reference.
9. `submitAttempt`; let the API validate response shape and compute score.
10. `getCandidateRuntimeData` to review one candidate-scoped runtime record.
11. `deleteCandidateRuntimeData` to remove learner runtime rows only.
12. `runConformance` with `{}`; let the API choose the configured QTI source bundle.

## Runnable Proof Check

Run the bundled check before building an app or report:

```sh
checks/qti-surface-check.sh
```

The check calls the live public demo surface, then fails unless the platform returns its own pinned answers: `outcomeState.SCORE` is `1`, the processing trace is `match-correct`, XML export is nonempty, runtime-data cleanup returns `204`, and conformance passes with zero failed/error assertions. It also calls the documented list endpoints and verifies their response envelope shape. The script stores redacted evidence in a temp directory and prints a summary JSON with `jwt: "redacted"`.

For a real tenant, supply operator credentials through environment variables:

```sh
QTI_TOKEN="$QTI_REVIEWER_JWT" \
QTI_TENANT_ID="$QTI_REVIEWER_TENANT_ID" \
QTI_CANDIDATE_REF="$QTI_CANDIDATE_REF" \
checks/qti-surface-check.sh
```

Do not edit the check to compute QTI results. If the check cannot pass without adding parsing, scoring, schema validation, deduping, or response-processing logic, report a surface leak.

## End-To-End Surface Example

This example creates a demo package ingest, verifies authoring and delivery reads, writes authoring JSON back unchanged with `If-Match`, exports XML, starts a session, submits a response, reads then deletes candidate runtime data, and runs conformance. Local shell only stores returned identifiers and checks surface-returned values.

```sh
set -euo pipefail

export QTI_BASE_URL="https://platform3-andymontgomery-9773s-projects.vercel.app/qti/1edtech/implementation/api"
export BASE_URL="$QTI_BASE_URL"
export RUN_ID="skill-$(date -u +%Y%m%dT%H%M%SZ)-$$"
export WORK_DIR="${TMPDIR:-/tmp}/qti-skill-$RUN_ID"
mkdir -p "$WORK_DIR"

DEMO_TOKEN_RESPONSE=$(curl -fsSL -X POST "$BASE_URL/dev/mint?tenantId=demo")
export TOKEN="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.token')"
export TENANT_ID="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.tenantId')"
export CANDIDATE_REF="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.demoCandidateRef')"
export DEMO_PACKAGE_ZIP_URL="$(printf '%s' "$DEMO_TOKEN_RESPONSE" | jq -r '.demoPackageZipUrl')"

curl -fsSL "$BASE_URL$DEMO_PACKAGE_ZIP_URL" -o "$WORK_DIR/qti-package.zip"

curl -fsSL -X POST "$BASE_URL/tenants/$TENANT_ID/qti/packages" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: pkg-upload-$RUN_ID" \
  -H "Content-Type: application/zip" \
  -H "X-QTI-Profile: qti-3.0" \
  --data-binary @"$WORK_DIR/qti-package.zip" \
  -o "$WORK_DIR/qti-ingest.json"

export ARTIFACT_ID="$(jq -r '.artifacts[0].artifactId' "$WORK_DIR/qti-ingest.json")"
export ARTIFACT_VERSION_ID="$(jq -r '.artifacts[0].artifactVersionId' "$WORK_DIR/qti-ingest.json")"

curl -fsSL "$BASE_URL/tenants/$TENANT_ID/qti/packages?limit=5" \
  -H "Authorization: Bearer $TOKEN" \
  -o "$WORK_DIR/qti-packages-list.json"

curl -fsSL "$BASE_URL/tenants/$TENANT_ID/qti/artifacts?limit=5" \
  -H "Authorization: Bearer $TOKEN" \
  -o "$WORK_DIR/qti-artifacts-list.json"

curl -fsSL "$BASE_URL/tenants/$TENANT_ID/qti/artifact-versions?limit=5" \
  -H "Authorization: Bearer $TOKEN" \
  -o "$WORK_DIR/qti-artifact-versions-list.json"

curl -fsSL "$BASE_URL/tenants/$TENANT_ID/qti/artifact-versions/$ARTIFACT_VERSION_ID/delivery-json" \
  -H "Authorization: Bearer $TOKEN" \
  -o "$WORK_DIR/qti-delivery.json"

curl -fsSL -D "$WORK_DIR/qti-authoring-read.headers" \
  "$BASE_URL/tenants/$TENANT_ID/qti/artifacts/$ARTIFACT_ID/authoring-json" \
  -H "Authorization: Bearer $TOKEN" \
  -o "$WORK_DIR/qti-authoring-read.json"

export ETAG="$(awk 'BEGIN{IGNORECASE=1} /^etag:/ {sub(/\r$/, "", $2); print $2}' "$WORK_DIR/qti-authoring-read.headers")"

curl -fsSL -X PUT "$BASE_URL/tenants/$TENANT_ID/qti/artifacts/$ARTIFACT_ID/authoring-json" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "If-Match: $ETAG" \
  --data-binary @"$WORK_DIR/qti-authoring-read.json" \
  -o "$WORK_DIR/qti-save.json"

export SAVED_ARTIFACT_VERSION_ID="$(jq -r '.artifactVersionId' "$WORK_DIR/qti-save.json")"

curl -fsSL -D "$WORK_DIR/qti-xml.headers" \
  "$BASE_URL/tenants/$TENANT_ID/qti/artifact-versions/$SAVED_ARTIFACT_VERSION_ID/xml" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/xml" \
  -o "$WORK_DIR/qti-export.xml"

curl -fsSL -X POST "$BASE_URL/tenants/$TENANT_ID/qti/delivery-sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"candidateRef\":\"$CANDIDATE_REF\",\"artifactVersionId\":\"$SAVED_ARTIFACT_VERSION_ID\"}" \
  -o "$WORK_DIR/qti-session.json"

export DELIVERY_SESSION_ID="$(jq -r '.deliverySessionId' "$WORK_DIR/qti-session.json")"

curl -fsSL -X POST "$BASE_URL/tenants/$TENANT_ID/qti/delivery-sessions/$DELIVERY_SESSION_ID/attempts" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"artifactVersionId\":\"$SAVED_ARTIFACT_VERSION_ID\",\"responses\":{\"RESPONSE\":\"ChoiceA\"}}" \
  -o "$WORK_DIR/qti-attempt.json"

curl -fsSL "$BASE_URL/tenants/$TENANT_ID/qti/candidates/$CANDIDATE_REF/runtime-data" \
  -H "Authorization: Bearer $TOKEN" \
  -o "$WORK_DIR/qti-runtime-data.json"

DELETE_STATUS=$(curl -sS -o "$WORK_DIR/qti-delete-body.txt" -w "%{http_code}" -X DELETE \
  "$BASE_URL/tenants/$TENANT_ID/qti/candidates/$CANDIDATE_REF/runtime-data" \
  -H "Authorization: Bearer $TOKEN")

curl -fsSL -X POST "$BASE_URL/qti/conformance-runs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data '{}' \
  -o "$WORK_DIR/qti-conformance.json"

jq -n \
  --arg packageId "$(jq -r '.packageId' "$WORK_DIR/qti-ingest.json")" \
  --arg artifactId "$ARTIFACT_ID" \
  --arg artifactVersionId "$SAVED_ARTIFACT_VERSION_ID" \
  --arg documentId "$(jq -r '.documentId' "$WORK_DIR/qti-delivery.json")" \
  --arg score "$(jq -r '.outcomeState.SCORE' "$WORK_DIR/qti-attempt.json")" \
  --arg traceEvent "$(jq -r '.processingTrace[0].event // .processingTrace[0].rule' "$WORK_DIR/qti-attempt.json")" \
  --arg sessions "$(jq -r '.deliverySessionCount' "$WORK_DIR/qti-runtime-data.json")" \
  --arg attempts "$(jq -r '.attemptCount' "$WORK_DIR/qti-runtime-data.json")" \
  --arg deleteStatus "$DELETE_STATUS" \
  --arg xmlBytes "$(wc -c < "$WORK_DIR/qti-export.xml" | tr -d ' ')" \
  --arg conformanceStatus "$(jq -r '.status' "$WORK_DIR/qti-conformance.json")" \
  --arg byStatus "$(jq -c '.summary.byStatus' "$WORK_DIR/qti-conformance.json")" \
  '{packageId:$packageId, artifactId:$artifactId, artifactVersionId:$artifactVersionId, documentId:$documentId, score:$score, traceEvent:$traceEvent, runtime:{sessions:$sessions, attempts:$attempts}, deleteStatus:$deleteStatus, xmlBytes:$xmlBytes, conformance:{status:$conformanceStatus, byStatus:($byStatus|fromjson)}}'
```

Expected shape for the public demo: `score` is `"1"`, `traceEvent` is `"match-correct"`, runtime counts are `"1"` session and `"1"` attempt before deletion, `deleteStatus` is `"204"`, XML bytes are greater than zero, and conformance has `status: "passed"` with zero failed/error assertions.

The worked example is illustrative. The runnable proof check in `checks/qti-surface-check.sh` is the acceptance check an installed agent should run and attach to its QA report.

## Produce Real Deliverables

A successful use of this skill should produce one of these customer-facing outputs, backed by files saved from the live API run:

1. Assessment app: follow `examples/assessment-app-spec.md`. Build a small app with credential setup, package ingest, delivery preview, attempt submission, results evidence, runtime-data cleanup, and conformance summary. Store QTI domain data through the API only.
2. QA report: follow `examples/qa-report-template.md`. Produce a Markdown or HTML report with run id, canonical docs cited, endpoint evidence, redacted credential handling, and unresolved leak/blocker findings.
3. Implementation handoff: if asked for a different deliverable, include the same evidence spine: package id, artifact/version ids, delivery session id, attempt id, runtime read/delete evidence, conformance run id, and canonical doc citations.

Do not convert these templates into a local QTI engine. Values in generated reports and UI must come from response bodies written by the live API.

## When Building Apps

- Store QTI domain data through the QTI API only. UI preferences can live in browser storage.
- Keep tokens out of logs and screenshots. Redact JWTs before producing evidence.
- Use `documentId`, `artifactId`, `artifactVersionId`, `deliverySessionId`, `attemptId`, `conformanceRunId`, and `requestId` returned by the API for reports.
- For user-facing explanations, cite the customer website and data dictionary links above instead of copying schema tables into the app.
