modules

Payments module

Connect Paystack, MTN MoMo, or Hubtel so your customers can pay you and so you can buy messaging credits.

Updated 2026-04-19

Two different payment flows

Opterflow handles two separate payment scenarios:

  1. Your customers pay you — someone pays your invoice through Paystack checkout, MTN MoMo prompt, or Hubtel. Money goes to your settlement account.
  2. You pay Opterflow for messaging credits — you top up SMS/WhatsApp credits via Paystack. Money goes to Opterflow.

Both live under the Payments module but use different plumbing.

Provider capabilities in Ghana

Provider Card MoMo (MTN) MoMo (Vodafone/AirtelTigo) Bank Fee Setup
Paystack ✓ ✓ ✓ ✓ ~1.95% + GHS 0.50 Easy — we create a subaccount under Opterflow
MTN MoMo direct ✗ ✓ ✗ ✗ ~1% Medium — you bring your own API creds
Hubtel ✓ ✓ ✓ ✓ ~1.5–2.5% Medium — you bring merchant account # + client ID/secret

Recommendation: connect Paystack first. Broadest coverage, easiest onboarding. Add MTN or Hubtel later if you want lower fees on MoMo-heavy flows.

Connecting Paystack

  1. Go to Settings → Payments
  2. Click Connect Paystack
  3. Fill in:
    • Business name (prefilled from your org)
    • Settlement bank — dropdown of Ghana banks
    • Settlement account number — where Paystack will deposit your money (auto-validates via Paystack; shows account holder name on blur)
    • Primary contact — your name, email, phone
  4. Submit. The status card shows pending while Paystack verifies.
  5. Once Paystack confirms the account (usually minutes), their webhook flips your status to connected.

What Opterflow stores: just the Paystack subaccount code. No secret keys — Paystack routes payments through our master account and splits the subaccount's cut at settlement time.

Connecting MTN MoMo direct

Harder than Paystack. You need to onboard with MTN's developer portal first, then paste creds here.

  1. Go to MTN's MoMo developer portal and provision Collections API credentials. You'll get:
    • API User (UUID)
    • API Key (secret)
    • Subscription Key (Ocp-Apim-Subscription-Key)
  2. In Opterflow: Settings → Payments → Connect MTN MoMo
  3. Paste all three + pick environment (sandbox / mtnghana)
  4. Opterflow encrypts them with AES-256-GCM before writing to the DB (requires PAYMENTS_KEK_BASE64 env var in Vercel)
  5. Validates by minting a bearer token. Success → status=connected. Failure → 400 with the reason.

Connecting Hubtel

  1. Open a Hubtel merchant account at hubtel.com
  2. Grab your Merchant Account Number, Client ID, and Client Secret
  3. In Opterflow: Settings → Payments → Connect Hubtel
  4. Paste all three. Opterflow encrypts and validates.

Requesting payment on an invoice

Invoices → click any invoice → Request pay button.

  • If you have one provider connected, it uses that.
  • If you have multiple, you're prompted to pick.

Paystack opens a checkout URL in a new tab. MTN MoMo sends a push prompt to the customer's phone (no URL). Hubtel opens a checkout URL.

Idempotent: clicking twice reuses the same PaymentIntent, no duplicate charges.

How we know a payment succeeded

Each provider fires a webhook. Opterflow verifies the signature (or shared secret for Hubtel/Unipile), deduplicates against PaymentWebhookEvent, and on success:

  1. Marks the PaymentIntent succeeded
  2. Flips the Invoice to paid with paidAt = now
  3. Creates a Receipt row with the reference
  4. Emits a ContactSignal(type='purchase') — this is what lets Campaign v2 branches fire on "did they pay"

Reconciliation cron

MTN MoMo callbacks are notoriously flaky. There's a cron at /api/cron/payments-reconcile that runs every 5 minutes:

  • Finds intents stuck in processing for > 5 minutes
  • Calls getStatus() on the provider
  • If SUCCESSFUL or FAILED, applies the same update path as the webhook

Paystack doesn't need this (their webhook is reliable). Hubtel gets reconciled too.

Messaging top-up (you paying Opterflow)

Separate flow. Open Settings → Messaging → Top Up Credits.

  • Admin or manager can submit
  • Pick channel (SMS / WhatsApp) + amount
  • Two payment paths:
    • "Pay with Paystack" (primary, blue CTA) — instant. Paystack webhook auto-approves the top-up. Credits applied within seconds.
    • "Bank transfer / cash" (secondary) — creates a topup_pending transaction. Super admin manually approves from /admin/messaging-topups after confirming payment off-platform.

Both paths work. Paystack is faster and doesn't need human approval.

Viewing transaction history

/payments shows every PaymentIntent for your org, filterable by provider, status, purpose, date range. Click a row → detail drawer with full webhook event timeline (every callback Paystack/MTN/Hubtel sent us).

Security

  • Per-tenant encrypted credentials (MTN, Hubtel) — AES-256-GCM with PAYMENTS_KEK_BASE64. DB dump leaks ciphertext only.
  • Webhook signature verification on every callback. Wrong signature = 401.
  • Amount tampering defence — if webhook amount ≠ intent amount, intent marked failed, invoice stays unpaid, flagged for super admin.
  • Disconnect clears ciphertext — revoking a provider zeros the encrypted columns.

Common mistakes

  • Paystack test mode keys in prod — Paystack sandbox sk_test_ keys can't charge real cards. Check your env vars.
  • Cloudflare proxy on webhook URLs — if webhooks go through Cloudflare's orange-cloud, raw body bytes may differ, breaking signature verification. Turn proxy OFF on the subdomain that serves /api/webhooks/*.
  • Missing callback URL on MTN MoMo — without MTN_MOMO_CALLBACK_HOST, MTN can't notify us. Reconciliation cron will catch it, but with 5-min lag.
  • Fees passed to customer. Default is tenant absorbs. Per-org passFeesToCustomer toggle exists but UI surface is coming in v1.1.

Was this article helpful?