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

Sagepay Laravel Package

omnipay/sagepay

Sage Pay (Opayo) driver for Omnipay, the framework-agnostic PHP payment library. Integrates Form, Server, Direct and XML gateways behind a consistent API, covering purchases, refunds, voids, recurring payments and 3D Secure in Laravel or any PHP application.

Deep Wiki
Context7

Getting Started

Install via Composer alongside Omnipay itself:

composer require omnipay/sagepay league/omnipay

Pick the gateway that matches your integration model — this is the single most important decision, and the package ships three:

  • Omnipay\SagePay\ServerGateway — redirect-based, Sage Pay hosts the payment page and 3D Secure. Best default for new integrations and the only one that comfortably handles SCA.
  • Omnipay\SagePay\DirectGateway — server-to-server, you collect card data. Requires PCI DSS scope. Use only if you must.
  • Omnipay\SagePay\FormGateway — legacy encrypted-form POST. Avoid for new builds.

First working call (Direct, test mode):

use Omnipay\Omnipay;

$gateway = Omnipay::create('SagePay\Direct');
$gateway->setVendor('myvendor');
$gateway->setTestMode(true);

$response = $gateway->purchase([
    'amount'   => '10.00',
    'currency' => 'GBP',
    'card'     => $card,
    'transactionId' => 'ORDER-123',
])->send();

if ($response->isSuccessful()) { /* ... */ }

Where to look first: the tests/ directory mirrors real request/response shapes and is the fastest way to learn the required fields per operation. Then read the gateway class docblocks — the supported options are enumerated per method. The first real use case to wire up is a purchase() against the Sage Pay simulator in test mode, then a refund() against the same transaction.

Implementation Patterns

Server gateway (recommended) — two-phase redirect flow. The pattern is: purchase() returns a redirect to Sage Pay; Sage Pay posts back to your notifyUrl and redirects the shopper to your returnUrl. Handle both, idempotently:

// Step 1 – kick off
$response = $gateway->purchase([
    'amount' => '25.00', 'currency' => 'GBP',
    'transactionId' => $order->id,
    'returnUrl' => route('sagepay.return'),
    'notifyUrl' => route('sagepay.notify'),
])->send();
$response->redirect(); // or $response->getRedirectUrl()

// Step 2a – browser return
$response = $gateway->completePurchase([
    'transactionId' => $request->transactionId,
])->send();

// Step 2b – server-to-server notification
$response = $gateway->acceptNotification()->send();

Direct gateway with 3D Secure. Because of SCA, a plain purchase() will frequently return a 3DS challenge. Structure it as purchase() → detect $response->isRedirect() → send shopper to ACS → completePurchase() with the PaRes/CRes value. Authorize-then-capture is often cleaner than purchase:

$auth = $gateway->authorize([...])->send();
// ... later
$gateway->capture(['amount' => '25.00', 'transactionReference' => $auth->getTransactionReference()])->send();

Storing references. Persist getTransactionReference() on every response, not just successful ones — refunds, voids, and repeated captures all key off it. Also persist getTransactionId() (your order ID) separately so you can reconcile Sage Pay's VPSTxId/VendorTxCode mapping.

Reconciliation / notifications. Treat the notify callback as canonical: verify the signature, then look up the order, then update state. Guard against duplicate deliveries (Sage Pay can retry) — check whether the transaction ID has already been processed and ack the second time.

Integration tip. Wrap the gateway in a small service that maps your domain responses to Omnipay's PurchaseResponse/Response objects, so controllers never touch raw Sage Pay statuses (OK, NOTAUTHED, 3DAUTH, REGISTERED, etc.). This keeps 3DS and error branching in one place.

Test harness. Use the simulator with vendor myvendor and test cards (4929 0000 0000 6 for success, 4111 1111 1111 1111 with a specific CVV for decline). The package's own tests show the exact form-encoded payloads, which is invaluable when a real response doesn't match expectations.

Gotchas and Tips

  • Sage Pay is now Opayo (Elavon). Endpoints and branding changed. Confirm which host names your gateway version targets; older releases may still point at sagepay.com test/live hosts. Check getEndpoint() and the constants in each gateway class before going live.
  • Amounts are strings in major units. '10.00', not 1000 and not a float. Mixing this up produces silent mischarges. Validate currency precision yourself.
  • Vendor name is not the API key. setVendor() expects the vendor name configured in MyOpayo. The encryption key only applies to the legacy Form gateway — do not confuse it with the API key used by newer integrations.
  • Server gateway returns plain text on notify, and it's form-encoded. Don't attempt json_decode. Use acceptNotification() and check $notification->getTransactionStatus() (or isSuccessful()/isPending() after the package normalises it), and always validate the signature before trusting the body.
  • 3DS is mandatory in the EU/UK. DirectGateway purchase() without 3DS will be rejected in live mode. ServerGateway handles this for you; if you're on Direct you must implement completePurchase with the ACS response yourself.
  • completeAuthorize vs completePurchase. For authorize() + 3DS you must call completeAuthorize(); calling completePurchase() will fail cryptically. Mirror whichever operation you started.
  • Missing basket/line items. Optional on Direct purchase, but some merchant configs (and 3DS2 in particular) expect a basket. If you see vague "invalid" errors with a correct-looking payload, add items to the request.
  • Status codes are strings, not booleans. $response->isSuccessful() covers the common case, but $response->getCode() / $response->getMessage() give the Sage Pay status (2000, 2001, 4021, etc.). Log the raw message; the human-readable text is often more useful than the code alone.
  • Idempotency on notifications. Sage Pay will re-deliver notifications until you return a plain OK (or 200 OK). Return that immediately from the callback and do the order updates in a queued job, otherwise a slow downstream call causes retries and double-processing.
  • Extension points. All three gateways extend AbstractGateway, and each operation is its own Parameter/Response class. Custom response handling (e.g. mapping a merchant-specific status) is done by subclassing the gateway's response classes — inspect Omnipay\SagePay\Message\*Response for the hooks.
  • Version pinning. The package was last released March 2024. Pin it explicitly and watch for Omnipay 3.x compatibility notes; the ecosystem is quiet, so don't assume composer update will be safe in a live payments path.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle