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:
- Verification link construction --- building the correct URL that encodes the invoice (or certificate) identity in a format the KSeF portal understands.
- QR code rendering --- converting that URL into a PNG buffer, base64 string, SVG, or SVG with a human-readable label.
The two classes involved:
| Class | File | Pattern |
|---|---|---|
VerificationLinkService | src/qr/verification-link-service.ts | Instance (holds baseQrUrl) |
QrCodeService | src/qr/qrcode-service.ts | Static (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.
Verification Link Types
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()
buildInvoiceVerificationUrl(
nip: string,
issueDate: Date | string,
invoiceHashBase64: string,
): stringURL format:
{baseQrUrl}/invoice/{NIP}/{DD-MM-YYYY}/{hash_base64url}| Segment | Description |
|---|---|
baseQrUrl | Environment-specific QR portal URL (see Environment URLs) |
NIP | 10-digit Polish tax identification number of the seller |
DD-MM-YYYY | Invoice issue date in day-month-year format (UTC) |
hash_base64url | Invoice hash, converted from standard base64 to base64url encoding (no padding) |
Example URL:
https://qr.ksef.mf.gov.pl/invoice/1234567890/15-03-2026/dGVzdC1oYXNoBase64url 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()
buildCertificateVerificationUrl(
contextType: string,
contextId: string,
sellerNip: string,
certSerial: string,
invoiceHashBase64: string,
privateKeyPem: string,
): stringURL format:
{baseQrUrl}/certificate/{contextType}/{contextId}/{sellerNip}/{certSerial}/{hash_base64url}/{signature_base64url}| Segment | Description |
|---|---|
baseQrUrl | Environment-specific QR portal URL |
contextType | Context identifier type (e.g., 'Nip', 'InternalId', 'NipVatUe', 'PeppolId' --- see QRCodeContextIdentifierType in src/models/qrcode/types.ts) |
contextId | Context identifier value |
sellerNip | Seller's NIP |
certSerial | Certificate serial number |
hash_base64url | Invoice hash in base64url encoding |
signature_base64url | Cryptographic signature over the path (see below) |
Example URL:
https://qr.ksef.mf.gov.pl/certificate/Nip/1234567890/1234567890/ABC123/dGVzdC1oYXNo/c2lnbmF0dXJlCode II Signature Construction
The signature in Code II proves that the URL was generated by the certificate holder. The signing process works as follows:
Build the unsigned path --- the full URL without the signature segment:
https://qr.ksef.mf.gov.pl/certificate/{contextType}/{contextId}/{sellerNip}/{certSerial}/{hash_base64url}Strip the protocol prefix --- remove
https://to get the data to sign:qr.ksef.mf.gov.pl/certificate/{contextType}/{contextId}/{sellerNip}/{certSerial}/{hash_base64url}Sign --- the signing algorithm is determined by the private key type:
Key type Algorithm Parameters RSA RSA-PSS SHA-256 digest, salt length = 32 bytes EC (ECDSA) ECDSA SHA-256 digest, IEEE P1363 encoding (fixed-length r || s, not DER)Encode --- the raw signature bytes are converted to base64url (same
+to-,/to_, strip=conversion).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.
// 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
| Environment | QR Base URL | API Base URL |
|---|---|---|
TEST | https://qr-test.ksef.mf.gov.pl | https://api-test.ksef.mf.gov.pl |
DEMO | https://qr-demo.ksef.mf.gov.pl | https://api-demo.ksef.mf.gov.pl |
PROD | https://qr.ksef.mf.gov.pl | https://api.ksef.mf.gov.pl |
Resolution logic (src/config/options.ts, resolveOptions()):
- If
baseQrUrlis provided in client options, use it directly. - Otherwise, use the
qrUrlfrom the selectedEnvironmentconfig. - If no environment is specified, default to
TEST.
// 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
| Method | Returns | Description |
|---|---|---|
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
interface QrCodeOptions {
width?: number; // Default: 300 (pixels)
margin?: number; // Default: 2 (modules)
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H'; // Default: 'M'
}| Level | Error Recovery | Use 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
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
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:
- Parsing the
viewBoxandheightattributes from the generated SVG. - Expanding the viewBox height by ~12% to accommodate the label.
- Scaling the pixel height proportionally.
- Inserting a
<text>element centered below the QR code. - 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
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
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:
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 invoiceThe 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)
ksef qr invoice \
--nip 1234567890 \
--date 2026-03-15 \
--hash "abc123base64hash==" \
--format png \
--size 400 \
-o invoice-qr.png \
--env prod| Flag | Required | Description |
|---|---|---|
--nip | Yes | Seller NIP number |
--date | Yes | Invoice issue date (ISO date string) |
--hash | Yes | Invoice hash (base64) |
--format | No | png (default) or svg |
--size | No | QR code width in pixels (default: 300) |
--label | No | Label text (SVG format only) |
-o | No | Output file path. Without it, prints base64 (PNG) or SVG markup to stdout |
--env | No | Environment: test, demo, or prod |
--json | No | Output as JSON (QrCodeResult format) |
ksef qr certificate --- Generate Certificate QR Code (Code II)
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| Flag | Required | Description |
|---|---|---|
--context-type | Yes | Context identifier type (Nip, InternalId, NipVatUe, PeppolId) |
--context-id | Yes | Context identifier value |
--seller-nip | Yes | Seller NIP number |
--cert-serial | Yes | Certificate serial number |
--hash | Yes | Invoice/certificate hash (base64) |
--key | Yes | Path to PEM private key file (RSA or ECDSA) |
--format | No | png (default) or svg |
--size | No | QR code width in pixels (default: 300) |
--label | No | Label text (SVG format only) |
-o | No | Output file path |
--env | No | Environment |
--json | No | Output 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.
ksef qr url \
--nip 1234567890 \
--date 2026-03-15 \
--hash "abc123base64hash==" \
--env prodOutput:
https://qr.ksef.mf.gov.pl/invoice/1234567890/15-03-2026/abc123base64hashWith --json:
{ "url": "https://qr.ksef.mf.gov.pl/invoice/1234567890/15-03-2026/abc123base64hash" }Files Reference
| File | Role |
|---|---|
src/qr/verification-link-service.ts | Builds Code I and Code II verification URLs |
src/qr/qrcode-service.ts | Static QR code generation (PNG, SVG, SVG+label) |
src/qr/index.ts | Barrel re-exports for the QR layer |
src/models/qrcode/types.ts | QrCodeOptions, QrCodeResult, QRCodeContextIdentifierType |
src/config/environments.ts | Environment configs with qrUrl per environment |
src/config/options.ts | resolveOptions() --- resolves baseQrUrl from environment or override |
src/client.ts | KSeFClient.qr property (line 72) --- VerificationLinkService instance |
src/cli/commands/qr.ts | CLI ksef qr command group (invoice, certificate, url subcommands) |