Reference

Cookbook

End-to-end recipes for the workflows you'll run most — from an everyday market-dues collection to a full cross-agency business registration. Every step is runnable against the live sandbox; citizens, mobile-money numbers, and expected payloads are wired up.

How to use these recipes

Each recipe is a complete, copy-pastable workflow against sente-rails.space/v1. Paste the curl commands in order and you'll drive the same state machines a counter clerk or an integrator app drives in production. They ramp in complexity — start with Recipe 1 and work down.

Test data reference

The sandbox is pre-seeded with forty-six agencies across twenty-six sectors, their service catalogues and fee schedules, and a set of citizens you can transact against. The three below appear throughout the recipes here.

PersonaNINDistrictUse for
John Patrick MukasaCM78001234ABCDGuluCounter-led collection & registration
Patrick Okello AkenaCM85042134GULUGuluCross-agency Lands scenarios
Robert Ssemakula MukasaCM81051712KLAAKampalaKampala-side Mode B scenarios

MoMo sandbox MSISDN: +256772123456. Any string ending with 123456 will resolve in the MTN sandbox. Other aggregator numbers are documented inside the Postman collection.

Recipe 1 — Everyday market-dues collection

The rail's highest-volume, most everyday transaction: a vegetable seller at Cereleno Market in Gulu pays her daily market dues — a small, fixed statutory fee — and a clerk rings it up in seconds. This is the bread-and-butter of local-government revenue, repeated thousands of times a day across the country. We use the pre-seeded Gulu citizen (CM78001234ABCD).

1. Open the counter shift

The clerk opens their till for the day with the cash already in the drawer (the opening float).

bash
curl -X POST https://sente-rails.space/v1/counter-shifts/open \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mda": "GULU", "opening_float": 100000 }'

2. Resolve the citizen

bash
curl https://sente-rails.space/v1/citizens/search-by-nin?nin=CM78001234ABCD \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY"

3. Assess the daily dues

One line, the daily vegetables-market fee. The price is set by the agency — the server fetches it from the service's fee schedule, so the amount can never be entered or undercut by the caller.

bash
curl -X POST https://sente-rails.space/v1/assessments \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "citizen": "CM78001234ABCD",
    "lines": [{ "service": "MD-VEGETABLES", "mda": "GULU" }]
  }'

Assessment returns Assessed with a total of 1500 UGX — the statutory daily fee.

4. Take the payment

At a market counter this is usually paid in cash (channel Cash); here we use the MoMo sandbox so the whole recipe runs end-to-end. The flow is identical either way.

bash
INTENT=$(curl -sS -X POST https://sente-rails.space/v1/payment-intents \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assessment": "ASSESS-2026-000200", "payment_channel": "MTN MoMo" }' \
  | jq -r '.data.name')

curl -X POST "https://sente-rails.space/v1/payment-intents/$INTENT/initiate" \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "payer_msisdn": "+256772123456" }'

On confirmation the dues are recorded against the seller, the funds are routed to Gulu's collection account, and the amount lands in the clerk's shift totals for end-of-day reconciliation. A 1,500-shilling market payment is now as traceable and verifiable as a bank transfer.

Recipe 2 — Counter-led trading licence renewal

A clerk at Gulu City Hall renews John Mukasa's trading licence end-to-end: NIN cascade, single-line assessment with an EFRIS fiscal document number (FDN), MoMo payment, and a receipt with a QR code that points at the public verifier. A routine licensing interaction, start to finish.

Sequence diagram available in the repository at docs/diagrams/04-workflow-vertical-1-trading-licence.svg.

1. Open the counter shift

bash
curl -X POST https://sente-rails.space/v1/counter-shifts/open \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mda": "GULU", "opening_float": 100000 }'

2. Resolve the citizen

bash
curl https://sente-rails.space/v1/citizens/search-by-nin?nin=CM78001234ABCD \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY"

3. Create the single-line assessment

bash
curl -X POST https://sente-rails.space/v1/assessments \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "citizen": "CM78001234ABCD",
    "lines": [{ "service": "TL-RENEW", "mda": "GULU" }]
  }'

Assessment status returns Assessed with a single FDN attached to the TL-RENEW line and total 50000 UGX.

4. Initiate the MoMo charge

bash
# create the intent
INTENT=$(curl -sS -X POST https://sente-rails.space/v1/payment-intents \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assessment": "ASSESS-2026-000123", "payment_channel": "MTN MoMo" }' \
  | jq -r '.data.name')

# charge
curl -X POST "https://sente-rails.space/v1/payment-intents/$INTENT/initiate" \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "payer_msisdn": "+256772123456" }'

5. Watch the trace

Either poll /trace or subscribe to the payment_intent.confirmed webhook (see Webhooks). On confirmation the trace carries the full state machine: initiated → confirmed → settled → propagated.

6. Verify the receipt as a citizen would

The printed receipt embeds a QR pointing at the public verifier. The citizen-facing summary at /verify/<payment-ref> is PII-safe and unauthenticated — anyone with the QR can confirm the payment is real:

bash
curl https://sente-rails.space/v1/payment-intents/PI-2026-000045/public-summary

Recipe 3 — Lands title transfer with EFRIS PRN

An API-led, cross-agency transaction handled entirely via /v1: two agencies in one assessment (Ministry of Lands title transfer and URA stamp duty), an EFRIS payment-registration number (PRN) generated per taxable line, a single unified MoMo charge, aggregator-level split disbursement to both agencies, and a side-by-side reconciliation against MTN's own API.

Sequence diagram in the repository at docs/diagrams/05-workflow-vertical-2-lands-title.svg.

1. Build the two-line cross-agency assessment

bash
curl -X POST https://sente-rails.space/v1/assessments \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "citizen": "CM85042134GULU",
    "lines": [
      { "service": "TITLE-TRANSFER", "mda": "MOL" },
      { "service": "STAMP-DUTY",     "mda": "URA" }
    ]
  }'

Response carries cross_mda: true, two FDNs (one per EFRIS-taxable line), and a total of 90000 UGX.

2. Initiate unified payment via MoMo

One MoMo charge for the whole 90,000. The split disbursement happens on the aggregator side after confirmation — Sente Rails never holds the money in between.

bash
INTENT=$(curl -sS -X POST https://sente-rails.space/v1/payment-intents \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assessment": "ASSESS-2026-000124", "payment_channel": "MTN MoMo" }' \
  | jq -r '.data.name')

curl -X POST "https://sente-rails.space/v1/payment-intents/$INTENT/initiate" \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "payer_msisdn": "+256772123456" }'

3. Side-by-side reconciliation

The /live-status endpoint queries the aggregator directly and returns both records side by side — the "our records match the aggregator's records" reconciliation proof, on demand:

bash
curl "https://sente-rails.space/v1/payment-intents/$INTENT/live-status" \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY"
Response shape · json
{
  "data": {
    "rail": {
      "status": "Confirmed",
      "aggregator_reference": "MOMO-SBX-2026-7891",
      "paid_at": "2026-05-25T08:30:09Z"
    },
    "aggregator": {
      "status": "SUCCESSFUL",
      "transaction_id": "MOMO-SBX-2026-7891",
      "amount": 90000,
      "currency": "UGX"
    },
    "match": true
  }
}

Recipe 4 — Cross-agency business registration

The rail's most ambitious flow: three agencies touched in a single transaction — URSB (name reservation + company registration), URA (TIN issuance), and Gulu City (new trading licence). Parallel propagation means all three artefacts are issued concurrently on payment confirmation, collapsing a multi-office errand into one interaction.

Sequence diagram in the repository at docs/diagrams/06-workflow-vertical-3-cross-mda.svg.

1. Resolve the prospective director

bash
curl https://sente-rails.space/v1/citizens/search-by-nin?nin=CM78001234ABCD \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY"

2. Build the four-line assessment across three agencies

bash
curl -X POST https://sente-rails.space/v1/assessments \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "citizen": "CM78001234ABCD",
    "lines": [
      { "service": "NAME-RESERVE", "mda": "URSB" },
      { "service": "COMPANY-REG",  "mda": "URSB" },
      { "service": "TIN-REG",      "mda": "URA"  },
      { "service": "TL-NEW",       "mda": "GULU" }
    ]
  }'

Total 350000 UGX. Three FDNs attached (TIN-REG is UGX 0, no FDN). Response includes cross_mda: true and mdas_count: 3.

3. Single MoMo charge, three propagation webhooks

bash
INTENT=$(curl -sS -X POST https://sente-rails.space/v1/payment-intents \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assessment": "ASSESS-2026-000125", "payment_channel": "MTN MoMo" }' \
  | jq -r '.data.name')

curl -X POST "https://sente-rails.space/v1/payment-intents/$INTENT/initiate" \
  -H "X-Sente-Authorization: Bearer $SENTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "payer_msisdn": "+256772123456" }'

On confirmation: one payment_intent.confirmed webhook, three assessment.propagated webhooks (one per agency), and the rail's ledger reflects URSB ← 300k / URA ← 0 / Gulu ← 50k at the aggregator level.

4. The headline outcome

From a cold start (no prior citizen lookup) this recipe completes in roughly ninety seconds against the sandbox — under thirty minutes from idea to a formalised business once you allow for the human steps in between. What used to be days of queuing is now a single counter interaction: same legal artefacts, same statutory fees, a fraction of the operational distance.

What this replaces

The traditional path for a Ugandan business registration: three separate government offices, three separate queues, three separate payment windows, paper-based handoffs between offices, and a typical multi-day turnaround. This recipe collapses it to a single counter interaction — same legal artefacts, same statutory fees, dramatically shorter operational distance.