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

Catalogue sync and relaying applications, payments and documents

The citizen sees one portal. Behind it, the hub mirrors each node's catalogue and relays applications, status, payments, documents and verification to the owning node. In this lesson you follow each flow, learn what is stored where, and learn the error codes an operator meets.

Catalogue sync

The node owns its services; the hub mirrors them.

The manifest. For each published service the node builds a service manifest from public catalogue data only: the passport (name, summary, who can apply, eligibility, processing time, fee text, outputs, process steps), fees in ngwee, required documents, the application form, capabilities, channels and the ministry's public contacts. It never includes staff, workflow configuration, templates, merchant settings or case data. A withdrawn service is sent as a tombstone. The manifest carries a version (grows by one on every change), a contentHash and its own signature (typ gsb-manifest+jws), so the hub can prove later where what it shows came from.

Push. Every save that changes a service's national entry marks it in the same transaction; after commit the node pushes PUT /fed/v1/catalogue/services/{id} (scope catalogue:write). This includes publish, pause, rollback, translations, intake windows and the ministry's public name, logo and contacts. If the content hash did not change, nothing is sent. Drafts never leave the node: with versioned publishing, a draft changes nothing until it is approved and published.

Hub checks. The sender must own the service; the ministry must be one the registry lists for the node; the content hash and the manifest signature must verify; the version must not be older than the one mirrored. Refusals include FED_NOT_OWNER, FED_HASH_MISMATCH, FED_MANIFEST_SIGNATURE, FED_STALE_VERSION and FED_VERSION_CONFLICT.

Pull. The hub's job fed-catalogue-pull runs every 15 minutes as a safety net. It pages through what changed and periodically compares a full listing, withdrawing services the node no longer lists.

Read side. The hub's catalogue adds the published services of active nodes, each marked with its provider. While a node is unreachable its services stay listed with stale: true. New applications get the new version; applications already in flight keep the version they were made under.

Applications

The national portal hosts the forms of node services; the node makes the case.

  1. The citizen starts an application on the hub, which autosaves answers and stages uploaded files after the usual

    upload checks (type by content, antivirus and so on).

  2. On submit, the hub checks the mirrored form's rules and required documents, then relays at once with

    submissions.create. The Idempotency-Key is the submission id, and a fresh citizen assertion goes with each attempt.

  3. Files travel as references: a short-lived, HMAC-signed link to the hub's staging area. The node fetches only the

    path, on its own configured HUB_URL, never the host named in the message, and checks size and SHA-256.

  4. The node validates the answers against its live form, runs its own upload intake, creates the case for the

    shadow applicant and answers with its case number.

  5. After the acknowledgement, the hub deletes the staged files and answers, keeping only hashes.

Possible answers: 201 accepted with the node's case number; 202 on its way (queued or sent); 422 refused, with field errors; 503 FED_NODE_UNAVAILABLE when a live-quoted fee cannot be queued.

If the node is down, the application is queued ("Received by the national portal ... will be sent when its system is back"). The hub job fed-submissions retries every minute with backoff, oldest first per node. After FED_SUBMISSION_QUEUE_DAYS (7) an undelivered application expires, its staged files are deleted and the citizen is told. Because the body and key are identical on every attempt, a retry after a lost answer still makes exactly one case. If the node published a new form meanwhile and a queued application no longer fits it, the node answers FED_FORM_CHANGED.

Status events and "My applications"

The node owns the case; the hub keeps an index row per application: case number, status word, actions waiting on the citizen, amount due, invoice and document references. No answers, documents, notes or messages.

The node writes CloudEvents in the same transaction as the change: case.submitted, case.status_changed, case.decided, case.closed, action.required, payment.invoiced, payment.settled, payment.refunded and document.issued. They are delivered as signed POST /fed/v1/events with the event id as Idempotency-Key, from the node's outbox, retried for about 22 hours before landing in a dead-letter list that can be replayed.

Each application's events carry a sequence with no holes. The hub applies them strictly in order: an old one is ignored as stale; one further ahead makes the hub fetch the missing events from the node's log (GET /fed/v1/events?record=&after=). While it cannot, it answers 503 FED_GAP_PENDING and the node sends again later. An older status never overwrites a newer one.

Citizen notices follow the same path: by default (NOTIFY_CITIZENS=hub) the node forwards the template key and a whitelist of variables, and the hub sends the SMS or e-mail in the citizen's language and preferences. No name, note or message is forwarded.

When the citizen opens an application's detail, the hub asks the node live with cases.applicantView; the detail is never stored on the hub. If the node is down the citizen sees the last known status (503 FED_NODE_UNREACHABLE).

Payments

  • The node raises the invoice on its own ZamPay merchant, receives ZamPay's callback on its own API and finalises

    the payment through its one payment path. Then it tells the hub with payment.invoiced and payment.settled.

  • The hub keeps an index for display only. It never holds a node's ZamPay keys and never marks a node invoice

    paid. A late invoiced event can never turn a paid invoice back to unpaid.

  • To pay, the hub asks the node (payments.get, with a citizen assertion). An open invoice carries a short-lived,

    node-signed checkout link; opening it, the node checks it is still that citizen's open invoice and redirects to its own ZamPay checkout.

  • Receipts on a node carry the node key in their reference, so the hub's receipt check is routed to the right node.

Documents and verification

  • Issued permits and case documents stay on the node. documents.link returns a link valid for 120 seconds, bound to

    the account. By default the hub fetches the bytes from the node's registry base URL over mTLS, checks the SHA-256 and streams them to the citizen without keeping a copy.

  • A permit's verification code issued on a node carries the node key. The hub's verification page relays such a code

    to verify.document on that node. Unknown codes, unknown nodes and a node's refusal all give the same NO_MATCH answer, so the page reveals nothing.

  • For numbers without a node key, the hub may fan out verify.lookup to nodes that advertised it, with limits on how

    many nodes, how fast and how often. "Could not check" (503) is never reported as "not found".

Check your understanding

  • What may a service manifest contain, and what may it never contain?
  • What happens to a node service's draft version on the hub?
  • Why does the node fetch staged files from its own HUB_URL and not from the host in the message?
  • What does the hub do with an event whose sequence is ahead of the last one it applied?
  • Who raises, finalises and indexes a node invoice?
  • Why does the hub answer NO_MATCH for an unknown node key as well as for an unknown code?

Sign in to record your progress.

Previous: Trust: signed messages, keys and JWKS Next: Onboarding a new MDA node