Flutterwave is the payment gateway I get asked about most often on projects that need to reach customers across multiple African countries from a single integration, cards, mobile money like M-Pesa and MTN MoMo, and bank transfers, rather than wiring a separate processor per corridor. There's no official Acumatica connector for it, and I haven't found one from a third party either, so every implementation I've built is a middle layer sitting between Acumatica's contract-based REST API and Flutterwave's own API. The shape of that layer matters more than any individual endpoint call, because the easy way to build it is also the way that lets a forged webhook mark an unpaid invoice as paid.
The flow: a payment reference tied to an AR Invoice
The pattern that holds up is to generate the Flutterwave payment reference from Acumatica, not the other way around. When a customer needs to pay an AR Invoice or a Sales Order, the middle layer calls Flutterwave's Payments initialization endpoint with a unique transaction reference you control, tied back to the Acumatica document, gets a hosted checkout link in return, and redirects the customer there. Flutterwave owns the checkout experience, the card or mobile money prompt, the 3-D Secure step, all of it, and Acumatica's job is just to know which of its own documents that reference belongs to.
$txRef = 'INV-' . $invoice->refNbr . '-' . Str::random(6); // unique per attempt
$response = Http::withToken(config('flutterwave.secret_key'))
->post('https://api.flutterwave.com/v3/payments', [
'tx_ref' => $txRef,
'amount' => $invoice->docBal,
'currency' => $invoice->curyId,
'redirect_url' => route('flutterwave.callback'),
'customer' => [
'email' => $invoice->customerEmail,
'phonenumber' => $invoice->customerPhone,
],
'meta' => ['acumatica_invoice_ref' => $invoice->refNbr],
]);
// Store $txRef against the invoice before redirecting, so the webhook
// has something to match against later.
PaymentAttempt::create(['tx_ref' => $txRef, 'invoice_ref' => $invoice->refNbr, 'status' => 'pending']);
return redirect($response->json('data.link'));
Storing the pending attempt before redirecting the customer matters: it's what lets the webhook handler look up "which invoice does this tx_ref belong to" without trusting anything else in the payload.
Verify the webhook signature before you trust "payment succeeded"
Flutterwave confirms payment through a webhook, and that webhook is the part I'd flag as the actual security-sensitive piece of this integration. Flutterwave signs webhook payloads with a secret hash you configure in your dashboard, sent back as a header on each webhook request. If your endpoint acts on a "charge.completed" event without checking that header against your own copy of the secret, you've built an endpoint that marks any invoice paid for anyone who can guess or replay a payload shape, which is a fraud vector, not a hypothetical one.
Route::post('/webhooks/flutterwave', function (Request $request) {
$signature = $request->header('verif-hash');
$expected = config('flutterwave.webhook_secret_hash');
if (!$signature || !hash_equals($expected, $signature)) {
abort(401); // do not process, do not touch AR, just reject
}
$payload = $request->json()->all();
if ($payload['event'] !== 'charge.completed' || $payload['data']['status'] !== 'successful') {
return response()->json(['status' => 'ignored'], 200);
}
ApplyFlutterwavePayment::dispatch($payload['data']['tx_ref'], $payload['data']['amount']);
return response()->json(['status' => 'accepted'], 200);
});
Flutterwave's verif-hash is a static value you set in your dashboard and compare against verbatim — it isn't an HMAC computed over the payload, so don't write comparison logic that tries to re-derive a hash from the body. hash_equals against the stored secret is the whole check. Skipping the constant-time comparison and using == instead is a smaller but real mistake on top of skipping verification entirely.
Idempotency: a replayed webhook can't double-apply a payment
Webhooks get retried by design, Flutterwave will resend if your endpoint doesn't acknowledge fast enough, and a network blip on your side can produce a duplicate delivery of an event you already processed. If the handler applies a payment to AR every time it sees a successful charge.completed for a given tx_ref, the second delivery double-pays the invoice. The fix is to key on the tx_ref and check the current state of that payment attempt before applying anything: if it's already marked applied, acknowledge the webhook with 200 and do nothing else. That check has to live in the same transaction as the AR update, not as a separate step, or two near-simultaneous deliveries can both pass the check before either writes the result.
Reconcile against Flutterwave's settlement report, not the webhook alone
Even with signature verification and idempotency handled, I don't treat the webhook as the sole source of truth that a payment happened. Webhooks can be delayed by minutes, dropped entirely during an outage on either side, or in rare cases arrive for an event your endpoint never finishes processing before a deploy restarts it. None of that is unique to Flutterwave, it's true of any webhook-driven confirmation, and the standard mitigation is the same one: pull Flutterwave's settlement or transaction report on a schedule (daily is usually enough, more often for higher-volume accounts) and reconcile it against what Acumatica has recorded as paid. Anything Flutterwave shows as settled that never landed in Cash Management is a webhook that got lost, and it needs a manual or automated catch-up path, not a shrug.
Wrapping up
There's no packaged Flutterwave connector for Acumatica, so the integration is whatever middle layer you build: generate the payment reference from the AR Invoice or Sales Order, send the customer to Flutterwave's hosted checkout, and let the webhook confirm the result. The part that actually determines whether this is safe is verifying the webhook's signature header before acting on it, since an unverified "payment succeeded" event is a direct fraud path into AR. Layer idempotency on the transaction reference so a retried webhook can't double-apply a payment, and run a scheduled reconciliation against Flutterwave's settlement report regardless, because webhooks are a convenience, not a guarantee, and the report is the only thing that tells you what actually got paid.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.