VDS-NC Digital Vaccination Certificates
VdsNcService generates WHO-compliant Visible Digital Seal (VDS-NC) QR codes for printable vaccination certificates.
What VDS-NC is
VDS-NC (Visible Digital Seal — Non-Constrained) is a WHO and ICAO Doc 9303 standard for encoding vaccination data in a compact JSON payload rendered as a QR code on paper documents. It enables verification of paper-based vaccination certificates without a central registry — the QR code contains the complete vaccination record.
The format is used by multiple national health authorities for COVID-19 vaccination certificates and is being extended to cover routine immunisations.
Payload structure
The service builds this JSON structure:
{
"typ": "icao.vacc",
"msg": {
"uvci": "URN:UVCI:01:AJ:3F7A9B2C",
"pid": {
"dob": "1985-03-15",
"n": "KUMAR<<PRIYA"
},
"ve": [
{
"des": "XM68M6",
"nam": "COVID-19 vaccine",
"dis": "RA01",
"vd": [
{
"dvc": "2021-03-01",
"seq": 1,
"lot": "LOT12345",
"adm": "Royal London Hospital"
},
{
"dvc": "2021-04-15",
"seq": 2,
"lot": "LOT67890",
"adm": "Royal London Hospital"
}
]
}
]
}
}
| Field | Source | Description |
|---|---|---|
typ | Constant "icao.vacc" | ICAO document type |
msg.uvci | Generated from patient name + DOB + nanotime hash | Unique Vaccination Certificate Identifier |
msg.pid.dob | Patient.birthDate | ISO date |
msg.pid.n | Patient.name → MRZ format | Name in ICAO MRZ format (uppercase, spaces → <) |
ve[].des | VaccinationRecord.vaccineCode | Vaccine code (CVX, SNOMED CT, or GTIN) |
ve[].nam | VaccinationRecord.vaccineName | Human-readable vaccine name |
ve[].dis | Constant "RA01" | ICD-11 disease code (default) |
vd[].dvc | VaccinationRecord.occurrenceDate | Date of vaccination (yyyy-MM-dd) |
vd[].seq | VaccinationRecord.doseNumber | Dose sequence number |
vd[].lot | VaccinationRecord.lotNumber | Lot/batch number |
vd[].adm | VaccinationRecord.performerDisplay | Administering facility |
Grouping
Records are grouped by vaccineCode — multiple doses of the same vaccine form a single ve entry with multiple vd entries. The insertion order is preserved (doses in chronological order since the vaccination history is already sorted newest-first, the service processes them as-is).
MRZ name encoding
private static String toMrz(String name) {
return name.toUpperCase()
.replace(" ", "<")
.replaceAll("[^A-Z0-9<]", "<");
}
Example: "Priya Kumar" → "PRIYA<KUMAR". Surname-first format (SURNAME<<GIVEN) as required by ICAO Doc 9303 is applied if the name is already in family given order from FHIR.
QR code generation
ZXing QRCodeWriter with error correction level Q (25% data recovery):
Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.Q);
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix matrix = new QRCodeWriter()
.encode(payload, BarcodeFormat.QR_CODE, sizePixels, sizePixels, hints);
ByteArrayOutputStream out = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(matrix, "PNG", out);
return Base64.getEncoder().encodeToString(out.toByteArray());
The result is a Base64-encoded PNG — embedded directly in the certificate template as <img src="data:image/png;base64,...">.
The default size is 300×300 pixels. The API endpoint accepts a size parameter:
GET /api/certificate/qr?size=400
Using the certificate
On the /certificate page, the QR code appears alongside the patient's vaccination table. Clicking Print (or Ctrl+P) hides the navigation bar via @media print and renders the card ready for printing or PDF export.
Production: signed certificates
This implementation generates unsigned payloads. A compliant VDS-NC certificate requires:
- A Document Signer Certificate (DSC) issued by your national authority
- Signing the payload with the DSC's private key (ECDSA P-256 or RSA-PSS)
- Including the signature and certificate reference in the payload
- Registration with the ICAO Public Key Directory (PKD)
The payload structure produced by buildVdsPayload() is already in the correct format for signing. To add signing, wrap the payload in a JWS (JSON Web Signature) envelope:
// Example signing step (implementation depends on your DSC format)
JWSSigner signer = new ECDSASigner(privateKey);
JWSObject jwsObject = new JWSObject(
new JWSHeader.Builder(JWSAlgorithm.ES256)
.x509CertChain(certChain)
.build(),
new Payload(buildVdsPayload(name, dob, records))
);
jwsObject.sign(signer);
return encodeToQrBase64(jwsObject.serialize(), sizePixels);
Next: Session & Security →