Skip to content

Custom Gateways & Extensibility ​

jengo/pesa is architected around clean interfaces and manager-driven resolution, making it easy to create and register custom payment gateway drivers (e.g. Flutterwave, Paystack, DPO Group, Bank APIs).


1. Creating a Gateway Driver ​

Extend Jengo\Pesa\Drivers\AbstractGateway and implement the capability interfaces your gateway supports:

  • StkPushInterface: For synchronous mobile money prompts (STK push, USSD push).
  • HostedCheckoutInterface: For hosted redirect payment flows.
  • B2CInterface: For payouts and business-to-customer disbursements.
  • C2BInterface: For customer-to-business URL registration.
php
<?php

namespace App\Gateways;

use Jengo\Pesa\Contracts\HostedCheckoutInterface;
use Jengo\Pesa\Contracts\WebhookHandlerInterface;
use Jengo\Pesa\Drivers\AbstractGateway;
use Jengo\Pesa\DTO\CheckoutRequest;
use Jengo\Pesa\DTO\CheckoutResponse;
use Jengo\Pesa\Exceptions\GatewayRequestException;

class FlutterwaveGateway extends AbstractGateway implements HostedCheckoutInterface
{
    protected ?FlutterwaveWebhookHandler $webhookHandler = null;

    public function getName(): string
    {
        return 'flutterwave';
    }

    public function getWebhookHandler(): WebhookHandlerInterface
    {
        if ($this->webhookHandler === null) {
            $this->webhookHandler = new FlutterwaveWebhookHandler($this->config['secret_hash'] ?? '');
        }
        return $this->webhookHandler;
    }

    public function checkout(CheckoutRequest $request): CheckoutResponse
    {
        $secretKey = (string) ($this->config['secret_key'] ?? '');
        $merchantRef = $request->reference ?? ('FLW-' . uniqid());

        $payload = [
            'tx_ref'          => $merchantRef,
            'amount'          => $request->amount,
            'currency'        => $request->currency,
            'redirect_url'    => $request->callbackUrl,
            'customer' => [
                'email'        => $request->email,
                'phonenumber'  => $request->phone,
            ],
            'customizations' => [
                'title'       => $request->description,
            ],
        ];

        try {
            $response = $this->getHttpClient()->post('https://api.flutterwave.com/v3/payments', [
                'headers' => [
                    'Authorization' => 'Bearer ' . $secretKey,
                    'Content-Type'  => 'application/json',
                ],
                'json' => $payload,
            ]);

            $data = json_decode((string) $response->getBody(), true);
            $redirectUrl = $data['data']['link'] ?? '';

            if ($response->getStatusCode() === 200 && ! empty($redirectUrl)) {
                // Record in ledger
                $this->recordPendingTransaction(
                    gatewayReference: $merchantRef,
                    type: 'hosted_checkout',
                    amount: $request->amount,
                    currency: $request->currency,
                    reference: $merchantRef,
                    email: $request->email,
                    rawRequest: $payload
                );

                return new CheckoutResponse(
                    successful: true,
                    redirectUrl: $redirectUrl,
                    gatewayReference: $merchantRef,
                    reference: $merchantRef,
                    raw: $data
                );
            }

            return new CheckoutResponse(successful: false, raw: $data);
        } catch (\Throwable $e) {
            throw new GatewayRequestException('Flutterwave error: ' . $e->getMessage(), 0, [], $e);
        }
    }
}

2. Implementing the Webhook Handler ​

Implement WebhookHandlerInterface to parse and verify incoming HTTP notifications:

php
<?php

namespace App\Gateways;

use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Jengo\Pesa\Contracts\WebhookHandlerInterface;
use Jengo\Pesa\DTO\WebhookPayload;
use Jengo\Pesa\Exceptions\WebhookVerificationException;

class FlutterwaveWebhookHandler implements WebhookHandlerInterface
{
    public function __construct(protected string $secretHash) {}

    public function handle(IncomingRequest $request): WebhookPayload
    {
        $signature = $request->getHeaderLine('verif-hash');

        if ($signature !== $this->secretHash) {
            throw new WebhookVerificationException('Invalid Flutterwave webhook signature header.');
        }

        $rawBody = (string) $request->getBody();
        $data = json_decode($rawBody, true) ?? [];

        $status = strtolower($data['data']['status'] ?? '');
        $isSuccess = ($status === 'successful');

        return new WebhookPayload(
            gateway: 'flutterwave',
            event: $isSuccess ? 'payment.success' : 'payment.failed',
            gatewayReference: (string) ($data['data']['tx_ref'] ?? ''),
            reference: (string) ($data['data']['tx_ref'] ?? ''),
            receiptNumber: (string) ($data['data']['flw_ref'] ?? ''),
            amount: (float) ($data['data']['amount'] ?? 0.0),
            currency: (string) ($data['data']['currency'] ?? 'USD'),
            payerEmail: (string) ($data['data']['customer']['email'] ?? ''),
            raw: $data
        );
    }

    public function createAcknowledgmentResponse(WebhookPayload $payload): ResponseInterface
    {
        return Services::response()->setStatusCode(200)->setJSON(['status' => 'acknowledged']);
    }
}

3. Registering the Driver with PesaManager ​

Register your custom gateway driver during application bootstrap (e.g. in app/Config/Events.php on pre_system or in a Service Provider):

php
use App\Gateways\FlutterwaveGateway;
use CodeIgniter\Events\Events;
use Jengo\Pesa\Pesa;

Events::on('pre_system', static function () {
    Pesa::getManager()->extend('flutterwave', static function ($config, $pesaConfig) {
        $flwConfig = $pesaConfig->gateways['flutterwave'] ?? [];
        return new FlutterwaveGateway($flwConfig, $pesaConfig);
    });
});

Now you can invoke your custom gateway anywhere in your application:

php
$checkout = Pesa::gateway('flutterwave')->checkout($request);

And webhooks will be automatically routed and verified at /pesa/webhook/flutterwave!

Released under the MIT License.