Skip to content

QR Codes & Verification Links

How to generate KSeF invoice verification URLs and QR codes. Covers the two verification link types (Code I and Code II), the QR code generation API, environment-aware base URLs, and the CLI commands.


Overview

Polish regulations require that printed invoices registered in KSeF carry a scannable QR code linking to the invoice's verification page on the Ministry of Finance portal. The QR layer in this library handles two concerns:

  1. Verification link construction --- building the correct URL that encodes the invoice (or certificate) identity in a format the KSeF portal understands.
  2. QR code rendering --- converting that URL into a PNG buffer, base64 string, SVG, or SVG with a human-readable label.

The two classes involved:

ClassFilePattern
VerificationLinkServicesrc/qr/verification-link-service.tsInstance (holds baseQrUrl)
QrCodeServicesrc/qr/qrcode-service.tsStatic (stateless utility)

VerificationLinkService is exposed as client.qr on KSeFClient (src/client.ts, line 72). QrCodeService is imported directly --- it has no dependency on client state.


KSeF defines two verification URL formats, referred to in Polish tax law as Kod I (Code I) and Kod II (Code II).

Code I --- Invoice Verification URL

Used on every printed invoice. Contains the seller's NIP, the invoice issue date, and the invoice hash. No cryptographic signature is required.

Method: VerificationLinkService.buildInvoiceVerificationUrl()

typescript
buildInvoiceVerificationUrl(
  nip: string,
  issueDate: Date | string,
  invoiceHashBase64: string,
): string

URL format:

{baseQrUrl}/invoice/{NIP}/{DD-MM-YYYY}/{hash_base64url}
SegmentDescription
baseQrUrlEnvironment-specific QR portal URL (see Environment URLs)
NIP10-digit Polish tax identification number of the seller
DD-MM-YYYYInvoice issue date in day-month-year format (UTC)
hash_base64urlInvoice hash, converted from standard base64 to base64url encoding (no padding)

Example URL:

https://qr.ksef.mf.gov.pl/invoice/1234567890/15-03-2026/dGVzdC1oYXNo

Base64url conversion: The invoiceHashBase64 parameter accepts standard base64. The service converts it internally: + becomes -, / becomes _, trailing = padding is stripped.

Date handling: The issueDate parameter accepts either a Date object or an ISO date string ("2026-03-15"). The date is formatted using UTC methods (getUTCDate, getUTCMonth, getUTCFullYear) to avoid timezone drift.


Code II --- Certificate Verification URL

Used for certificate-based verification of invoice provenance. Unlike Code I, Code II includes a cryptographic signature over the URL path, proving that the URL was generated by the holder of the certificate's private key.

Method: VerificationLinkService.buildCertificateVerificationUrl()

typescript
buildCertificateVerificationUrl(
  contextType: string,
  contextId: string,
  sellerNip: string,
  certSerial: string,
  invoiceHashBase64: string,
  privateKeyPem: string,
): string

URL format:

{baseQrUrl}/certificate/{contextType}/{contextId}/{sellerNip}/{certSerial}/{hash_base64url}/{signature_base64url}
SegmentDescription
baseQrUrlEnvironment-specific QR portal URL
contextTypeContext identifier type (e.g., 'Nip', 'InternalId', 'NipVatUe', 'PeppolId' --- see QRCodeContextIdentifierType in src/models/qrcode/types.ts)
contextIdContext identifier value
sellerNipSeller's NIP
certSerialCertificate serial number
hash_base64urlInvoice hash in base64url encoding
signature_base64urlCryptographic signature over the path (see below)

Example URL:

https://qr.ksef.mf.gov.pl/certificate/Nip/1234567890/1234567890/ABC123/dGVzdC1oYXNo/c2lnbmF0dXJl

Code II Signature Construction

The signature in Code II proves that the URL was generated by the certificate holder. The signing process works as follows:

  1. Build the unsigned path --- the full URL without the signature segment:

    https://qr.ksef.mf.gov.pl/certificate/{contextType}/{contextId}/{sellerNip}/{certSerial}/{hash_base64url}
  2. Strip the protocol prefix --- remove https:// to get the data to sign:

    qr.ksef.mf.gov.pl/certificate/{contextType}/{contextId}/{sellerNip}/{certSerial}/{hash_base64url}
  3. Sign --- the signing algorithm is determined by the private key type:

    Key typeAlgorithmParameters
    RSARSA-PSSSHA-256 digest, salt length = 32 bytes
    EC (ECDSA)ECDSASHA-256 digest, IEEE P1363 encoding (fixed-length r || s, not DER)
  4. Encode --- the raw signature bytes are converted to base64url (same + to -, / to _, strip = conversion).

  5. Append --- the signature is appended as the final path segment.

The private key is passed as a PEM string. The service uses crypto.createPrivateKey() to parse it and inspects key.asymmetricKeyType to select the signing algorithm. Unsupported key types throw an error.

typescript
// src/qr/verification-link-service.ts, lines 45-61
const key = crypto.createPrivateKey(privateKeyPem);

if (key.asymmetricKeyType === 'rsa') {
  signature = crypto.sign('sha256', Buffer.from(dataToSign), {
    key,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
    saltLength: 32,
  });
} else if (key.asymmetricKeyType === 'ec') {
  signature = crypto.sign('sha256', Buffer.from(dataToSign), {
    key,
    dsaEncoding: 'ieee-p1363',
  });
} else {
  throw new Error(`Unsupported key type: ${key.asymmetricKeyType}`);
}

Environment-Aware Base URLs

The QR portal URL varies by KSeF environment. The base URL is configured automatically based on the environment option passed to KSeFClient, or can be overridden with baseQrUrl.

File: src/config/environments.ts

EnvironmentQR Base URLAPI Base URL
TESThttps://qr-test.ksef.mf.gov.plhttps://api-test.ksef.mf.gov.pl
DEMOhttps://qr-demo.ksef.mf.gov.plhttps://api-demo.ksef.mf.gov.pl
PRODhttps://qr.ksef.mf.gov.plhttps://api.ksef.mf.gov.pl

Resolution logic (src/config/options.ts, resolveOptions()):

  1. If baseQrUrl is provided in client options, use it directly.
  2. Otherwise, use the qrUrl from the selected Environment config.
  3. If no environment is specified, default to TEST.
typescript
// Custom QR URL (e.g., for a staging proxy):
const client = new KSeFClient({
  environment: 'PROD',
  baseQrUrl: 'https://my-proxy.example.com/qr',
});

QR Code Generation

File: src/qr/qrcode-service.ts

QrCodeService is a static utility class that wraps the qrcode npm package. All methods are async and accept an optional QrCodeOptions configuration.

Methods

MethodReturnsDescription
generateQrCode(url, options?)Promise<Buffer>PNG image as a Node.js Buffer
generateQrCodeBase64(url, options?)Promise<string>PNG image as a base64-encoded string
generateQrCodeSvg(url, options?)Promise<string>SVG markup string
generateQrCodeSvgWithLabel(url, label, options?)Promise<string>SVG markup with a text label below the QR code
generateResult(url, options?)Promise<QrCodeResult>Combined object with { url, qrCode } (base64 PNG)

QrCodeOptions

File: src/models/qrcode/types.ts

typescript
interface QrCodeOptions {
  width?: number;                          // Default: 300 (pixels)
  margin?: number;                         // Default: 2 (modules)
  errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';  // Default: 'M'
}
LevelError RecoveryUse Case
L~7%Maximum data density
M~15%Default, good balance
Q~25%Environments with some damage risk
H~30%Harsh printing/scanning conditions

QrCodeResult

File: src/models/qrcode/types.ts

typescript
interface QrCodeResult {
  url: string;    // The verification URL encoded in the QR code
  qrCode: string; // Base64-encoded PNG image
}

QRCodeContextIdentifierType

File: src/models/qrcode/types.ts

typescript
type QRCodeContextIdentifierType = 'Nip' | 'InternalId' | 'NipVatUe' | 'PeppolId';

Used as the contextType parameter in Code II verification URLs.

SVG with Label

generateQrCodeSvgWithLabel() extends the QR code SVG by:

  1. Parsing the viewBox and height attributes from the generated SVG.
  2. Expanding the viewBox height by ~12% to accommodate the label.
  3. Scaling the pixel height proportionally.
  4. Inserting a <text> element centered below the QR code.
  5. XML-escaping the label text to prevent injection.

If the SVG cannot be parsed (fallback path), the label is appended before </svg> with a fixed font size.


Usage Examples

Code I: Invoice Verification URL + QR Code

typescript
import { KSeFClient, QrCodeService } from 'ksef-client-ts';

const client = new KSeFClient({ environment: 'PROD' });

// After sending an invoice and receiving the hash from KSeF:
const invoiceHash = 'abc123base64hash==';  // from KSeF response
const sellerNip = '1234567890';
const issueDate = new Date('2026-03-15');

// Step 1: Build the verification URL
const url = client.qr.buildInvoiceVerificationUrl(sellerNip, issueDate, invoiceHash);
// → "https://qr.ksef.mf.gov.pl/invoice/1234567890/15-03-2026/abc123base64hash"

// Step 2: Generate QR code as PNG buffer (for embedding in PDF)
const pngBuffer = await QrCodeService.generateQrCode(url, { width: 400 });

// Step 2 (alt): Generate QR code as base64 (for HTML <img> tags)
const base64Png = await QrCodeService.generateQrCodeBase64(url);

// Step 2 (alt): Generate QR code as SVG with label
const svg = await QrCodeService.generateQrCodeSvgWithLabel(
  url,
  'Faktura KSeF #FA/2026/001',
  { width: 300, errorCorrectionLevel: 'H' },
);

// Step 2 (alt): Get combined result object
const result = await QrCodeService.generateResult(url);
// → { url: "https://qr.ksef.mf.gov.pl/...", qrCode: "iVBORw0KGgo..." }

Code II: Certificate Verification URL + QR Code

typescript
import * as fs from 'node:fs';
import { KSeFClient, QrCodeService } from 'ksef-client-ts';

const client = new KSeFClient({ environment: 'PROD' });

const privateKeyPem = fs.readFileSync('./my-key.pem', 'utf-8');

const url = client.qr.buildCertificateVerificationUrl(
  'Nip',              // contextType
  '1234567890',       // contextId
  '1234567890',       // sellerNip
  'CERT-SN-001',      // certSerial
  'invoiceHash==',    // invoiceHashBase64
  privateKeyPem,      // PEM private key (RSA or ECDSA)
);

// Generate QR code from the signed URL
const pngBuffer = await QrCodeService.generateQrCode(url);
fs.writeFileSync('./certificate-qr.png', pngBuffer);

Standalone QrCodeService (No Client Needed)

QrCodeService is fully static and does not require a KSeFClient instance. You can use it to generate QR codes for any URL:

typescript
import { QrCodeService } from 'ksef-client-ts';

const svg = await QrCodeService.generateQrCodeSvg('https://example.com', {
  width: 200,
  margin: 4,
  errorCorrectionLevel: 'Q',
});

Integration with Invoice Workflow

The typical end-to-end flow for QR codes on printed invoices:

1. Open KSeF session

2. Send invoice XML to KSeF

3. Receive response with invoice hash (SHA-256, base64)

4. Build verification URL
     │  client.qr.buildInvoiceVerificationUrl(nip, issueDate, hash)

5. Generate QR code image
     │  QrCodeService.generateQrCode(url) → PNG Buffer

6. Embed QR code in printed/PDF invoice

7. Customer scans QR → opens KSeF portal → verifies invoice

The invoice hash is returned by KSeF when the invoice is accepted. This hash, combined with the seller's NIP and the issue date, forms the minimum information needed for Code I verification.


CLI Commands

File: src/cli/commands/qr.ts

The ksef qr command group provides three subcommands.

ksef qr invoice --- Generate Invoice QR Code (Code I)

bash
ksef qr invoice \
  --nip 1234567890 \
  --date 2026-03-15 \
  --hash "abc123base64hash==" \
  --format png \
  --size 400 \
  -o invoice-qr.png \
  --env prod
FlagRequiredDescription
--nipYesSeller NIP number
--dateYesInvoice issue date (ISO date string)
--hashYesInvoice hash (base64)
--formatNopng (default) or svg
--sizeNoQR code width in pixels (default: 300)
--labelNoLabel text (SVG format only)
-oNoOutput file path. Without it, prints base64 (PNG) or SVG markup to stdout
--envNoEnvironment: test, demo, or prod
--jsonNoOutput as JSON (QrCodeResult format)

ksef qr certificate --- Generate Certificate QR Code (Code II)

bash
ksef qr certificate \
  --context-type Nip \
  --context-id 1234567890 \
  --seller-nip 1234567890 \
  --cert-serial CERT-SN-001 \
  --hash "invoiceHash==" \
  --key ./my-private-key.pem \
  --format svg \
  --label "Certificate Verification" \
  -o cert-qr.svg \
  --env prod
FlagRequiredDescription
--context-typeYesContext identifier type (Nip, InternalId, NipVatUe, PeppolId)
--context-idYesContext identifier value
--seller-nipYesSeller NIP number
--cert-serialYesCertificate serial number
--hashYesInvoice/certificate hash (base64)
--keyYesPath to PEM private key file (RSA or ECDSA)
--formatNopng (default) or svg
--sizeNoQR code width in pixels (default: 300)
--labelNoLabel text (SVG format only)
-oNoOutput file path
--envNoEnvironment
--jsonNoOutput as JSON

ksef qr url --- Print Verification URL Only

Generates and prints the Code I verification URL without producing a QR code image. Useful for scripting and debugging.

bash
ksef qr url \
  --nip 1234567890 \
  --date 2026-03-15 \
  --hash "abc123base64hash==" \
  --env prod

Output:

https://qr.ksef.mf.gov.pl/invoice/1234567890/15-03-2026/abc123base64hash

With --json:

json
{ "url": "https://qr.ksef.mf.gov.pl/invoice/1234567890/15-03-2026/abc123base64hash" }

Files Reference

FileRole
src/qr/verification-link-service.tsBuilds Code I and Code II verification URLs
src/qr/qrcode-service.tsStatic QR code generation (PNG, SVG, SVG+label)
src/qr/index.tsBarrel re-exports for the QR layer
src/models/qrcode/types.tsQrCodeOptions, QrCodeResult, QRCodeContextIdentifierType
src/config/environments.tsEnvironment configs with qrUrl per environment
src/config/options.tsresolveOptions() --- resolves baseQrUrl from environment or override
src/client.tsKSeFClient.qr property (line 72) --- VerificationLinkService instance
src/cli/commands/qr.tsCLI ksef qr command group (invoice, certificate, url subcommands)

Released under the MIT License.