The Press
Field NotesIssue 001 · The Launch Issue

The PayFast Universal Hub: One Webhook, Six Applications, Zero Leakage

PayFast only allows one set of ITN webhook URLs per merchant account. I have six applications. Here's the routing architecture that makes one webhook serve all of them, with full tenant isolation.

Nandawula Regine9 March 20267 min read

# The PayFast Universal Hub: One Webhook, Six Applications, Zero Leakage

PayFast's merchant dashboard allows one set of return, cancel, and notify URLs per account. One notify URL. One merchant account.

I have six applications that process payments through a shared PayFast merchant account:

  • AdminOS (subscription tiers)
  • VarsityOS (premium features)
  • StokvelOS (membership)
  • K53 Drill Master (premium game modes)
  • WatchSankofa (creator payouts, subscriptions)
  • Sanyu Botanicals (product sales)

The solution: creativelynanda.co.za is the universal PayFast hub.

The Architecture

Every PayFast payment across all six applications is initiated with a structured m_payment_id:

m_payment_id format: {app}_{userId36}_{tier}_{timestamp}
Examples:
  adminos_abc123_growth_1717891200
  k53_def456_premium_1717891300
  varsityos_ghi789_starter_1717891400

The prefix before the first underscore identifies the application. The universal ITN handler at /api/payfast/universal-notify reads this prefix and routes the payment confirmation to the correct application's Supabase instance.

The Handler

export async function POST(req: NextRequest) {
  const body = await req.formData();
  const mPaymentId = body.get('m_payment_id') as string;
  const app = mPaymentId.split('_')[0]; // 'adminos', 'k53', etc.

  const config = getAppConfig(app);
  if (!config) return new Response('Unknown app', { status: 400 });

  // Verify signature with app-specific passphrase
  const isValid = await verifySignature(body, config.passphrase);
  if (!isValid) return new Response('Invalid signature', { status: 403 });

  // Route to app-specific handler
  return config.handler(body, config.supabase);
}

Each application has its own Supabase instance, its own passphrase, and its own handler function. The routing layer is the only place all six coexist.

Signature Verification: The Bug That Taught Me Most

PayFast ITN verification requires regenerating the signature from the POST body and comparing it to the signature field. Simple in principle; subtle in practice.

The bug I encountered in production: my original implementation sorted the POST parameters alphabetically before generating the signature. PayFast verifies using insertion order, the order the payment form submitted the fields.

The fix was removing the .sort() call. Three characters removed. One hour of debugging that taught me more about the difference between "the API docs say" and "the API actually does" than any tutorial has.

The lesson: always verify your signature implementation against PayFast's PHP reference implementation, not against your intuition about how alphabetical sorting should work.

The Return and Cancel Pages

PayFast's dashboard URL configuration is the global fallback. Each application overrides it per-payment using PayFast's per-payment URL fields:

return_url: https://creativelynanda.co.za/payfast/return?app=adminos
cancel_url: https://creativelynanda.co.za/payfast/cancel?app=adminos
notify_url: https://creativelynanda.co.za/api/payfast/universal-notify

The return and cancel pages read the ?app= parameter and render the appropriate branded experience: AdminOS branding for AdminOS payments, K53 branding for K53 payments.

Without the ?app= parameter (the PayFast dashboard fallback), both pages render a generic Mirembe Muse branded experience.

What This Architecture Makes Possible

Adding a new application to the payment hub requires four steps:

  1. Add a case 'newapp': block in getAppConfig()
  2. Add the app to APP_CONFIGS in the return and cancel pages
  3. Add three Vercel environment variables
  4. Set m_payment_id correctly in the new app's initiate route

A new revenue stream in an afternoon.

What infrastructure would let you add a new product in an afternoon?

Colophon

Imprint
Field Notes
Issue
Issue 001, April 2026
Published
9 March 2026
Length
501 words
Drafts on file
None yet
Set in
Cormorant Garamond & DM Sans

Published by The House of Roses Press, KuGompo City.

1 views

Reader Insights

0 responses

No insights yet. Be the first!