Reference
Errors
Every error Harbour can answer with, and what to do about each one.
Every error Harbour answers with carries a machine-readable code and a sentence a person can read.
The sentences are written to be shown to a business owner, not decoded by one, so validation messages are safe to
put on screen as they are.
The shape of an error
{
"error": {
"code": "validation_failed",
"message": "Amount must be at least ₦100.",
"fields": { "amount_kobo": "Amount must be at least ₦100." }
}
}
In the SDK this becomes a typed exception. Everything at runtime extends
Nordaxiz\HarbourConnect\Exception\HarbourException, so one catch can be your backstop.
The exceptions
| Exception | HTTP · code | What happened, and what to do |
|---|---|---|
AuthenticationException | 401 unauthorized | Missing, wrong or revoked key. Check which environment you are in; do not retry. |
PermissionException | 403 forbidden | The key may not do this. Approving a refund, for example, is never a key's to do. |
PermissionException | 403 email_unverified | The account has not confirmed its email, so live actions are held. The business fixes this in the dashboard. |
NotFoundException | 404 not_found | No such reference in this mode. A live reference with a test key looks exactly like this. |
IdempotencyConflictException | 409 idempotency_conflict | The same key was used with a different body. Use a new key for a genuinely new request. |
ValidationException | 422 validation_failed | A field is wrong. getFieldErrors() says which, in plain language. |
ValidationException | 422 no_provider | The business has no provider connected in this mode. They connect one in Providers. |
RateLimitException | 429 rate_limited | Too many requests. getRetryAfterSeconds() says how long to wait. |
HarbourException | 502 provider_unavailable | The provider (Paystack, Flutterwave or OPay) is down or refusing. Harbour keeps polling; try again shortly. |
HarbourException | 500 server_error | Our fault, and logged. Safe to retry with the same idempotency key. |
ApiConnectionException | — no response | DNS, TLS, timeout. The request may still have happened: retry with the same idempotency key. |
SignatureVerificationException | — webhook | A delivery failed its signature or timestamp check. Answer 400 and look at which secret you used. |
Mistakes Harbour catches before anything is sent — a float amount, an unknown option, a public key used as a secret
key, a plain-HTTP base — throw Nordaxiz\HarbourConnect\Exception\InvalidArgumentException, which extends
PHP's own. Those are bugs in the calling code, not runtime conditions.
Handling them well
PHP
<?php
use Nordaxiz\HarbourConnect\Exception\ApiConnectionException;
use Nordaxiz\HarbourConnect\Exception\HarbourException;
use Nordaxiz\HarbourConnect\Exception\RateLimitException;
use Nordaxiz\HarbourConnect\Exception\ValidationException;
try {
$checkout = Harbour::charge($order->total_kobo, $order->email, ['reference' => $order->reference]);
} catch (ValidationException $e) {
// something about the request is wrong and will stay wrong: show it, do not retry
return back()->withErrors($e->getFieldErrors());
} catch (RateLimitException $e) {
return $this->retryAfter($e->getRetryAfterSeconds() ?? 60);
} catch (ApiConnectionException $e) {
// no answer at all. The charge may or may not exist: retry with the SAME idempotency key
report($e);
return back()->with('error', 'We could not reach payments. Try again in a moment.');
} catch (HarbourException $e) {
report($e->getErrorCode() . ': ' . $e->getMessage());
return back()->with('error', 'Payments are having a moment. Try again shortly.');
}
Every HarbourException has getHttpStatus() and getErrorCode(): log the code, not
the message, so a reworded sentence never breaks a dashboard you built on top of your logs.
Showing validation errors
PHP
<?php
try {
Harbour::charge($amountKobo, $email);
} catch (ValidationException $e) {
$e->getFieldErrors(); // ['amount_kobo' => 'Amount must be at least ₦100.']
$e->getMessage(); // the same sentence, for one-line forms
}