arca / payment-gateways
Payment gateway integration for Laravel — clean, testable, extensible
Requires
- php: ^8.4
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- spatie/laravel-package-tools: ^1.93
Requires (Dev)
- dnetix/redirection: ^2.1
- larastan/larastan: ^3.13
- laravel/pint: ^1.24
- orchestra/testbench: ^10.12 || ^11.3
- pestphp/pest: ^4.4 || ^5.0
- pestphp/pest-plugin-arch: ^4.0 || ^5.0
- pestphp/pest-plugin-laravel: ^4.1 || ^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- transbank/transbank-sdk: 5.1
Suggests
- dnetix/redirection: Required by the Getnet driver (^2.1)
- transbank/transbank-sdk: Required by the Webpay driver (^5.0)
Provides
None
Conflicts
- transbank/transbank-sdk: <5.0
Replaces
None
This package is auto-updated.
Last update: 2026-10-15 22:02:43 UTC
README
Latin American payment gateways for Laravel behind one API: create a payment, send the buyer to
its checkout URL, and listen to PaymentApproved. The package handles the return from the
gateway, the notifications and every status change.
| Gateway | Countries | Checkout | Refunds | Notifications |
|---|---|---|---|---|
| Webpay Plus | Chile (CLP) | Redirect | Full and partial | — |
| Getnet | Chile | Redirect | Full | Signed (sha1 / sha256) |
| Flow | Chile | Redirect | — | Confirmation URL |
| Mercado Pago | Latin America | Redirect | Full and partial | Signed (x-signature) |
| PayPal | Worldwide | Redirect | Full and partial | Verified with PayPal API |
| Niubiz | Peru (PEN, USD) | Embedded form | — | — |
How it protects payments
- The gateway API is the source of truth. Return URLs and notifications only tell the package to ask the gateway; query strings are never trusted.
- Every result is checked against the payment: the reference (order number) must match, and an approval for a lower amount or another currency is refused.
- Statuses only move forward. Out-of-order notifications never turn a paid payment into a rejected one, and a payment never goes back to pending.
- Notifications are verified (signatures, PayPal's verification API) and a missing secret is a refusal in production.
- One place changes a payment:
PaymentServicelocks the row, applies the result and dispatches the events after the transaction commits.
Requirements
- PHP 8.4+
- Laravel 12 or 13
Installation
composer require arca/payment-gateways
Install the SDK of the gateways that need one:
composer require transbank/transbank-sdk # Webpay (^5.0) composer require dnetix/redirection # Getnet (^2.1)
Publish the config and the migration, then migrate:
php artisan vendor:publish --tag="payment-gateways-config" php artisan vendor:publish --tag="payment-gateways-migrations" php artisan migrate
Configuration
Each gateway has an environment (sandbox or production). Leave it empty to follow the
application: production when APP_ENV=production, sandbox otherwise.
PAYMENT_GATEWAY=webpay # Webpay: empty in sandbox, Transbank's integration credentials are used WEBPAY_COMMERCE_CODE= WEBPAY_API_KEY= # Getnet GETNET_LOGIN= GETNET_TRAN_KEY= # Flow FLOW_API_KEY= FLOW_SECRET_KEY= # Mercado Pago MERCADOPAGO_ACCESS_TOKEN= MERCADOPAGO_WEBHOOK_SECRET= # required in production MERCADOPAGO_WEBHOOK_TOLERANCE= # optional, seconds (replay protection) MERCADOPAGO_STATEMENT_DESCRIPTOR= # PayPal PAYPAL_CLIENT_ID= PAYPAL_CLIENT_SECRET= PAYPAL_WEBHOOK_ID= # required in production # Niubiz NIUBIZ_MERCHANT_ID= NIUBIZ_USER= NIUBIZ_PASSWORD=
Several accounts of one gateway
The gateway name is a config key and driver picks the implementation, so one driver can serve
several accounts, for example one Webpay commerce for courses and another for workshops:
'gateways' => [ 'webpay_courses' => [ 'driver' => 'webpay', 'commerce_code' => env('WEBPAY_COURSES_COMMERCE_CODE'), 'api_key' => env('WEBPAY_COURSES_API_KEY'), ], 'webpay_workshops' => [ 'driver' => 'webpay', 'commerce_code' => env('WEBPAY_WORKSHOPS_COMMERCE_CODE'), 'api_key' => env('WEBPAY_WORKSHOPS_API_KEY'), ], ],
$payment = PaymentGateways::create('webpay_courses', Money::CLP(50000), 'Laravel course', payable: $enrollment);
Each payment remembers its account ($payment->gateway === 'webpay_courses'), so the return,
sync and refunds use that account's credentials, and notifications arrive at
/payments/webhooks/webpay_courses. Tell them apart in listeners by $payment->gateway or by
the payable model.
Usage
Taking a payment
use Arca\PaymentGateways\Facades\PaymentGateways; use Arca\PaymentGateways\ValueObjects\Customer; use Arca\PaymentGateways\ValueObjects\Money; $payment = PaymentGateways::create( gateway: 'webpay', money: Money::CLP(15000), description: "Order #{$order->id}", payable: $order, // optional, morph relation customer: new Customer(name: $user->name, email: $user->email), ); return redirect($payment->checkoutUrl());
checkoutUrl() starts the checkout: it redirects the buyer to the gateway (or renders the Niubiz
form). When the buyer comes back, the package confirms the payment with the gateway and shows
the success, pending or failed page.
Then fulfil the order when it is paid, whether the buyer came back or only a notification arrived:
use Arca\PaymentGateways\Events\PaymentApproved; Event::listen(PaymentApproved::class, function (PaymentApproved $event) { $event->payment->payable->markAsPaid(); });
Routes
| Route | URL | Purpose |
|---|---|---|
payments.show |
/payments/{payment} |
Starts the checkout (checkoutUrl()) |
payments.commit |
/payments/{payment}/commit |
The buyer is back (GET or POST, no CSRF) |
payments.success / pending / failed |
/payments/{payment}/… |
Result pages |
payments.webhook |
/payments/webhooks/{gateway} |
Gateway notifications |
Send the buyer to your own pages instead of the package views:
// config/payment-gateways.php 'redirects' => [ 'success' => 'orders.thanks', // route names, they receive the payment 'pending' => 'orders.pending', 'failed' => 'orders.failed', ],
Or publish and edit the views (--tag="payment-gateways-views") and translations
(--tag="payment-gateways-translations", Spanish and English included).
Notifications
The webhook URL is sent to the gateway automatically when it is public HTTPS (not on localhost):
- Mercado Pago: set the webhook secret from Your integrations > Webhooks in
MERCADOPAGO_WEBHOOK_SECRET. - PayPal: register
https://your-app/payments/webhooks/paypalin the developer dashboard (eventsCHECKOUT.ORDER.APPROVEDandPAYMENT.CAPTURE.*) and setPAYPAL_WEBHOOK_ID. - Getnet: configure
https://your-app/payments/webhooks/getnetas notification URL of the site. - Flow: nothing to do, the confirmation URL goes with each payment.
Schedule the sync command to catch notifications that never arrived:
Schedule::command('payment-gateways:sync')->everyFifteenMinutes();
It asks the gateways for the status of pending payments started at least 10 minutes ago
(--older-than, --max-age, --gateway, or a single payment uuid).
Status, money and refunds
$payment->status; // PaymentStatus::Pending, Authorized, Paid, Failed, Refunded, Reversed $payment->isPaid(); $payment->money()->format(); // "15.000 CLP" $payment->failureMessage(); // why the gateway rejected it $payment->receipt('installments'); // receipt details: card, installments, payment type... $payment->metadata; // your own data, never written by the package PaymentGateways::sync($payment); // ask the gateway now PaymentGateways::refund($payment); // full refund PaymentGateways::refund($payment, Money::CLP(5000)); // partial refund
Money holds amounts in the currency's minor unit: Money::CLP(15000) is 15.000 pesos,
Money::USD(1999) is 19.99 dollars, and Money::fromDecimal('19.99', 'USD') builds the same.
Audit log
Every exchange with a gateway is stored raw in payment_logs: API calls (request and answer as
sent and received, secrets masked), the buyer coming back, and every notification, including
refused ones and those about unknown payments.
$payment->logs; // the full trail, oldest first
Keep them as long as you need: set PAYMENT_GATEWAYS_LOGS_RETENTION_DAYS and schedule
Schedule::command('model:prune')->daily() to delete older ones.
DATA.md maps every column and log to the fields of each gateway: where to find the buy order, authorization code, VCI, card digits or a notification when a certification asks.
Events
| Event | When |
|---|---|
PaymentInitiated |
The checkout started |
PaymentStatusChanged |
Any status change (with from and to) |
PaymentAuthorized |
Authorized, not captured yet |
PaymentApproved |
Paid |
PaymentRejected |
Rejected or cancelled |
PaymentRefunded |
Fully refunded |
PaymentReversed |
Chargeback or reversal |
WebhookReceived |
A notification passed verification |
Status events are dispatched after the database transaction commits.
Custom gateways
php artisan make:payment-driver Khipu
The driver lands in app/PaymentDrivers/Khipu. Register it and add its config:
PaymentGateways::extend('khipu', fn ($app, array $config) => new KhipuDriver($config));
A driver implements charge(), complete() and query(); add SupportsRefunds and
SupportsWebhooks when the gateway offers them. Return the reference and amount the gateway
reports in each ChargeResult: the package checks them before changing the payment.
Testing your application
use Arca\PaymentGateways\Facades\PaymentGateways; $fake = PaymentGateways::fake('webpay'); $this->get($payment->checkoutUrl()); // redirects to a fake checkout $this->get(route('payments.commit', $payment)); // the fake approves it $fake->assertCharged()->assertCompleted(); // Other answers $fake->completeWith(new ChargeResult(status: PaymentStatus::Failed, message: 'Card declined')); $fake->respondRefundWith(new RefundResult(status: PaymentStatus::Paid)); $fake->rejectWebhooks();
Run the package tests with composer test.
Upgrading from v1.x
v2 is a clean break: v1.x is discontinued and v2 ships no compatibility layer. See UPGRADE.md.
Changelog
See CHANGELOG.md.
Security
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.