An official developer portal of the Government of the Republic of Zambia · Test environmentVerify a certificate
Pamodzi Developer HubBuild, integrate and learn the e-Services framework

Verifying issued documents

Permits, licences and certificates issued on the platform are checked every day: at roadblocks, at banks, at border posts, by other ministries. In this lesson you learn what makes a document checkable, which endpoint to use for which job, and how to report a result without ever overstating it.

What a document carries

Every issued document has:

  • a verification code, random, printed under the QR code. It is deliberately not the permit number: permit numbers

    are sequential, and checking by number would let anyone walk the register. On a federated deployment the code starts with the issuing node's key (for example EXAMPLE-MDA.7KQ2-M9XZ-4A);

  • a QR code holding the address <portal>/verify/<code>. When it fits, the address is followed by # and a compact

    signed payload (a JWS) that can be checked offline. Browsers never send the part after # to a server;

  • an electronic seal: a PAdES signature with the issuing ministry's seal certificate. Issued permits are sealed at

    the long-term level (B-LTA) and carry their own validation evidence;

  • a time-stamp that proves when the seal was applied;
  • an entry in the platform's register of issued documents, matched by the SHA-256 of the file, with its lifecycle:

    active, expired, suspended, revoked, replaced.

A document whose sealing failed is held back in a pending-seals queue; it is not issued as if it were sealed.

Choosing the endpoint

JobUse
A person scans a QR codeThe portal page the QR opens, /verify/<code>
Your system checks codes or numbersPartner API: GET /api/v1/permits/verify/{code} or GET /api/v1/verify/{documentNumber}?issuer= (scope permits:verify)
Your system checks many items at oncePOST /api/v1/verify/documents with up to 20 items: codes (permits:verify) or base64 PDFs (documents:validate)
Someone uploads a PDFPOST /api/verify/document with the PDF as the body (public, 10 checks per 10 minutes per address)
A report to keepPOST /api/verify/document/report?format=pdf (sealed by the platform) or format=json (ETSI TS 119 102-2 shape)
A receiptGET /api/receipts/verify/{ref}?sig=...
An older system that speaks the GSB conventionGET /api/upstream/VerifyCertificates?PermitNumber=...&DocType=... (answers true or false only)
No connectionThe signed QR payload against a saved copy of the trust list

The public endpoints are throttled per address for people. A system that checks many documents must use the Partner API, with its own client and limits.

curl -s "$API/permits/verify/7KQ2-M9XZ-4A" -H "Authorization: Bearer $TOKEN"

Uploaded PDFs are read in memory and never stored. Files over 10 MB or 100 pages, encrypted files and non-PDFs are refused.

Reading the answer

Every check ends in one vocabulary:

  • outcome: valid, warning, invalid, unknown or not-signed;
  • headline: one plain sentence;
  • points[]: findings with a stable code and a tone (ok, warn, bad, info);
  • seal: who sealed, for which ministry, when, and whether the time is proven by a time-stamp or only claimed;
  • changed: whether the file was altered after sealing;
  • record: the issued record it matches, without holder data.

A code check returns the record itself: valid (true only when the status is active), status, statusLabel, validFrom, validUntil, validityText, the issuer, the service, the holder's initials and surname, a masked identifier, and lifecycle details such as supersededBy and suspendedUntil.

Seal and status, side by side

The most common mistake is to treat a genuine seal as a valid document. They are two separate facts, and the platform always reports both:

  • the seal is genuine, and the document was revoked on a date for a reason;
  • the seal is genuine, and the document was replaced by another number;
  • the seal is genuine, and the permit expired;
  • the seal certificate was revoked after sealing: the seal stays valid. Revoked before the proven signing time:

    the seal is not valid;

  • a back-captured paper permit is pending-validation until a second officer validates it, never valid before.

The rules

  1. Show "valid" only when the platform said valid. Show warnings as warnings.
  2. Show the document's status next to the seal result, never instead of it.
  3. Do not invent an expiry. When there is no validUntil, show validityText as given.
  4. A 503 means "could not check now" (for example the owning node is down), not "invalid". Offer to try again.
  5. "Not found" is one answer for every unknown, malformed or foreign code. Do not try to read meaning into it.
  6. Do not cache answers: they are no-store, and a permit can be suspended at any moment.
  7. Show no more personal data than you were given. The answer masks the holder on purpose.

The trust list and offline checks

The platform publishes the certificates that seal, time-stamp and answer for revocation at /.well-known/gsb-trust-list.json, with a detached CMS signature at .../gsb-trust-list.json.p7s. Older versions are available with ?sequence=N. Each seal entry has a kid and a public jwk, which is what an offline verifier needs:

const entry = list.entries.find((e) => e.kid === header.kid);       // the JWS header names the key
if (!entry || entry.kind !== 'seal') return { ok: false };
const key = crypto.createPublicKey({ key: entry.jwk, format: 'jwk' });
const ok = crypto.verify('sha256', Buffer.from(`${h}.${p}`),
  header.alg.startsWith('ES') ? { key, dsaEncoding: 'ieee-p1363' } : key, b64url(s));
// then refuse a key revoked on or before the payload's issue date (payload.i)

Verify the list's own signature against the national root before you trust it, refresh it whenever you are online, and say on screen when a result was checked offline: an offline check cannot see a suspension made after your copy of the list.

Exercise

Take a sandbox permit. Check it by code through the Partner API, then upload its PDF to the public document check. Then change one byte of the PDF and upload it again. Write down the outcome, changed and record of both answers.

Check your understanding

  • Why is a document checked by a random verification code rather than by its permit number?
  • Which endpoint should a bank use to check hundreds of permits a day, and why not the public one?
  • What is the difference between the seal being valid and the document being valid?
  • What should your screen say when the answer is 503?
  • What can an offline QR check prove, and what can it not prove?
  • Where is the trust list published, and how do you know it is genuine?

Sign in to record your progress.

Previous: Webhooks and events Next: Signing people in: Pamodzi Pass and ZamPass