Payment Workflow (Invoice-First, Shared Pay Page)¶
Last Updated: May 2026
Applies to: All services that collect payments (ordering, subscription, cafe-website, treasury-ui). Treasury-api is the single source for payment intents and gateway orchestration.
Overview¶
Payments follow an invoice-first flow:
- A service (ordering-backend, subscription-api, cafe-website backend, etc.) creates a payment intent (invoice) via treasury-api with payment details and
payment_method: "pending". - Treasury-api returns intent_id and invoice details (amount, currency, reference_id, reference_type).
- The service’s frontend redirects the user to the shared treasury-ui pay page with these details (and optional
intent_id,invoice_number,initiate_url, etc.) in the URL. - The pay page shows an invoice summary at the top, then lists active payment gateways (Paystack, M-Pesa, COD). The user picks one; the corresponding payment modal opens with the same payment context.
- When the user completes the flow (redirect to Paystack, STK push, or COD/manual confirmation), the intent is updated via webhooks or API; the user is sent to the callback page or redirect_url with a configurable button.
1. Create Invoice (Payment Intent) — Treasury API¶
Endpoint: POST /api/v1/{tenant}/payments/intents
Auth: Required (service backend calls with valid JWT).
Request (invoice-only; user will choose method on pay page):
{
"reference_id": "order-123",
"reference_type": "order",
"payment_method": "pending",
"currency": "KES",
"amount": 1500,
"source_service": "ordering",
"description": "Order #123"
}
- Use
payment_method: "pending"(or omit) to create the intent without initiating any gateway. No redirect or STK is sent yet. source_serviceis REQUIRED for revenue attribution. Each service must use its canonical identifier:ordering— ordering-backend (online delivery orders)pos— pos-api (point-of-sale transactions)subscriptions— subscriptions-api (plan billing)logistics— logistics-api / truload (delivery/weighing fees)cafe— cafe-website (direct cafe orders)isp— isp-billing (ISP payments)- Treasury-api uses
source_serviceto attribute revenue per service and generate per-service analytics. - Response:
intent_id,status,amount,currency,reference_id,reference_type. The service stores or passes these to the frontend.
Optional: If the service already knows the method (e.g. “paystack” or “mpesa”), it can send that and treasury-api will initiate the gateway immediately (existing behavior). For the shared pay page flow, use "pending".
2. Initiate Payment for an Existing Intent — Treasury API¶
After the user selects a gateway on the pay page, the calling service’s backend (or a proxy) calls:
Endpoint: POST /api/v1/{tenant}/payments/intents/{intentID}/initiate
Auth: Required.
Request:
{
"payment_method": "paystack",
"customer_email": "user@example.com"
}
- For M-Pesa:
payment_method: "mpesa",phone_number: "254712345678". - For COD:
payment_method: "cash"— intent is marked succeeded without gateway. - For manual / paid at till:
payment_method: "manual"or"till"— intent is marked succeeded without calling the gateway (user confirmed they paid at agent/till).
Response: Same shape as create intent — e.g. authorization_url (Paystack), checkout_request_id (M-Pesa), or status: "succeeded" for cash/manual.
3. Confirm Manual Payment (Paid at Till) — Treasury API¶
When the user clicks “I paid at till / agent” in the UI, the service backend can either:
- Call initiate with
payment_method: "manual"(see above), or - Call confirm-manual for the intent:
Endpoint: POST /api/v1/{tenant}/payments/intents/{intentID}/confirm-manual
Auth: Required.
Body: Empty.
Effect: Intent status set to succeeded; payment_method can be updated to "manual".
4. Shared Pay Page (Treasury UI)¶
URL: Public; e.g. https://books.codevertexafrica.com/pay
Route: /(public)/pay (no auth).
Query parameters (passed by the service frontend when redirecting):
| Parameter | Required | Description |
|---|---|---|
amount |
Yes* | Amount to pay (*or intent_id if amount from server) |
tenant |
Yes | Tenant ID (UUID) |
intent_id |
No | Treasury payment intent ID (invoice-first flow) |
invoice_number |
No | Display only (e.g. INV-xxx or reference_id) |
reference_id |
No | External reference (order id, subscription id) |
reference_type |
No | e.g. order, subscription |
currency |
No | Default KES |
description |
No | Shown in summary |
redirect_url |
No | Where to send user after payment (path or URL) |
button_text |
No | Label for post-payment button (e.g. “View my order”) |
initiate_url |
No | Service backend URL to POST to initiate/confirm |
gateways |
No | Comma-separated: paystack, mpesa, cod (default: all) |
Page behavior:
- Renders invoice summary at top (invoice number, amount, description).
- Renders gateway cards with official logos (Paystack, M-Pesa, COD) for the chosen
gateways. - On gateway click, opens the respective payment modal (Paystack, M-Pesa, or COD) with the same payment details.
initiate_url (recommended): The pay page and modals POST to this URL with body e.g. { intent_id, payment_method, customer_email?, phone_number? }. The service backend then calls treasury-api POST .../intents/{id}/initiate (or confirm-manual) and returns authorization_url, checkout_request_id, or redirect_url so the UI can redirect or show “Check your phone”.
5. Payment Modals (Flexible, QR & Manual)¶
All payment dialogs in treasury-ui support:
- Standard flow: Paystack → redirect to
authorization_url; M-Pesa → STK push, then optional “Check your phone” / redirect; COD → confirm and mark succeeded. - QR code: When an
authorization_urlis returned (e.g. Paystack), the Paystack modal can show a QR code for that URL so the user can scan and pay on another device. - Manual / till: An “I paid at till / agent” (or similar) button that POSTs
payment_method: "manual"toinitiate_url; the backend calls treasury-api initiate with"manual"or confirm-manual, then the UI redirects toredirect_url.
This applies in all services that use the shared pay page and in treasury-ui itself when it hosts payment flows.
6. Callback Page (After Payment)¶
After Paystack (or other redirect-based gateways) complete, the user lands on a callback page with ?reference=... (and optionally redirect_url, button_text). See paystack-callback-page.md. The callback page is public; it may call a verify endpoint and then show a single button (custom text and URL) to continue.
7. Webhooks & Auto-Generated URLs¶
- Webhook and callback URLs for Paystack and M-Pesa are auto-generated in treasury-api from
HTTP_PUBLIC_BASE_URLand fixed paths (see treasury-api docs and paystack-and-platform-admin.md). - Production base URL is set in
devops-k8s/apps/treasury-api/values.yamlasTREASURY_HTTP_PUBLIC_BASE_URL.
8. Embedded Payment via TreasuryPaymentModal (Iframe)¶
For services that want to trigger payment without redirecting to treasury-ui directly, the @bengo-hub/shared-ui-lib provides TreasuryPaymentModal — a React component that embeds treasury-ui in a modal iframe.
import { TreasuryPaymentModal } from "@bengo-hub/shared-ui-lib/payments";
<TreasuryPaymentModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
intentId={intentId} // from treasury-api POST .../intents response
customerEmail={user.email}
onPaymentSuccess={(result) => handleSuccess(result)}
onPaymentFailed={(err) => handleError(err)}
/>
postMessage events (from iframe → parent):
- treasury:payment_initiated — gateway flow started
- treasury:payment_confirmed — payment succeeded
- treasury:payment_failed — payment failed
- treasury:resize — iframe height changed (handled automatically)
Paystack redirect URL (v0.1.5+) includes intent_id and amount as query params so the callback page can verify and display the correct order details.
Services using it: ordering-frontend, pos-ui, cafe-website, subscriptions-ui, notifications-ui, inventory-ui, truload-frontend — check the consuming service's package.json for the exact @bengo-hub/shared-ui-lib version in use, since it's updated frequently.
References¶
- Paystack callback page
- Treasury API Paystack & platform admin
- Treasury API integrations
- Treasury-ui pay page:
finance-service/treasury-ui/src/app/(public)/pay/page.tsx - Payment modals:
finance-service/treasury-ui/src/components/payments/ - TreasuryPaymentModal:
shared/shared-ui-lib/src/components/payments/treasury-payment-modal.tsx