ignited/laravel-omnipay
Laravel integration for the Omnipay payment library, giving you a clean, unified API to accept payments through PayPal, Stripe, Authorize.Net and dozens of other gateways. Configure drivers in Laravel, then purchase, authorize, capture, refund, and handle callbacks from one consistent interface.
Install the wrapper — and the gateway you actually need. ignited/laravel-omnipay only ships the Laravel glue plus omnipay/common. Each payment provider is a separate package:
composer require ignited/laravel-omnipay
composer require omnipay/stripe # or omnipay/paypal, omnipay/authorizenet, ...
On Laravel 5.5+ the LaravelOmnipayServiceProvider and the Omnipay facade alias are auto-discovered. On older versions add them to config/app.php manually (Ignited\LaravelOmnipay\LaravelOmnipayServiceProvider and 'Omnipay' => Ignited\LaravelOmnipay\Facades\OmnipayFacade::class).
Publish and fill the config. This is the one file that matters:
php artisan vendor:publish --provider="Ignited\LaravelOmnipay\LaravelOmnipayServiceProvider" --tag=config
// config/omnipay.php
return [
'default' => 'stripe',
'gateways' => [
'stripe' => [
'driver' => 'Stripe',
'options' => [
'apiKey' => env('STRIPE_SECRET'),
'testMode' => env('STRIPE_TEST_MODE', true),
],
],
'paypal' => [
'driver' => 'PayPal_Express',
'options' => [
'username' => env('PAYPAL_USERNAME'),
'password' => env('PAYPAL_PASSWORD'),
'signature' => env('PAYPAL_SIGNATURE'),
'testMode' => env('PAYPAL_TEST_MODE', true),
],
],
],
];
driver is an Omnipay gateway name (underscored: PayPal_Express, AuthorizeNet_AIM, SecurePay_DirectPost), and options are fed straight to the gateway's initialize(), so each key maps to a setter (apiKey → setApiKey(), testMode → setTestMode()).
Where to look first. The wrapper is thin — it is ~200 lines. Nearly all your day-to-day knowledge has to come from the individual Omnipay gateway's README, not from this package. Read the config file, then read omnipay/<your-gateway>'s docs for the request/response parameters.
First use case — a one-shot charge:
use Ignited\LaravelOmnipay\LaravelOmnipay;
class CheckoutController extends Controller
{
public function charge(Request $request, LaravelOmnipay $omnipay)
{
$response = $omnipay->gateway('stripe')->purchase([
'amount' => '49.00', // string, major units
'currency' => 'USD',
'token' => $request->input('stripeToken'),
'description' => 'Order #'.$request->input('order_id'),
])->send();
if ($response->isSuccessful()) {
// $response->getTransactionReference()
}
}
}
Type-hint LaravelOmnipay rather than reaching for the facade — it's the same singleton, but it makes faking trivial in tests.
Gateway → request → response. Every flow in this package is the same three beats, so build one internal convention around it:
$gateway = Omnipay::gateway('stripe'); // returns an Omnipay\Common\AbstractGateway
$response = $gateway->authorize($params)->send(); // or purchase/capture/refund/void/createCard
$response is an Omnipay\Common\Message\ResponseInterface; branch on isSuccessful(), isRedirect(), isPending(), isCancelled(), and read getTransactionReference(), getMessage(), getCode(), getData().
Wrap it in your own service, not scattered facade calls. The highest-leverage move with this package is treating it as an infrastructure detail:
interface PaymentGateway {
public function charge(Order $order, PaymentMethod $method): PaymentResult;
}
class OmnipayPaymentGateway implements PaymentGateway {
public function __construct(private LaravelOmnipay $omnipay, private string $name) {}
public function charge(Order $order, PaymentMethod $method): PaymentResult
{
$response = $this->omnipay->gateway($this->name)->purchase([
'amount' => $order->total->format(2, '.', ''), // never floats
'currency' => $order->currency,
'token' => $method->token,
'transactionId' => $order->reference, // your idempotency anchor
])->send();
return PaymentResult::fromOmnipay($response);
}
}
Bind it in a service provider, and swap in a fake (omnipay/dummy or a stub implementing your interface) for tests and local dev.
Redirect flow (PayPal Express, Sage Pay, most EU gateways). Two requests, one persisted row:
// 1. start
$response = Omnipay::gateway('paypal')->purchase([
'amount' => '49.00', 'currency' => 'USD',
'returnUrl' => route('checkout.return'),
'cancelUrl' => route('checkout.cancel'),
])->send();
Payment::create(['reference' => $ref = Str::uuid(), 'amount' => 4900, 'status' => 'pending']);
$response->redirect(); // handles GET vs auto-submitting POST form for you
// 2. return
$payment = Payment::where('reference', $request->input('reference'))->firstOrFail();
$response = Omnipay::gateway('paypal')->completePurchase([
'amount' => number_format($payment->amount / 100, 2, '.', ''),
'currency' => $payment->currency,
'transactionId' => $payment->reference, // must match what you sent
])->send();
$response->redirect() is the right call, not redirect()->away($response->getRedirectUrl()) — several gateways require a POST with hidden fields (getRedirectMethod() / getRedirectData()).
Always re-read amount and currency from your own database on return. The return URL is user-controllable; a tampered amount passed into completePurchase is a classic underpayment bug.
Tokenisation / saved cards. createCard() (or purchase() with 'createCard' => true) returns a token via getCardReference() / getToken(). Store that; never store PAN/CVV. purchase(['cardReference' => $token]) for subsequent charges. Build the card with Omnipay\Common\CreditCard when you have raw fields and call $card->validate() before sending — it gives you far better errors than a gateway round-trip.
Multiple gateways / routing by currency or country. That's just config entries plus a runtime choice:
$name = $order->currency === 'EUR' ? 'stripe_eu' : 'stripe_us';
Omnipay::gateway($name)->purchase($params)->send();
Keep one config entry per merchant account rather than mutating options at runtime.
Post-payment operations belong in queued jobs. capture, refund, void, and fetchTransaction are idempotent-ish network calls; queue them with retries and store getTransactionReference() on the order so you can reconcile.
Testing. Two layers:
LaravelOmnipay ($this->mock(LaravelOmnipay::class) or Omnipay::shouldReceive('gateway')->andReturn($fakeGateway)) and return hand-built Omnipay\Common\Message\AbstractResponse subclasses.omnipay/dummy gateway in testMode so you exercise real serialization/parsing without network calls.Omnipay::shouldReceive('gateway')->with('stripe')->andReturn($gateway = Mockery::mock(AbstractGateway::class));
$gateway->shouldReceive('purchase->send')->andReturn(new PurchaseResponse($gateway, ['success' => true]));
Amounts are strings in major units. '49.00', not 4900, not 49.0. Omnipay does no currency-aware conversion — amount for JPY is whole yen, for USD it's dollars-and-cents. Keep integer minor units in your DB and convert with number_format($cents / 100, 2, '.', '') at the boundary. Mixing moneyphp/money in is fine, just don't hand it floats.
testMode goes inside options, as a boolean. 'testMode' => 'false' (string) is truthy and will silently keep you in sandbox. Cast from env explicitly.
Config is resolved at boot. env() calls only work inside config/*.php. If you rotate API keys at runtime, run php artisan config:cache on deploy, and remember Omnipay::gateway() caches the resolved instance per name for the process.
That caching bites in Octane / long-lived workers. Gateway instances are memoised on the singleton, so any setters you call (e.g. setCard(), setReturnUrl()) leak into later requests. Either avoid mutating gateways, or re-resolve between requests (app()->forgetInstance(LaravelOmnipay::class)) / hook RequestTerminated.
Unknown gateway = exception, not null. Omnipay::gateway('typo') throws InvalidArgumentException. Config key is default (not gateway) in current releases — if Omnipay::gateway() with no argument fails, check the published file's key name against the release you installed.
Version alignment is the #1 support issue. This package has 2.x (Omnipay 2) and 3.x (Omnipay 3) lines, and each omnipay/* gateway package has its own 2.x/3.x split. A mismatched pair produces confusing Call to undefined method or interface errors. Pin both in composer.json.
Missing parameters throw. Omnipay validates in getData() and throws Omnipay\Common\Exception\InvalidRequestException (and InvalidCreditCardException for bad card data). Catch these separately from HTTP/transport failures and map them to validation errors — they're almost always a bug in your params array, not the customer's fault.
Debugging a failed charge. In order: $response->getMessage() (human text), $response->getCode() (gateway code), $response->getData() (raw decoded payload). Log the last one — but never log $params, which contains card data. Scrub with array_except/Arr::except before any Log::info.
Add logging at the HTTP layer, not around every call. Omnipay 3 lets you inject a PSR-18 client: $gateway->setHttpClient(new Psr18Client($stack)) — wrap Guzzle with a logging middleware and you get full request/response traces in one place for every gateway.
Use transactionId for idempotency. Pass your own order reference on every purchase/capture/refund. Gateways that support idempotency keys will deduplicate; those that don't still give you a correlation key in their dashboard when a customer double-clicks "Pay".
Webhooks are per-gateway, not per-package. This wrapper gives you nothing here. Verify signatures with the gateway's own SDK/algorithm, make the handler idempotent (unique index on transaction reference), then call fetchTransaction() to get the authoritative state rather than trusting the payload.
Extension point for ad-hoc gateways. If a gateway isn't worth a config entry (one-off, dynamic credentials), use the underlying factory instead of fighting the config: Omnipay::getFactory()->create('Stripe', $httpClient)->initialize($options).
Reconciliation job is worth writing early. A nightly fetchTransaction() sweep over recent pending payments catches the "customer closed the tab after paying" case that redirect flows lose.
How can I help you explore Laravel packages today?