Laravel package for ifthenpay payment gateway that facilitates payment generation with Portuguese payment methods and others.
(working on backwards compatibility for Laravel 11 and 12)
- PHP ^8.3
- Laravel ^13.0
composer require ifthenpay/laravelPublish the config file:
php artisan vendor:publish --tag=ifthenpay-configRun the migrations (creates the ifthenpay_payments table used for optional persistence, loaded automatically by the package):
php artisan migrateConfigure the package by adding these keys to your .env file.
The following is an example of how you would fully configure the package.
IFTHENPAY_BACKOFFICE_KEY=0000-0000-0000-0000
IFTHENPAY_MULTIBANCO_KEY=ITP-000000
IFTHENPAY_MBWAY_KEY=ITP-000000
IFTHENPAY_PAYSHOP_KEY=ITP-000000
IFTHENPAY_CREDITCARD_KEY=ITP-000000
IFTHENPAY_PIX_KEY=ITP-000000
IFTHENPAY_PAYBYLINK_KEY=ITPG-000000
IFTHENPAY_PAYBYLINK_METHODS=MULTIBANCO|ITP-000000;MBWAY|ITP-000000;PAYSHOP|ITP-000000;CREDITCARD|ITP-000000;PIX|ITP-000000;GOOGLE|ITP-000000;APPLE|ITP-000000
IFTHENPAY_PAYBYLINK_DEFAULT_METHOD=GOOGLE
IFTHENPAY_ANTIPHISHING_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
IFTHENPAY_WEBHOOK_PATH=ifthenpay/webhook
IFTHENPAY_WEBHOOK_RATE_LIMIT_PER_MINUTE=60Configure only what you need, use the table below for reference.
| configuration | purpose | example | required for |
|---|---|---|---|
IFTHENPAY_BACKOFFICE_KEY |
Authenticates the webhook registration call | 0000-0000-0000-0000 |
Registering webhooks |
IFTHENPAY_MULTIBANCO_KEY |
Multibanco key — dynamic (online) or entity-subentity (offline) | (online) ITP-000000 or (offline) 00000-000 |
Generating Multibanco payments |
IFTHENPAY_MBWAY_KEY |
MB WAY key | ITP-000000 |
Generating MB WAY payments |
IFTHENPAY_PAYSHOP_KEY |
Payshop key | ITP-000000 |
Generating Payshop payments |
IFTHENPAY_CREDITCARD_KEY |
Credit Card key | ITP-000000 |
Generating Credit Card payments |
IFTHENPAY_PIX_KEY |
Pix key | ITP-000000 |
Generating Pix payments |
IFTHENPAY_PAYBYLINK_KEY |
Pay-by-link key | ITPG-000000 |
Generating Pay by Link payments |
IFTHENPAY_PAYBYLINK_METHODS |
Method|key pairs, separated by ;, used to define what methods to display in the gateway page |
GOOGLE|ITP-000000;APPLE|ITP-000000 |
Generating Pay by Link payments |
IFTHENPAY_PAYBYLINK_DEFAULT_METHOD |
Sets preselected method on the gateway page | GOOGLE |
Generating Pay by Link payments |
IFTHENPAY_ANTIPHISHING_KEY |
Validated against incoming webhook requests | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
Registering webhooks; updating payment status through webhook |
IFTHENPAY_WEBHOOK_PATH |
Relative path used to register the webhook URL (defaults to ifthenpay/webhook) |
ifthenpay/webhook |
Receiving webhooks |
IFTHENPAY_WEBHOOK_RATE_LIMIT_PER_MINUTE |
Requests per minute allowed on the webhook route middleware (defaults to 60) |
60 |
Receiving webhooks |
There are two ways to generate payments:
Using the facade This is the recommended approach, but keep in mind it requires you to configure the payment method keys you'll be using.
use Ifthenpay\Laravel\Facades\Ifthenpay;
$paymentResponse = Ifthenpay::multibanco()->generatePayment(
orderId: 'order-1887',
amount: '10.99',
description: 'Order #1887', // optional
expiryDays: 3, // optional
);Direct instantiation In this approach you are responsible for configuring and instantiating the gateway object
use Ifthenpay\Laravel\Gateways\MultibancoGateway;
$multibancoGateway = new MultibancoGateway(['multibanco_key' => 'ITP-000000']);
$multibancoGateway->generatePayment(
orderId: 'order-1887',
amount: '10.99',
description: 'Order #1887', // optional
expiryDays: 3, // optional
);Given the payment method type, the generatePayment() method will expect different parameters.
// ifthenpay/laravel-ifthenpay/src/Gateways/MultibancoGateway.php
public function generatePayment(string $orderId, string $amount, ?string $description = null, ?int $expiryDays = null): MultibancoResponse
// ifthenpay/laravel-ifthenpay/src/Gateways/MbwayGateway.php
public function generatePayment(string $orderId, string $amount, string $mobileNumber, ?string $description = null, ?string $email = null, ?int $expiryMinutes = null): MbwayResponse
// ifthenpay/laravel-ifthenpay/src/Gateways/PayshopGateway.php
public function generatePayment(string $orderId, string $amount, ?int $expiryDays = null): PayshopResponse
// ifthenpay/laravel-ifthenpay/src/Gateways/CreditcardGateway.php
public function generatePayment(string $orderId, string $amount, string $successUrl, string $errorUrl, string $cancelUrl, string $language = 'en', ?int $expiryMinutes = null): CreditcardResponse
// ifthenpay/laravel-ifthenpay/src/Gateways/PixGateway.php
public function generatePayment(string $orderId, string $amount, string $customerCpf, string $customerName, string $customerEmail, string $customerPhone, string $redirectUrl, ?string $description = null, ?string $customerAddress = null, ?string $customerStreetNumber = null, ?string $customerCity = null, ?string $customerZipCode = null, ?string $customerState = null, ?int $expiryMinutes = null): PixResponse
// ifthenpay/laravel-ifthenpay/src/Gateways/PaybylinkGateway.php
public function generatePayment(string $orderId, string $amount, string $successUrl, string $errorUrl, string $cancelUrl, ?string $description = null, ?int $expiryDays = null, bool $otp = false, string $lang = 'pt', ?string $btnCloseUrl = null, ?string $btnCloseLabel = null): PaybylinkResponseBut their return will always be an implementation of IfthenpayResponse interface.
// ifthenpay/laravel-ifthenpay/src/Contracts/IfthenpayResponse.php
public function isSuccessful(): bool; // result of payment generation
public function getMessage(): ?string; // error message, in case of failure
public function toArray(): array; // response payload with keys prepared for mass-assignment to ifthenpay_payments modeluse Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$multibancoResponse = Ifthenpay::multibanco()->generatePayment(
orderId: '1000',
amount: '10.99',
description: 'Order #1887', // optional
expiryDays: 3, // optional
);
if(!$multibancoResponse->isSuccessful()) {
throw new Exception("Error Generating Payment", 1);
}
// store payment or another action
Payment::create($response->toArray());use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$mbwayResponse = Ifthenpay::mbway()->generatePayment(
orderId: '1000',
amount: '10.99',
mobileNumber: '351#912345678',
description: 'Order #1887', // optional
email: 'jane@example.com', // optional
expiryMinutes: 10, // optional
);
if(!$mbwayResponse->isSuccessful()) {
throw new Exception("Error Generating Payment", 1);
}
// store payment or another action
Payment::create($mbwayResponse->toArray());use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$payshopResponse = Ifthenpay::payshop()->generatePayment(
orderId: '1000',
amount: '10.99',
expiryDays: 5, // optional
);
if(!$payshopResponse->isSuccessful()) {
throw new Exception("Error Generating Payment", 1);
}
// store payment or another action
Payment::create($payshopResponse->toArray());use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$creditcardResponse = Ifthenpay::creditcard()->generatePayment(
orderId: '1000',
amount: '10.99',
successUrl: 'https://example.com/success',
errorUrl: 'https://example.com/error',
cancelUrl: 'https://example.com/cancel',
language: 'en', // optional
expiryMinutes: 15, // optional
);
if(!$creditcardResponse->isSuccessful()) {
throw new Exception("Error Generating Payment", 1);
}
// store payment or another action
Payment::create($creditcardResponse->toArray());use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$pixResponse = Ifthenpay::pix()->generatePayment(
orderId: '1000',
amount: '10.99',
customerCpf: '123.456.789-00',
customerName: 'John Doe',
customerEmail: 'john@example.com',
customerPhone: '351912345678',
redirectUrl: 'https://example.com/redirect',
description: 'Order #1887', // optional
customerAddress: 'Rua Exemplo', // optional
customerStreetNumber: '10', // optional
customerCity: 'Lisboa', // optional
customerZipCode: '1000-001', // optional
customerState: 'Lisboa', // optional
expiryMinutes: 15, // optional
);
if(!$pixResponse->isSuccessful()) {
throw new Exception("Error Generating Payment", 1);
}
// store payment or another action
Payment::create($pixResponse->toArray());use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$paybylinkResponse = Ifthenpay::paybylink()->generatePayment(
orderId: '1000',
amount: '10.99',
successUrl: 'https://example.com/success',
errorUrl: 'https://example.com/error',
cancelUrl: 'https://example.com/cancel',
description: 'Order #1887', // optional
expiryDays: 5, // optional
otp: false, // optional
lang: 'pt', // optional
btnCloseUrl: 'https://example.com/close', // optional
btnCloseLabel: 'Close', // optional
);
if(!$paybylinkResponse->isSuccessful()) {
throw new Exception("Error Generating Payment", 1);
}
// store payment or another action
Payment::create($paybylinkResponse->toArray());If you published and ran the migration for this package, you can use the Payment model Ifthenpay\Laravel\Models\Payment to persist the payments in your database
You can store a payment separately right after generating it:
use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$multibancoResponse = Ifthenpay::multibanco()->generatePayment(orderId: 'order-1887', amount: '10.99', expiryDays: 3);
Payment::create($multibancoResponse->toArray());Or add the HasIfthenpayPayments trait to any Eloquent model (e.g. your Order) to generate and persist a payment linked to it in one line.
// app/Models/Order.php
use Ifthenpay\Laravel\Traits\HasIfthenpayPayments;
class Order extends Model
{
use HasIfthenpayPayments;
}// app/Http/Controllers/YourController.php
use App\Models\Order;
$order = Order::find(1);
$payment = $order->createMbwayPayment(orderId: 'order-1887', amount: '10.99', mobileNumber: '351#912345678');
$orderPayments = $order->payments; // all payments for this order (MorphMany)
$orderMbwayPayments = $order->mbwayPayments; // only its MB WAY paymentsEquivalent methods to createMbwayPayment() and mbwayPayments() exist for every payment method.
| Payment method | Method | Eloquent relation |
|---|---|---|
| Multibanco | createMultibancoPayment() | multibancoPayments() |
| MB WAY | createMbwayPayment() | mbwayPayments() |
| Payshop | createPayshopPayment() | payshopPayments() |
| Pix | createPixPayment() | pixPayments() |
| Credit Card | createCreditcardPayment() | creditcardPayments() |
| Pay by Link | createPaybylinkPayment() | paybylinkPayments() |
The payment model also exposes the following methods
| Method | Description |
|---|---|
| isPaid() | check if payment record has paid status, returns true if payment status is paid (Enum of PaymentStatus) |
| secondsToExpire() | returns seconds remaining until expires_at, or 0 if already past/unset |
| totalSecondsToExpire() | returns total seconds between created_at and expires_at, or 0 if either is unset, useful if you want to create a progress bar like blade component of the remaining time a user has to complete the payment |
| markAsPaid() | sets status to paid and stamps paid_at (if not already set) |
| markAsCanceled() | sets status to canceled and stamps canceled_at |
| markAsExpired() | sets status to expired |
| scopePending() | query scope for payments with status pending |
| scopePaid() | query scope for payments with status paid |
| scopeCanceled() | query scope for payments with status canceled |
| scopeExpired() | query scope for pending payments whose expires_at has passed |
| scopeByMultibanco() | query scope for payments generated via Multibanco |
| scopeByMbway() | query scope for payments generated via MB WAY |
| scopeByPayshop() | query scope for payments generated via Payshop |
| scopeByPix() | query scope for payments generated via Pix |
| scopeByCreditcard() | query scope for payments generated via Credit Card |
| scopeByPaybylink() | query scope for payments generated via Pay by Link |
Register your callback URL with ifthenpay via the ifthenpay:register-webhook command.
Note: If IFTHENPAY_ANTIPHISHING_KEY isn't set yet, the command offers to generate and save one for you.
With no options (recommended), it registers a webhook for every payment method you have a key configured for (including every method bundled into paybylink_methods):
php artisan ifthenpay:register-webhookOr register a single method explicitly, if you are not using the config to load the method keys and are instantiating the gateway classes directly:
php artisan ifthenpay:register-webhook --method=mbway --key=ITP-000000By default this command registers the named route ifthenpay.webhook as the url, but you can use --url to override it and set your own route, just be sure to pass an absolute path without any querystrings.
php artisan ifthenpay:register-webhook --method=mbway --key=ITP-000000 --url="https://yoururl.com"If you need to add more parameters in the webhook, you must add them with --params
php artisan ifthenpay:register-webhook --method=mbway --key=ITP-000000 --url="https://yoururl.com" --params="scope=01"Warning: If the same key is used both standalone and inside
paybylink_methods(e.g.IFTHENPAY_MULTIBANCO_KEY=ITP-000000andIFTHENPAY_PAYBYLINK_METHODS=MULTIBANCO|ITP-000000;...), running the command with no options registers a webhook for that key twice — once for the standalone method, once forpaybylink— and whichever registration ifthenpay processes last wins, silently overriding the other's callback URL. This is easy to hit by accident sincepaybylink_methodscommonly reuses the same keys configured for the individual methods. Avoid reusing a key across a standalone method andpaybylink_methodsif they need different callback URLs, or re-run the command for the affected method/key after registering all methods to make sure the URL you want ends up as the last one registered.
By default the package registers a route at the path configured by ifthenpay.webhook_path (default ifthenpay/webhook), protected by the api middleware group and a named rate limiter (ifthenpay-webhook, default 60/minute — configurable via IFTHENPAY_WEBHOOK_RATE_LIMIT_PER_MINUTE).
To fully customize or disable this route (e.g. to point it at your own controller), publish the routes file into your app:
php artisan vendor:publish --tag=ifthenpay-routesOnce routes/ifthenpay.php exists in your application, the package loads that file instead of its own — edit, replace, or empty it as needed.
Incoming requests are validated (required pm/apk/val/oid/ref/req query params, apk checked against IFTHENPAY_ANTIPHISHING_KEY), matched to a stored Payment by method-specific fields, and amount-checked in cents to avoid float issues. On success the matching payment is marked paid; a negative amount is treated as a refund and leaves the payment's status untouched. Every outcome fires an event you can listen for — see Payment Webhook Confirmed, Refunded, and Rejected below.
All payment methods can be attributed an expiration, but only Multibanco(online), Payshop, Pay-by-link can use it to block access to payment after that time expires, meaning after that payment has expired, the user will not be able to pay (Multibanco and Payshop) or access the gateway page (Pay-by-link).
This functionality can still be used to manage abandoned payments for all payment methods, so that you can update the payments that are pending to expired. To use this functionality you need to pass the expiration (varies by payment method: days or minutes) when generating the payment.
The command ifthenpay:payments:expire, marks pending payments as expired once expires_at has passed and fires an event Ifthenpay\Laravel\Events\PaymentExpired for each (--dry-run lists them without changing anything).
Schedule it yourself in routes/console.php, and set the frequency you like.
// /routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('ifthenpay:payments:expire')->hourly();If you want to add expiration to payments you may use this table as suggestion
| Payment method | Suggested expiration |
|---|---|
| Multibanco | 3 days |
| MB WAY | 4 minutes |
| Payshop | 3 days |
| Credit Card | 15 minutes |
| Pix | 5 minutes |
| Pay by Link | 1 day |
You can make use of these utilities if you want to change/improve the user experience, but are optional and you can still implement the methods without relying on them.
MB WAY payments can be actively polled for status via checkStatus(), in addition to being updated by webhook. It requires the Payment's request_id (returned by ifthenpay when the payment was generated) to be set.
This functionality is useful when implementing a countdown timer that shows feedback about the status to the user.
use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$payment = Payment::find(1);
$statusResponse = Ifthenpay::mbway()->checkStatus($payment);
if ($statusResponse->isSuccessful()) {
$payment->markAsPaid();
}checkStatus() returns a MbwayStatusResponse with a typed status (Ifthenpay\Laravel\Enums\MbwayStatus: PENDING, PAID, EXPIRED, REFUSED_BY_USER, REJECTED_BY_MBWAY) and a message. isSuccessful() only returns true when status is MbwayStatus::PAID.
// ifthenpay/laravel-ifthenpay/src/Responses/MbwayStatusResponse.php
public function __construct(
public ?string $message,
public MbwayStatus $status,
) {}When generating a credit card payment, you'll get a Credit Card gateway page url in the response, to which you will redirect the user, upon being redirected back (to successUrl, errorUrl, or cancelUrl), ifthenpay appends a sk (secret key) query parameter.
Use verifyPayment() to confirm the redirect is authentic before trusting it — e.g. before showing a success page.
You can also ignore this verification system and rely solely on the webhook.
use Ifthenpay\Laravel\Exceptions\IfthenpayException;
use Ifthenpay\Laravel\Facades\Ifthenpay;
use Ifthenpay\Laravel\Models\Payment;
$payment = Payment::where('order_id', request('orderId'))->firstOrFail();
try {
Ifthenpay::creditcard()->verifyPayment(secretKey: request('sk'), payment: $payment);
} catch (IfthenpayException $e) {
abort(403, 'Invalid payment return.');
}
// verification passed, safe to treat as returning from a genuine ifthenpay redirectverifyPayment() returns void on success and throws IfthenpayException (secret key mismatch) on failure — it does not return a response object like the other gateway methods.
The package registers its views under the ifthenpay namespace, so its Blade components are available anywhere in your app as <x-ifthenpay::*>, no publishing required.
All of them are styled with Tailwind CSS v4 utility classes (including dark: variants for dark mode) and ship no CSS of their own — they render correctly out of the box only in an app that already has Tailwind set up. See Customizing the components below if your app doesn't use Tailwind, or if you just want a different look.
Renders a card summarizing a Payment model, dispatching to the right method-specific partial (below) based on $payment->method.
<x-ifthenpay::payment-details :payment="$payment" />
{{-- show a status badge (pending/paid/canceled/expired/failed) alongside the amount --}}
<x-ifthenpay::payment-details :payment="$payment" :show-status="true" />| Prop | Type | Default | Purpose |
|---|---|---|---|
payment |
Ifthenpay\Laravel\Models\Payment |
— | The payment to render. |
showStatus |
bool |
false |
Show a colored status badge next to the amount. |
Any attributes you pass through (e.g. class="...") are merged onto the root <div>.
payment-details renders one of these internally based on $payment->method, but each can also be used standalone if you're building your own layout — they all just take a :payment prop:
| Component | Method | Renders |
|---|---|---|
<x-ifthenpay::payment-details-multibanco> |
Multibanco | Entity/reference, and expiry date if expires_at is set. |
<x-ifthenpay::payment-details-mbway> |
MB WAY | Instructions to confirm in-app, plus a live countdown bar to expires_at (vanilla JS, no Alpine/Livewire dependency). |
<x-ifthenpay::payment-details-payshop> |
Payshop | Reference to pay at a Payshop agent/CTT store, and expiry date if set. |
<x-ifthenpay::payment-details-credit-card> |
Credit Card | A link to payment_url (same tab, so the gateway's redirect back to successUrl/errorUrl/cancelUrl lands where the user started). |
<x-ifthenpay::payment-details-pix> |
Pix | A link to payment_url (same tab, so the gateway's redirect back to redirectUrl lands where the user started). |
<x-ifthenpay::payment-details-pay-by-link> |
Pay by Link | A link to payment_url (same tab). |
Each partial no-ops (renders nothing) if the field it needs (entity/reference/payment_url) isn't set on the payment yet.
These components are bland and generic, because they are meant as a base for what you may want to display to your user. Publish the views to get an editable copy in your own app and customize it to your needs:
php artisan vendor:publish --tag=ifthenpay-viewsThis copies every .blade.php file (payment-details and all six method-specific partials) into resources/views/vendor/ifthenpay/components/. Laravel prefers published views over the package's own, so once they exist there, editing them (markup, copy, or swapping the Tailwind classes for your own CSS/framework) is picked up automatically by every <x-ifthenpay::*> tag already in use — no need to change any of your existing usages.
If you keep the Tailwind classes, make sure your resources/views/vendor/ifthenpay/** directory is covered by Tailwind's @source/content scanning (Tailwind v4's automatic content detection already covers resources/views by default, so this is usually a non-issue unless you publish elsewhere or use a custom Tailwind config).
The package fires typed events for webhook outcomes and payment expiry. Every event implements Ifthenpay\Laravel\Contracts\LoggableEvent (logLevel(), logMessage(), logContext()), which is what the bundled LogIfthenpayEvent listener relies on.
Fired by the ifthenpay:payments:expire command for each payment it marks as expired (see Expiring payments).
use Ifthenpay\Laravel\Events\PaymentExpired;
use Illuminate\Support\Facades\Event;
Event::listen(function (PaymentExpired $event) {
// $event->payment — the Payment model instance that was just marked as expired
});Fired when an incoming webhook passes validation with a positive amount matching the stored payment. The payment has already been marked as paid by the time this fires.
use Ifthenpay\Laravel\Events\PaymentWebhookConfirmed;
use Illuminate\Support\Facades\Event;
Event::listen(function (PaymentWebhookConfirmed $event) {
// $event->payload — Ifthenpay\Laravel\DTO\WebhookPayload: method, orderId, reference, requestId, amount
});Fired when an incoming webhook carries a negative amount. The matching payment's status is left untouched. This packaged does not have a refund feature, but you can make use of this event to implement it yourself. Check the official documentation on how to implement the refund endpoint LINK
use Ifthenpay\Laravel\Events\PaymentWebhookRefunded;
use Illuminate\Support\Facades\Event;
Event::listen(function (PaymentWebhookRefunded $event) {
// $event->payload — Ifthenpay\Laravel\DTO\WebhookPayload: method, orderId, reference, requestId, amount
});Fired when an incoming webhook fails validation — missing/invalid query params, an anti-phishing key mismatch, no matching payment, or an amount mismatch. The HTTP response is always a generic 400 regardless of reason, so a caller can't learn which check failed; the reason is only available to your listener.
use Ifthenpay\Laravel\Events\PaymentWebhookRejected;
use Illuminate\Support\Facades\Event;
Event::listen(function (PaymentWebhookRejected $event) {
// $event->payload — Ifthenpay\Laravel\DTO\WebhookPayload, built defensively from the raw (possibly invalid) query
// $event->reason — internal string explaining why the webhook was rejected
});Optionaly, you can log the webhook events by just using the event listener LogIfthenpayEvent included in this package to enable logging for the four events mentioned above.
To enable it, just add this line in the your AppServiceProvider.
// app/Providers/AppServiceProvider.php
use Ifthenpay\Laravel\Contracts\LoggableEvent;
use Ifthenpay\Laravel\Listeners\LogIfthenpayEvent;
use Illuminate\Support\Facades\Event;
Event::listen(LoggableEvent::class, LogIfthenpayEvent::class);This log never includes secrets (e.g. the webhook's anti-phishing key is stripped before logging). It reports
method, order_id, reference, request_id, amount for webhook events, or id, method, order_id,
expires_at, status for PaymentExpired.
MIT.