myckhel/laravel-paystack
A Laravel package that wraps the Paystack payment API, letting you accept payments, verify transactions, manage customers and plans, and handle webhooks through a clean, fluent interface. Supports cards, bank transfers, USSD, and subscriptions.
Install and configure
composer require binkode/laravel-paystack
php artisan vendor:publish --tag=paystack-config
Add your keys to .env (test keys first — Paystack test and live keys are not interchangeable):
PAYSTACK_PUBLIC_KEY=pk_test_xxx
PAYSTACK_SECRET_KEY=sk_test_xxx
PAYSTACK_PAYMENT_URL=https://api.paystack.co
[email protected]
Where to look first
config/paystack.php — the only file you should need to touch for a standard integration.src/ — skim the public methods once; most of what you need day-to-day is initialize, verify, and the transaction helpers.Collections — check which, because it changes how you read data.authorization_url).First use case: a redirect checkout
The canonical flow is initialize → redirect → verify on callback. Put the reference in the database before redirecting, so the callback has something to reconcile against:
$response = Paystack::initialize([
'email' => $user->email,
'amount' => $order->total_kobo, // subunits, see Gotchas
'reference' => $order->reference,
'callback_url' => route('paystack.callback'),
]);
return redirect($response['data']['authorization_url']);
Then in the callback route, verify server-side and only then mark the order paid.
Requirements note (v1.6.0): CI and dependency constraints now cover PHP 8.4 and Laravel 13, so the package is safe on modern stacks. If you are pinned to an older PHP (8.0 or below) or Laravel 9, stay on v1.5.x.
Wrap the facade, don't spray it everywhere. Bind a thin PaystackGateway service (or an interface) in your container that delegates to the facade. It gives you one seam for tests, one place to add logging/retries, and one place to switch keys per tenant.
Persist the reference, treat the callback as a hint. The browser callback only tells you the user came back. The source of truth is verify($reference) or the webhook:
$result = Paystack::verify($reference);
if (($result['data']['status'] ?? null) === 'success'
&& $result['data']['amount'] === $order->total_kobo) {
$order->markPaid($result['data']);
}
Note the amount check — verifying status alone is not enough.
Webhook handler. Register a route outside auth/csrf, read the raw body, and compare x-paystack-signature against hash_hmac('sha512', $rawBody, config('paystack.secretKey')). Then dispatch a job so the HTTP response returns 200 fast:
Route::post('/paystack/webhook', PaystackWebhookController::class)
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
Store the event id/reference in a unique-indexed table for idempotency — Paystack retries.
Queues and jobs. Initialization is a network call; inside a request it's fine, but anything that fans out (bulk refunds, subscriptions sweeps) belongs in a queued job with $tries and backoff set. Treat non-2xx responses as retryable and 4xx validation errors as terminal.
Per-tenant keys. If you serve multiple merchants, resolve keys at call time from the tenant model rather than the global config. The cleanest approach is a scoped binding: register your gateway with the tenant's secret key in a request-scoped container binding, or pass an explicit key argument where the package methods allow it.
New in v1.6.0 — Terminal, Virtual Terminal, and Order APIs. These surfaces are now available in the package, which means three new workflows:
For all three: check the method signatures in src/ against Paystack's docs before wiring them in. Feature PRs like these typically map one-to-one onto the API but can lag on optional parameters, and the package will not always validate payload shapes for you.
Testing. Fake at the HTTP layer (Http::fake([...])) rather than mocking the facade, so you also exercise the package's request construction — particularly the amount and reference fields, which are the two things you most want regression coverage on. For webhooks, build a signed payload with hash_hmac in the test rather than disabling signature checks.
Amounts are subunits. 1000 is ₦10.00, not ₦1000. Store money in the smallest unit in your DB (or a dedicated accessor) and never mix the two representations. This is the single most common production bug with any Paystack wrapper.
Currency matters. The default currency typically comes from config or the merchant account. If you sell in multiple currencies, pass currency explicitly on initialize and confirm the account supports it — a mismatched currency fails at the API, not in your code.
Verify signature against the raw body. $request->all() re-serialises and will not match the HMAC. Use $request->getContent(). If you have middleware that parses or rewrites JSON, exclude the webhook route.
References must be unique. Reusing a reference returns an error, and re-verifying a reference that is already consumed is fine (verify is idempotent) but re-initializing is not. Generate references server-side with a prefix, e.g. order_2024_000123.
Config caching. php artisan config:cache will freeze your keys. If you do per-tenant key switching, do it at runtime in your gateway, not via config() mutation + cache.
Never trust the client-side callback payload. Anyone can hit your callback URL with a forged reference. Always verify() and always check amount and currency and status.
Log responses, not keys. Add a debug log of the response body and the request reference in your gateway wrapper, but scrub authorization, keys, and card metadata. Paystack responses can include authorization objects you do not want in your logs.
Extension point: your own gateway class. Rather than patching the vendor package, wrap it. When a new API surface lands (as Terminal/Order did in v1.6.0) you can expose your own typed methods and normalise the array responses into DTOs, so a future package change only touches one file.
Watch the release cadence for API coverage. v1.6.0 is a feature-plus-support release with no documented breaking changes, so it is a safe minor upgrade from v1.5.x. Because the package tracks Paystack's API surface directly, pin a minor range (^1.6) and read release notes before jumping minors — new endpoints rarely break, but response normalisation sometimes shifts.
How can I help you explore Laravel packages today?