Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Paystack Laravel Package

binkode/laravel-paystack

A Laravel wrapper for the Paystack payment gateway with a clean, expressive API. Process transactions, manage customers, plans, and subscriptions, and handle webhooks through simple methods. Supports one-off and recurring payments, with publishable config for quick, convention-friendly setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up before touching any endpoint class:

composer require myckhel/laravel-paystack
php artisan vendor:publish --tag=paystack-config

Add PAYSTACK_PUBLIC_KEY, PAYSTACK_SECRET_KEY, and (if you use it) PAYSTACK_PAYMENT_URL to .env. The published config/paystack.php is the single source of truth for keys, base URL, and default currency — read it first, then read src/Paystack.php (the facade root) and the resource classes it dispatches to. The facade is the entry point for everything; the resource classes are one-per-Paystack-endpoint-group and are the best documentation of what is actually supported in the version you installed.

First use case — a redirect checkout, which is the 80% path:

use Myckhel\Paystack\Facades\Paystack;

$res = Paystack::transaction()->initialize([
    'email'        => $user->email,
    'amount'       => 50000, // kobo
    'reference'    => $reference = 'ord_'.Str::uuid(),
    'callback_url' => route('paystack.callback'),
    'metadata'     => ['order_id' => $order->id, 'user_id' => $user->id],
]);

return redirect($res['data']['authorization_url']);

Then on the callback route, Paystack::transaction()->verify($reference) server-side before you mark anything paid. Do not trust the query string.

Note on v1.6.0: this release adds Laravel 13 / PHP 8.4 support (CI + dependency constraints) and exposes the Terminal, Virtual Terminal, and Order APIs. If you are on Laravel 11/12 today, the upgrade is non-breaking for existing calls; the new surface is purely additive.

Implementation Patterns

Dynamic resource dispatch is the core idiom. Paystack::transaction(), Paystack::charge(), Paystack::transfer(), Paystack::customer(), and now Paystack::terminal(), Paystack::virtualTerminal(), Paystack::order() all return a resource object you call methods on. The returned value is a plain array shaped like Paystack's JSON. There is no DTO layer, so wrap it immediately:

final class PaystackGateway
{
    public function verify(string $reference): PaymentResult
    {
        $res = Paystack::transaction()->verify($reference);
        // normalize, assert $res['status'] === true, map data.status
    }
}

One service class per domain concept keeps the array-shape fragility in one place and gives you something to fake in tests.

Escape hatch for anything not wrapped. The package exposes a generic route-based call for arbitrary endpoints. When the new Terminal/Order wrappers don't cover a specific Paystack endpoint (or when Paystack ships a new one before the package does), use that rather than dropping to raw Guzzle — you keep the package's base URL, auth headers, and error handling:

Paystack::route('order')->post($payload);
Paystack::route('virtual-terminal/'.$code)->get();

This is the most important pattern for day-to-day work: the wrapper will always lag the API, the route escape hatch won't.

Webhooks, not callbacks, are the source of truth. Register a single route, verify the x-paystack-signature header as HMAC-SHA512 of the raw body using your secret key, return 200 immediately, and dispatch the actual fulfilment to a queued job keyed on the event reference. Handle charge.success, transfer.success, transfer.failed, and — new with v1.6.0 — the terminal/order charge events. Terminal and Virtual Terminal charges are assisted and asynchronous, so webhook reconciliation is not optional there; a synchronous "did it work?" check will be wrong or racy.

Idempotency by reference. Store a unique index on reference (transactions) and on the webhook event identity. Paystack retries; your job will run twice. First-write-wins in the DB is cheaper than a distributed lock.

Amounts are integers in subunits. 50000 is ₦500.00, 50000 is also $500.00 if the currency is GHS/USD — the integer means nothing without currency. Persist both together and always cross-check the verified amount and currency against the order total before fulfilment.

Per-tenant / per-store keys. Multi-tenant apps need different keys per request. The pragmatic pattern is to resolve the tenant's keys and push them into config before the call (config()->set('paystack.secretKey', $tenant->paystack_secret)), scoped tightly inside a service method, or to construct a fresh client instance per tenant. Avoid setting this globally in a middleware — it leaks across queued jobs and long-running Octane workers.

Testing. Point at sandbox keys, and use Laravel's HTTP client fakes against api.paystack.co/* to assert the payload you build (amount, reference, metadata) rather than the response shape. Have one sandbox smoke test per flow you actually ship; the rest should be faked.

Gotchas and Tips

  • status is a boolean, not an HTTP code. Paystack frequently returns HTTP 200 with "status": false and a message. Always gate on $res['status'] === true and $res['data']['status'] === 'success' for transactions. A try/catch around the call will not catch a declined charge.
  • Webhook signature must be computed on the raw body. Use $request->getContent(), not $request->all() — any re-serialization breaks the HMAC. Exclude the route from CSRF, and be aware that global middleware which mutates input can still make this fail confusingly. Log the signature mismatch, not the body.
  • Verification is not reconciliation. Verify-on-callback is a UX optimisation (fast redirect), the webhook is the record. If you only do the former, you will lose payments from users who close the tab.
  • Floating point will bite you. (int) ($total * 100) occasionally truncates. Use round() or, better, store prices in subunits and never multiply.
  • No typed responses. Everything is array. Static analysis won't catch a renamed key; a Paystack-side field change becomes a silent null. Normalize at the boundary and assert with explicit checks.
  • v1.6.0's new Terminal / Virtual Terminal / Order surface is young. The wrappers are thin and likely under-tested relative to the transaction APIs. Validate response shapes in sandbox before building on them, and keep the Paystack::route() escape hatch in mind — if a method is missing or misbehaves, calling the underlying endpoint directly through the package costs you nothing.
  • Laravel 13 / PHP 8.4 support means the constraint was widened, not that every path was exercised. If you're upgrading, run composer update --with-all-dependencies and smoke-test your own flows — CI green on a matrix doesn't cover your webhook and queue interactions.
  • Timeouts and retries. Default HTTP timeouts are usually too long for a checkout redirect; wrap gateway calls with an explicit timeout and a bounded retry, but never retry a charge initialize blindly — you'll create duplicate references.
  • Logging hygiene. Log the reference, amount, currency, and response code. Never log the secret key, and be careful with metadata — it often contains customer PII.
  • Extension points worth knowing: the facade's generic route call (new endpoints), the published config (base URL, keys — useful for pointing tests at a local stub), and the service container binding for the underlying client if you need custom middleware. Beyond that the package is deliberately thin, so your own PaystackGateway service is where business logic belongs — don't let controllers call the facade directly, or you'll have no seam for faking or for swapping providers later.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package