Generate and verify digitally signed, encrypted employee certificates and shipment safety declarations embedded in QR codes.
The generate endpoint requires an API key. Pass it in the X-API-Key header.
API_KEY environment variable is set, the server generates a random key on startup and prints it to the console. Set API_KEY for a persistent key.
Example header
X-API-Key: your-api-key-here
Environment variables are loaded with Next.js-style .env handling, so the app can read files such as .env, .env.local, .env.development, and .env.production.
Use BASE_URL for the verification link host and API_KEY to keep the generate endpoint protected with a stable key. ENCRYPTION_KEY is a 64-character hex string that encrypts every payload; it must never change, because QR codes already printed on certificates are encrypted with it.
PORT defaults to 3000 and HOST to 0.0.0.0. Set HOST=127.0.0.1 when running behind a reverse proxy.
Example .env.local
BASE_URL=https://certs.example.com API_KEY=replace-with-a-fixed-key ENCRYPTION_KEY=64-character-hex-string PORT=3000 HOST=127.0.0.1
Generate a signed, encrypted certificate QR code.
Certificate types
| Type | Description |
|---|---|
| employee | Training certificate held by a person. Default when type is omitted. |
| shipment | Airfreight safety declaration for a shipment. Inferred when the body contains awb or customerName. |
Request body — employee certificate
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | optional | employee (default) |
| name | string | required | Full name of the certificate holder |
| dateOfBirth | string | required | Date of birth (DD-MM-YYYY) |
| course | string | required | Course or certificate name |
| validUntil | string | required | Expiry date (DD-MM-YYYY) |
| certificateDate | string | required | Certificate date provided when the QR code is created |
| certificateNumber | string | required | Unique certificate number |
Request body — shipment certificate
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | optional | shipment |
| awb | string | required | Air waybill number |
| customerName | string | required | Customer the shipment belongs to |
| destination | string | optional | Destination IATA code |
| coli | string | optional | Number of colli (colli is accepted as an alias) |
| weight | string | optional | Weight as printed, eg. 85 kg |
| issuedAt | string | optional | Declaration date + time, defaults to now. Shown as a single combined Date + Time |
| status | string | optional | accepted (default) or rejected. Not displayed as a field — it drives the rejected banner so a refused shipment is never shown as valid |
| reasonDenied | string | optional | Rejection reason, only stored when status is rejected |
YYYY-MM-DD, ISO-8601 or DD-MM-YYYY and normalised to DD-MM-YYYY. Shipment times are normalised to DD-MM-YYYY HH:MM in Europe/Amsterdam, matching the declaration PDF. Empty optional fields are dropped so the QR code stays scannable at print size.
Response
| Field | Type | Description |
|---|---|---|
| type | string | The certificate type that was generated |
| qrCode | string | Base64 PNG data URL of the QR code |
| verifyUrl | string | The verification URL encoded in the QR |
| certificate | object | The signed certificate data |
cURL example — employee certificate
curl -X POST https://pmtqr.test.intuitive.nl/api/generate \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -d '{ "type": "employee", "name": "John Doe", "dateOfBirth": "15-05-1990", "course": "AI Fundamentals", "certificateDate": "31-03-2026", "validUntil": "31-12-2027", "certificateNumber": "CERT-2026-001" }'
cURL example — shipment declaration
curl -X POST https://pmtqr.test.intuitive.nl/api/generate \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -d '{ "type": "shipment", "awb": "016-38227405", "customerName": "ACME Logistics BV", "destination": "ORD", "coli": "3", "weight": "85 kg", "status": "accepted", "issuedAt": "2026-09-21T13:05:00.000Z" }'
PowerShell example
$headers = @{
"Content-Type" = "application/json"
"X-API-Key" = "your-api-key"
}
$body = @{
type = "shipment"
awb = "016-38227405"
customerName = "ACME Logistics BV"
destination = "ORD"
coli = "3"
weight = "85 kg"
status = "accepted"
issuedAt = "2026-09-21T13:05:00.000Z"
} | ConvertTo-Json
$resp = Invoke-RestMethod -Uri https://pmtqr.test.intuitive.nl/api/generate `
-Method POST -Headers $headers -Body $body
# Save QR code to file
$bytes = [Convert]::FromBase64String($resp.qrCode.Split(',')[1])
[IO.File]::WriteAllBytes("certificate.png", $bytes)
Verification page opened when a QR code is scanned. Decrypts the payload server-side, verifies the digital signature, and displays the result.
The page picks its layout from the certificate type: employee certificates show holder and course details, shipment declarations show the five detail rows printed on the safety declaration (AWB, customer, destination, coli, weight) plus its date and time.
This page is embedded in the QR code URL — no manual interaction needed.
JSON API for programmatic certificate verification.
Query parameters
| Param | Type | Description |
|---|---|---|
| data | string | The encrypted certificate payload |
Response
| Field | Type | Description |
|---|---|---|
| valid | boolean | True when the payload decrypted and the signature verified |
| expired | boolean | True when a valid certificate is past its validUntil date |
| type | string | employee or shipment, null when the payload could not be read |
| certificate | object | The signed certificate data, null when invalid |
| error | string | Only present when verification failed |
Employee certificate
{
"valid": true,
"expired": false,
"type": "employee",
"certificate": {
"type": "employee",
"name": "John Doe",
"dateOfBirth": "15-05-1990",
"course": "AI Fundamentals",
"certificateNumber": "CERT-2026-001",
"certificateDate": "31-03-2026",
"validUntil": "31-12-2027"
}
}
Shipment declaration
{
"valid": true,
"expired": false,
"type": "shipment",
"certificate": {
"type": "shipment",
"awb": "016-38227405",
"customerName": "ACME Logistics BV",
"destination": "ORD",
"coli": "3",
"weight": "85 kg",
"status": "accepted",
"issuedAt": "21-09-2026 15:05"
}
}
awb, customerName, destination, coli, weight and issuedAt as a single combined Date + Time — the five detail rows printed on the declaration plus its timestamp. status and reasonDenied are carried in the payload but are not listed as fields.
"status": "rejected" also carries reasonDenied. It is authentic, so valid stays true — the verification page shows it as a rejected shipment with the reason.
Liveness probe for monitoring. Returns { "status": "ok" } with HTTP 200. / redirects to /docs.
smartpoint-api calls POST /api/generate with type: "shipment" whenever it produces a safety declaration PDF, and prints the returned verifyUrl as the QR code on that PDF. Configure it there with PMT_QR_URL and PMT_QR_API_KEY.
If this service is unreachable, smartpoint-api falls back to encoding the plain declaration reference, so declaration generation never fails. Declarations printed before this integration carry that plain reference and cannot be verified here.
ECDSA P-256 digital signatures prevent forgery — certificates cannot be faked without the private key.
AES-256-CBC encryption hides the certificate data inside the QR code payload.
Both keys remain server-side only. Without a database, certificates cannot be revoked — use the validUntil field for expiry.