API Documentation

API Reference

QR Certificate API

Generate and verify digitally signed, encrypted employee certificates and shipment safety declarations embedded in QR codes.

How it works

Step 1
Generate
POST certificate data to the API
Step 2
QR Code
Get a QR code with an embedded URL
Step 3
Scan
Recipient scans with any phone camera
Step 4
Verify
Browser shows valid / invalid / expired / rejected

Authentication

The generate endpoint requires an API key. Pass it in the X-API-Key header.

Note: If no 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 configuration

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

Endpoints

POST /api/generate Requires API Key

Generate a signed, encrypted certificate QR code.

Certificate types

TypeDescription
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

FieldTypeRequiredDescription
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

FieldTypeRequiredDescription
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
Dates: accepted as 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

FieldTypeDescription
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)
GET /verify?data=...

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.

GET /api/verify?data=...

JSON API for programmatic certificate verification.

Query parameters

ParamTypeDescription
data string The encrypted certificate payload

Response

FieldTypeDescription
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"
  }
}
Displayed fields: the verification page shows 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.
Rejected shipments: a declaration with "status": "rejected" also carries reasonDenied. It is authentic, so valid stays true — the verification page shows it as a rejected shipment with the reason.
GET /health

Liveness probe for monitoring. Returns { "status": "ok" } with HTTP 200. / redirects to /docs.

Shipment declarations

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.

Security

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.