Paystack frontend callback page (flexible, reusable)¶
All services that integrate with treasury and use Paystack (ordering, subscriptions, treasury-ui, cafe, etc.) should use a flexible, public payment callback page so that after Paystack redirects the user back, they see a consistent experience with a single primary button whose label and destination are configurable per service.
Payment flow context: Payments use an invoice-first flow: the service creates a payment intent (invoice) via treasury-api, then redirects the user to the shared treasury-ui pay page (/pay) with invoice details; the user selects a gateway and completes payment. After Paystack, the user is sent to this callback page. See payment-workflow.md for the full flow.
Contract (same for every app)¶
- Route: Public, no auth. Example:
/(public)/payment/callbackor/payment/callback. - Query params (Paystack sends
reference; your app can append the rest when building the callback URL): reference(required) – from Paystack redirect (referenceortrxref).redirect_url(optional) – where to send the user after payment (path or full URL). Default:/or app home.button_text(optional) – label for the primary button. Default: e.g. "Continue" or "Back to dashboard".payment_type(optional) – used by the page to choose copy or verify API (e.g.order,subscription,billing).tenantororg(optional) – tenant slug for tenant-scoped verify APIs.
Building the callback URL when creating the intent¶
When your frontend (or backend) creates a Paystack payment intent, set the redirect (callback) URL to your own frontend callback page and pass the desired post-payment destination and button text:
{origin}/payment/callback?redirect_url={encodeURIComponent('/orders')}&button_text={encodeURIComponent('View my orders')}
Example by service:
- Ordering:
redirect_url=/ordersor/{orgSlug}/orders,button_text=View my orders - Subscriptions:
redirect_url=/billing,button_text=Back to billing - Treasury / general:
redirect_url=/,button_text=Go to dashboard
Paystack will append &reference=... when redirecting. The callback page reads reference, redirect_url, and button_text and shows a primary button with that label linking to that URL.
Page behavior¶
- Public – no authentication; anyone can land here from Paystack redirect.
- Loading – show a short “Verifying payment…” state.
- Verify (optional) – if the app configures a verify URL (e.g. from env
NEXT_PUBLIC_PAYMENT_VERIFY_URLor a per–payment_type verify endpoint), call it withreferenceand use the result to show success / failed / pending. - Result – show success, failed, or pending and a single primary button: custom
button_text→redirect_url(or “Try again” / “Back” on failure). - Support – optional footer with support contact (e.g. mailto, phone).
Reuse across apps¶
- Same pattern everywhere: Each app (ordering-frontend, subscription-ui, treasury-ui, cafe-website, etc.) implements one public callback page that respects the query params above.
- Reference implementation: See
finance-service/treasury-ui/src/app/(public)/payment/callback/page.tsxfor a minimal implementation that usesredirect_urlandbutton_textand optional verify. - ISP Billing: The same idea is used in
ISPBilling/isp-billing-frontendat(public)/payment/callbackwithpayment_typeandorgto vary copy and buttons per product (hotspot, PPPoE, SMS, etc.). Services that use treasury can use the simpler contract above (redirect_url + button_text) or extend with payment_type for custom copy.
Middleware¶
Ensure the callback route is public (no auth redirect). For example in Next.js middleware, exclude /payment/callback from auth checks (see isp-billing-frontend middleware.ts).
Production base URLs¶
- Treasury API base URL (for auto-generated webhook/callback URLs in the backend) is set in devops-k8s only:
devops-k8s/apps/treasury-api/values.yamlasTREASURY_HTTP_PUBLIC_BASE_URL(same host as ingress, e.g.https://booksapi.codevertexafrica.com). In production, do not duplicate this; the chart injects it from values. Local dev can useHTTP_PUBLIC_BASE_URLorTREASURY_HTTP_PUBLIC_BASE_URLin.env.