Skip to main content

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"
}
]
}
]
}
}
FieldSourceDescription
typConstant "icao.vacc"ICAO document type
msg.uvciGenerated from patient name + DOB + nanotime hashUnique Vaccination Certificate Identifier
msg.pid.dobPatient.birthDateISO date
msg.pid.nPatient.name → MRZ formatName in ICAO MRZ format (uppercase, spaces → <)
ve[].desVaccinationRecord.vaccineCodeVaccine code (CVX, SNOMED CT, or GTIN)
ve[].namVaccinationRecord.vaccineNameHuman-readable vaccine name
ve[].disConstant "RA01"ICD-11 disease code (default)
vd[].dvcVaccinationRecord.occurrenceDateDate of vaccination (yyyy-MM-dd)
vd[].seqVaccinationRecord.doseNumberDose sequence number
vd[].lotVaccinationRecord.lotNumberLot/batch number
vd[].admVaccinationRecord.performerDisplayAdministering 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:

  1. A Document Signer Certificate (DSC) issued by your national authority
  2. Signing the payload with the DSC's private key (ECDSA P-256 or RSA-PSS)
  3. Including the signature and certificate reference in the payload
  4. 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 →