Start here
Quickstart
From nothing to a paid test payment, in six steps.
This is the whole integration, in the order it is done. It ends with a real (test) payment landing in the dashboard feed. Budget twenty minutes, most of it waiting for a provider dashboard to load.
-
1
Connect a provider in test mode
In the dashboard open Providers → Add a provider, choose Paystack, Flutterwave or OPay, pick Test keys, and paste the test public and secret keys from that provider's own dashboard (Paystack: Settings → API Keys & Webhooks. Flutterwave: Settings → API Keys. OPay: API Keys & Webhooks, where OPay also shows the merchant ID Harbour asks for). Harbour validates them with the provider before it stores anything.
Harbour then shows a webhook URL to paste back into the provider's dashboard, and for Flutterwave a secret hash shown once. Do that now: it is how Harbour hears that a payment succeeded.
-
2
Create your test keys
Open Settings → Developers and create a key pair in test mode. You get:
hb_sk_test_…— the secret key, shown once. Server only.hb_pk_test_…— the public key, safe in a browser.
On the same screen, add a webhook endpoint pointing at your server and copy its
whsec_…signing secret, also shown once. -
3
Install Harbour Connect
Terminal composer require nordaxiz/harbour-connect:^0.1PHP 8.1 or newer, with
ext-curlandext-json. No other dependencies. The SDK is below 1.0.0, so^0.1pins it to patches while the API settles (why). Put the keys in your environment, never in code:.env HARBOUR_SECRET_KEY=hb_sk_test_your_key HARBOUR_WEBHOOK_SECRET=whsec_your_endpoint_secret -
4
Send the customer to checkout
Create the charge on your server, store the Harbour reference against the order, then redirect.
checkout.phpPHP
<?php use Nordaxiz\HarbourConnect\Harbour; Harbour::configure((string) getenv('HARBOUR_SECRET_KEY')); $checkout = Harbour::charge(12_000_00, 'funke@example.ng', [ 'reference' => 'order-1043', // your own order reference 'description' => 'Aso-ebi order, 12 yards', 'return_url' => 'https://shop.example.ng/thanks', ]); // store $checkout->reference ("HB-88413") against the order before you redirect $order->harbour_reference = $checkout->reference; $order->save(); header('Location: ' . $checkout->checkoutUrl, true, 303);Amounts are integer kobo:
12_000_00is ₦12,000. Floats are refused. -
5
Confirm the payment
The customer comes back to your
return_urlwith?reference=HB-88413&status=succeeded. Ignore thatstatusand ask Harbour:thanks.phpPHP
<?php use Nordaxiz\HarbourConnect\Harbour; $checkout = Harbour::verify($_GET['reference'] ?? ''); // GET /v1/charges/HB-88413 if ($checkout->isPaid() && $checkout->merchantReference === $order->reference && $checkout->amountKobo === $order->total_kobo) { $order->markPaid(); // idempotent: the webhook may get here first }Check the amount and your own reference before you fulfil anything.
-
6
Add the webhook
A customer who closes the tab never reaches your return URL, but the webhook still arrives. This, not the redirect, is what makes fulfilment reliable.
webhook.phpPHP
<?php use Nordaxiz\HarbourConnect\Exception\SignatureVerificationException; use Nordaxiz\HarbourConnect\Harbour; use Nordaxiz\HarbourConnect\Webhook\Event; try { Harbour::webhooks((string) getenv('HARBOUR_WEBHOOK_SECRET'))->handle(null, function (Event $event): void { if ($event->type !== Event::CHARGE_SUCCEEDED) { return; } $transaction = $event->transaction(); $order = Order::findByReference($transaction->merchantReference); if ($order && $order->total_kobo === $transaction->amountKobo) { $order->markPaid($event->id); // de-duplicate on the event id } }); http_response_code(200); } catch (SignatureVerificationException $e) { http_response_code(400); }Answer 2xx within ten seconds and do slow work in a queue. Harbour retries a failed delivery for 24 hours. The full rules are on Webhooks.
What you have now
Pay for a test order with the provider's test card. Within a second or two:
- your webhook fires and the order is marked paid;
- the payment appears in the dashboard feed with its provider, channel and fee;
Harbour::verify()andGET /v1/transactionsboth show it.
Nothing about this changes when you go live except the keys. Read Testing for the checks worth automating, then Going live for the switch.