Test Vectors
Spec version: 0.10.2 · Generated: 2026-06-12 · Live vectors valid until: 2027-06-12
Machine-readable conformance vectors for KTC senders and receivers. Every vector is a JSON file pairing an input (a SHLink, in bare and viewer-prefixed forms, plus a QR image) with expected outcomes — the decoded payload, the SHA-256 of the decrypted Bundle, or the pipeline stage at which a conformant receiver rejects.
All content is synthetic (Jessica Argonaut). Keys are published on purpose: the decryption key travels inside every SHLink anyway, and these links protect nothing.
Three ways to use them
1. Live. Payload urls point at static files on this site, which serves Access-Control-Allow-Origin: * — so the links resolve from any receiver, including browser-based ones, with no test server. (Static hosting can't enforce the recipient parameter or exercise dynamic revocation/use counting — test those against a reference implementation.)
2. Offline, in CI. Each vector embeds a responses map (URL → status, headers, body file), so a harness can shim fetch and run with no network:
import { makeFetchShim, runVector } from './harness.mjs' // served at /vectors/harness.mjs
const index = JSON.parse(await readFile('vectors/index.json', 'utf8'))
const vectors = await Promise.all(index.vectors.map(v => readJson(`vectors/${v.file}`)))
const fetchFn = makeFetchShim(vectors, f => readFile(`vectors/${f}`, 'utf8'))
for (const v of vectors) console.log(await runVector(v, { fetchFn }))Download everything: index.json · harness.mjs — or swap runVector's reference pipeline for calls into your own implementation and assert against expect.
3. By hand. Point your scanner at the QR wall below, or paste any SHLink — one of these, or one your own app produced — into the debugger.
Catalog
Tier 1 — Decode
Pure functions: SHLink carrier forms and payload decoding. No network needed.
| ID | Title | Level | Expected | Spec | Files |
|---|---|---|---|---|---|
ktc-d1-baseline | Bare SHLink, baseline Canonical bare shlink:/ URI. U flag, exp, label, uncompressed JWE, minimal Bundle (Patient + Patient Story PDF). | baseline | resolves | #shlink-payload-decoded | vector · bundle · QR |
ktc-d2-prefixed | Viewer-prefixed SHLink The same link conveyed as https://viewer.example/#shlink:/… — the form patients paste in the ahead-of-time check-in workflow. Receivers extract the shlink:/ substring. This vector's QR encodes the PREFIXED form. | baseline | resolves | #shlink-constraints #workflow | vector · bundle · QR |
ktc-d3-b64url | base64url alphabet (the atob trap) Payload whose base64url encoding contains both '-' and '_'. Decoders using plain base64/atob() fail here; correct base64url decoding succeeds. Observed failure in a real scanner. | baseline | resolves | #shlink-payload-decoded | vector · bundle · QR |
ktc-d4-utf8-label | UTF-8 label Label contains multi-byte UTF-8 (em dash, accents). Decoders that treat decoded bytes as latin1 show mojibake; the expected label string is published. | baseline | resolves | #shlink-payload-decoded | vector · bundle · QR |
ktc-d5-no-label | No label label is 0..1 — absent here. Receivers must not require it. | baseline | resolves | #shlink-payload-decoded | vector · bundle · QR |
ktc-d6-no-exp | Missing exp (valid base SHL, not KTC) exp is required by this IG (1..1). A payload without it is a conformant base-spec SHLink but NOT a KTC link; receivers validating KTC conformance flag it. | negative | reject @ payload | #shlink-payload-decoded | vector |
ktc-d7-flag-p | Flag P (out of KTC scope) KTC requires flag exactly "U". P (passcode + manifest flow) is valid base SHL but out of scope here. | negative | reject @ payload | #shlink-payload-decoded | vector |
ktc-d8-flag-empty | Empty flag (manifest flow, out of KTC scope) No U flag means the URL is a manifest endpoint (POST), not a direct file. Out of KTC scope; receivers must not GET it as if it were U-flagged. | negative | reject @ payload | #shlink-payload-decoded | vector |
ktc-d9-truncated | Truncated payload Garbage in: the base64url JSON is cut off. Decoders must reject without crashing. | negative | reject @ decode | #shlink-payload-decoded | vector |
Tier 2 — Retrieve
HTTP behavior: missing files, expired links.
| ID | Title | Level | Expected | Spec | Files |
|---|---|---|---|---|---|
ktc-r1-gone | Gone: payload URL returns 404 A well-formed, unexpired link whose file is gone (revoked / exhausted). Receivers show a clear, friendly error — never a crash, never partial data. | robustness | reject @ retrieve | #retrieval-protocol | vector · QR |
ktc-r2-expired | Expired link exp is in the past (pinned constant). Receivers SHOULD display an expiration error when scanned — ideally before fetching at all. The file is still hosted, so behavior past the exp check is observable. | robustness | reject @ payload | #expiration | vector · QR |
Tier 3 — Decrypt
JWE handling: compression, tampering, key alphabet.
| ID | Title | Level | Expected | Spec | Files |
|---|---|---|---|---|---|
ktc-e1-zip-def | JWE with zip: DEF The base SHL spec permits zip: DEF (raw RFC 1951 deflate before encryption); this IG does not rule it out. Receivers should inflate when the header declares it. Observed failure in a real scanner. | robustness | resolves | #decryption | vector · bundle · QR |
ktc-e2-tampered | Tampered ciphertext One ciphertext character flipped. AES-GCM authentication MUST fail; the receiver must fail closed and never render partially-decrypted output. | negative | reject @ decrypt | #decryption #security-considerations | vector · QR |
ktc-e3-key-urlsafe | Key with base64url-only characters The 43-char key contains '-' and '_'. Decoders converting via plain base64 corrupt the key and fail the auth tag. | baseline | resolves | #decryption | vector · bundle · QR |
Tier 4 — Bundle
PatientSharedBundle profile, persistence floor, App Attestation.
| ID | Title | Level | Expected | Spec | Files |
|---|---|---|---|---|---|
ktc-b1-discrete-only | Discrete resources only (no DocumentReference) Apps MAY share only discrete FHIR resources. No PDF anywhere; receivers must not require one. | baseline | resolves | #fhir-bundle-profile-patientsharedbundle | vector · bundle · QR |
ktc-b2-both-pdfs | Both PDF kinds in one Bundle FHIR-Rendered PDF (60591-5) and Patient Story PDF (51855-5) together. Receivers SHALL distinguish them by type and treat them as distinct documents. | baseline | resolves | #documentreference-profile-patientshareddocumentreference | vector · bundle · QR |
ktc-b3-persistence-core | The SHALL-persist four US Core-shaped Condition, MedicationRequest, AllergyIntolerance, and Immunization plus a FHIR-Rendered PDF. A conformant receiver persists all five chart-associated. | baseline | resolves | #conformance-requirements | vector · bundle · QR |
ktc-b4-carin-dic | CARIN DIC Coverage Coverage per CARIN Digital Insurance Card — an in-scope MAY for senders; receivers SHOULD persist it as part of full-USCDI content. | baseline | resolves | #fhir-bundle-profile-patientsharedbundle | vector · bundle · QR |
ktc-b9-carried-note | Ordinary DocumentReference carried along A US Core clinical-note DocumentReference from the patient's record (LOINC 11506-3, text/rtf, no patient-shared category) alongside the KTC PDFs. The PatientSharedDocumentReference profile constrains only the two patient-shared kinds; other DocumentReferences are ordinary USCDI content and MUST be accepted. | baseline | resolves | #documentreference-profile-patientshareddocumentreference | vector · bundle · QR |
ktc-b5-attestation | App Attestation (valid) Bundle.meta.extension carries an ES256 JWS. iss = https://ktc-spec.github.io/vectors/attestation; JWKS at {iss}/.well-known/jwks.json (hosted by this site). Verification should succeed; the key is an intentionally public test key. | baseline | resolves | #app-attestation-optional | vector · bundle · QR |
ktc-b6-attestation-broken | App Attestation (unverifiable) The attestation signature fails verification. Receivers SHALL NOT block Bundle processing — show no provenance indicator and continue. | robustness | resolves | #app-attestation-optional | vector · bundle · QR |
ktc-b7-bad-type | Bundle.type is document (invalid) type must be collection. Receivers reject or flag — and never crash. | negative | reject @ bundle | #fhir-bundle-profile-patientsharedbundle | vector · bundle · QR |
ktc-b8-single-entry | Patient-only Bundle (invalid) entry is 2..* — a Patient with no content entry violates the profile. | negative | reject @ bundle | #fhir-bundle-profile-patientsharedbundle | vector · bundle · QR |
QR Wall
Point a real scanner at the screen. Each QR encodes the bare shlink:/ URI (the prefixed-form vector encodes the https://…#shlink:/… URL instead). Compare what your implementation shows against the expectation.



















SHLink Debugger
Paste any SHLink — bare or viewer-prefixed, a vector from this page or one produced by your own Patient App — and watch each pipeline stage pass or fail. Everything runs in your browser (decode, fetch with recipient=KTC Spec Debugger, WebCrypto decrypt incl. zip: DEF, profile checks); nothing is sent to any server other than the link's own payload URL.