mosamy / payment
Simple payment gateway integration for Laravel
Requires
- php: >=8.2
- laravel/cashier: ^16.5
- myfatoorah/laravel-package: 2.1
README
A multi-gateway payment integration package for Laravel. Provides a unified API to initiate payments, store transactions, handle gateway callbacks, and query payment state on any Eloquent model — without coupling your application to a specific provider.
Table of Contents
- Requirements
- Installation
- Configuration
- Database
- Supported Gateways
- Basic Usage
- PaymentPayload DTO
- CheckoutResponse DTO
- PaymentResponse DTO
- The Invoiceable Contract
- HasTransactions Trait
- Transaction Model
- Adding a Custom Gateway
- Gateway Reference
Requirements
| Dependency | Version |
|---|---|
| PHP | >= 8.2 |
| Laravel | >= 10 |
| myfatoorah/laravel-package | ^2.1 |
| stripe/stripe-php (Cashier) | ^16.5 |
All other gateways (Tabby, Tamara, PayTabs, Paymob, PayPal, 2Checkout, HyperPay) use Laravel's built-in HTTP client — no additional Composer dependencies required.
Installation
Install via Composer:
composer require mosamy/payment
Laravel's auto-discovery will register Mosamy\Payment\PaymentServiceProvider automatically. No manual provider registration is needed.
Configuration
Publishing Config
Publish the configuration file to your application:
php artisan vendor:publish --tag=payment-config
This creates config/payment.php in your application which you can freely modify.
Environment Variables
Add the following to your .env file according to which gateways you use:
# Active default gateway
PAYMENT_DEFAULT=myfatoorah
# Comma-separated list of enabled gateways (defaults to PAYMENT_DEFAULT if omitted)
PAYMENT_ACTIVATED_GATEWAYS=myfatoorah,stripe,tabby,tamara,paytabs,paymob,paypal,2checkout,hyperpay
# Default currency (ISO 4217 — defaults to SAR if omitted)
PAYMENT_CURRENCY=SAR
# Global redirect route (named route with {transaction} model binding)
# If set, will be used as the post-payment redirect for all transactions that don't specify a redirectUrl
# The route must accept a transaction model parameter: Route::get('/transaction/{transaction}', ...)->name('front.transaction')
PAYMENT_REDIRECT_ROUTE=front.transaction
# ── MyFatoorah ────────────────────────────────────────────────────────────────
MYFATOORAH_API_KEY=your-api-key-here
MYFATOORAH_TEST_MODE=false
MYFATOORAH_COUNTRY_ISO=SAU # e.g. SAU, KWT, ARE, QAT, BHR, OMN, EGY
# ── Stripe ────────────────────────────────────────────────────────────────────
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
# ── Tabby ─────────────────────────────────────────────────────────────────────
TABBY_PUBLIC_KEY=pk_test_...
TABBY_SECRET_KEY=sk_test_...
TABBY_MERCHANT_CODE=your-merchant-code
TABBY_TEST_MODE=false
# ── Tamara ────────────────────────────────────────────────────────────────────
TAMARA_API_TOKEN=your-api-token
TAMARA_NOTIFICATION_TOKEN=your-notification-token
TAMARA_TEST_MODE=false
# ── PayTabs ───────────────────────────────────────────────────────────────────
PAYTABS_PROFILE_ID=your-profile-id
PAYTABS_SERVER_KEY=your-server-key
PAYTABS_REGION=saudi # saudi, egypt, oman, jordan, uae, global
# ── Paymob ────────────────────────────────────────────────────────────────────
PAYMOB_API_KEY=your-api-key
PAYMOB_INTEGRATION_ID=123456
PAYMOB_IFRAME_ID=654321
PAYMOB_HMAC_SECRET=your-hmac-secret
# ── PayPal ────────────────────────────────────────────────────────────────────
PAYPAL_CLIENT_ID=your-client-id
PAYPAL_CLIENT_SECRET=your-client-secret
PAYPAL_TEST_MODE=false
# ── 2Checkout ─────────────────────────────────────────────────────────────────
TWOCHECKOUT_SELLER_ID=your-seller-id
TWOCHECKOUT_SECRET_KEY=your-secret-key
TWOCHECKOUT_SECRET_WORD=your-secret-word
TWOCHECKOUT_TEST_MODE=false
# ── HyperPay ──────────────────────────────────────────────────────────────────
HYPERPAY_BEARER_TOKEN=your-bearer-token
HYPERPAY_ENTITY_ID=your-entity-id
HYPERPAY_TEST_MODE=false
HYPERPAY_CHECKOUT_ROUTE=front.pay # named route in your app that shows the payment page
Full Config File
// config/payment.php
return [
/*
|--------------------------------------------------------------------------
| Default Gateway
|--------------------------------------------------------------------------
| The gateway used when no gateway is explicitly specified.
| Must match a key under "gateways".
*/
'default' => env('PAYMENT_DEFAULT'),
/*
|--------------------------------------------------------------------------
| Active Gateways
|--------------------------------------------------------------------------
| Comma-separated list of enabled gateway keys. Only listed gateways are
| registered as valid callback route parameters.
*/
'active' => explode(',', env('PAYMENT_ACTIVATED_GATEWAYS', env('PAYMENT_DEFAULT'))),
/*
|--------------------------------------------------------------------------
| Default Currency
|--------------------------------------------------------------------------
| Used as the fallback currency when PaymentPayload is constructed without
| an explicit currency value.
*/
'currency' => env('PAYMENT_CURRENCY', 'SAR'),
/*
|--------------------------------------------------------------------------
| Redirect Route
|--------------------------------------------------------------------------
| Global fallback redirect route used when PaymentPayload::redirectUrl
| is not explicitly provided. This named route must accept {transaction}
| with implicit route model binding.
*/
'redirect_route' => env('PAYMENT_REDIRECT_ROUTE'),
/*
|--------------------------------------------------------------------------
| Transaction Statuses
|--------------------------------------------------------------------------
| The allowed values for the transaction status column and PaymentResponse.
*/
'statuses' => ['pending', 'paid', 'failed', 'refunded', 'cancelled'],
/*
|--------------------------------------------------------------------------
| Gateways
|--------------------------------------------------------------------------
| Each entry maps a friendly name to its gateway class and its own config
| block. You may add custom gateways here.
*/
'gateways' => [
'myfatoorah' => [
'class' => \Mosamy\Payment\Gateways\MyFatoorah::class,
'config' => [
'api_key' => env('MYFATOORAH_API_KEY'),
'test_mode' => env('MYFATOORAH_TEST_MODE', false),
'country_iso' => env('MYFATOORAH_COUNTRY_ISO', 'SAU'),
],
],
'stripe' => [
'class' => \Mosamy\Payment\Gateways\Stripe::class,
'config' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
],
'tabby' => [
'class' => \Mosamy\Payment\Gateways\Tabby::class,
'config' => [
'public_key' => env('TABBY_PUBLIC_KEY'),
'secret_key' => env('TABBY_SECRET_KEY'),
'merchant_code' => env('TABBY_MERCHANT_CODE'),
'test_mode' => env('TABBY_TEST_MODE', false),
],
],
'tamara' => [
'class' => \Mosamy\Payment\Gateways\Tamara::class,
'config' => [
'api_token' => env('TAMARA_API_TOKEN'),
'notification_token' => env('TAMARA_NOTIFICATION_TOKEN'),
'test_mode' => env('TAMARA_TEST_MODE', false),
],
],
'paytabs' => [
'class' => \Mosamy\Payment\Gateways\PayTabs::class,
'config' => [
'profile_id' => env('PAYTABS_PROFILE_ID'),
'server_key' => env('PAYTABS_SERVER_KEY'),
'region' => env('PAYTABS_REGION', 'saudi'),
],
],
'paymob' => [
'class' => \Mosamy\Payment\Gateways\Paymob::class,
'config' => [
'api_key' => env('PAYMOB_API_KEY'),
'integration_id' => env('PAYMOB_INTEGRATION_ID'),
'iframe_id' => env('PAYMOB_IFRAME_ID'),
'hmac_secret' => env('PAYMOB_HMAC_SECRET'),
],
],
'paypal' => [
'class' => \Mosamy\Payment\Gateways\PayPal::class,
'config' => [
'client_id' => env('PAYPAL_CLIENT_ID'),
'client_secret' => env('PAYPAL_CLIENT_SECRET'),
'test_mode' => env('PAYPAL_TEST_MODE', false),
],
],
'2checkout' => [
'class' => \Mosamy\Payment\Gateways\TwoCheckout::class,
'config' => [
'seller_id' => env('TWOCHECKOUT_SELLER_ID'),
'secret_key' => env('TWOCHECKOUT_SECRET_KEY'),
'secret_word' => env('TWOCHECKOUT_SECRET_WORD'),
'test_mode' => env('TWOCHECKOUT_TEST_MODE', false),
],
],
'hyperpay' => [
'class' => \Mosamy\Payment\Gateways\HyperPay::class,
'config' => [
'bearer_token' => env('HYPERPAY_BEARER_TOKEN'),
'entity_id' => env('HYPERPAY_ENTITY_ID'),
'base_url' => env('HYPERPAY_TEST_MODE', false)
? 'https://eu-test.oppwa.com'
: 'https://eu-prod.oppwa.com',
'checkout_route' => env('HYPERPAY_CHECKOUT_ROUTE'),
],
],
],
];
Database
Run the package migrations to create the transactions table:
php artisan migrate
The transactions table schema:
| Column | Type | Notes |
|---|---|---|
| id | bigint (PK) | |
| reference | string (unique) | Invoice / order reference, indexed |
| gateway | string | Gateway key (e.g. myfatoorah), indexed |
| amount | decimal(12,2) | |
| currency | string | ISO 4217 code (e.g. SAR, USD) |
| status | enum | pending, paid, failed, refunded, cancelled |
| meta | json nullable | Customer data and extra fields |
| response | json nullable | Raw gateway responses stored per stage |
| payable_type | string nullable | Polymorphic owner type |
| payable_id | bigint nullable | Polymorphic owner id |
| created_at | timestamp | |
| updated_at | timestamp |
The allowed status values are driven by config('payment.statuses'), so they stay in sync if you extend that list.
Supported Gateways
| Gateway | Status | Key | Integration method |
|---|---|---|---|
| MyFatoorah | Full support | myfatoorah | Hosted redirect |
| Stripe | Full support | stripe | Hosted redirect |
| Tabby | Full support | tabby | Hosted redirect |
| Tamara | Full support | tamara | Hosted redirect |
| PayTabs | Full support | paytabs | Hosted redirect |
| Paymob | Full support | paymob | Hosted iframe |
| PayPal | Full support | paypal | Hosted redirect |
| 2Checkout | Full support | 2checkout | Hosted redirect |
| HyperPay | CopyAndPay (full) | hyperpay | Embedded widget (iframe) |
Basic Usage
Creating a Payment
The main entry point is the Payment class. Call Payment::gateway() with an optional gateway key (falls back to PAYMENT_DEFAULT), then pass a PaymentPayload to create():
use Mosamy\Payment\Payment;
use Mosamy\Payment\DTO\PaymentPayload;
$payload = new PaymentPayload(
id: (string) $order->id, // unique identifier for this invoice
amount: $order->total,
currency: 'SAR',
customer: [
'name' => $user->name,
'email' => $user->email,
'phone' => $user->phone, // MyFatoorah requires 05XXXXXXXX format
],
items: [
['name' => 'Product A', 'quantity' => 2, 'unit_cost' => 50.00],
],
meta: [
'payment_method_id' => 2, // required by MyFatoorah; get list via their API
],
redirectUrl: route('orders.show', $order),
);
$response = Payment::gateway('myfatoorah') // or omit to use default
->payable($order) // optional: links transaction to any Eloquent model
->create($payload);
if ($response->success) {
return redirect($response->redirectUrl);
}
// Handle failure
return back()->withErrors($response->response['message'] ?? 'Payment initiation failed.');
->payable($model) is optional. When provided, the transaction row is linked to that model via a polymorphic relation (payable_type / payable_id).
Handling the Callback
The package automatically registers a callback route:
GET /payments/callback/{gateway}/{transaction}
Named: mosamy.payment.callback
The gateway redirects the user back to this URL after payment. The controller resolves the Transaction model via route-model binding, delegates to the correct gateway's handleCallback(), and redirects the user to PaymentResponse::$redirectUrl (or / if none is set).
You do not need to define this route yourself. It is loaded automatically by the service provider.
PaymentPayload DTO
use Mosamy\Payment\DTO\PaymentPayload;
new PaymentPayload(
id: string, // Required. Unique invoice identifier (e.g. order id).
amount: float, // Required. Amount in the given currency.
reference: ?string, // Optional. Defaults to "REF-{id}".
currency: ?string, // Optional. Defaults to config('payment.currency') — e.g. 'SAR'.
items: ?array, // Optional. Line items for the invoice.
customer: ?array, // Optional (required by some gateways). See gateway docs.
meta: ?array, // Optional. Extra data stored on the transaction + gateway.
redirectUrl: ?string, // Optional. Full URL to redirect after callback.
);
You can choose one of two redirect strategies:
- Set a full redirect URL in
PaymentPayload::$redirectUrl. - Omit
redirectUrland configure a global named route inconfig('payment.redirect_route')(must use{transaction}model binding).
Example — explicit full redirect URL in payload:
// No model involved, just a raw Payment::gateway() call.
$payload = new PaymentPayload(
id: 'INV-45',
amount: 120.50,
redirectUrl: url("transaction/INV-45"),
);
$response = Payment::gateway()->create($payload);
// Callback will redirect to the exact URL provided above.
Example — redirect from an Invoiceable model:
public function toPaymentPayload(?array $meta = []): PaymentPayload
{
return new PaymentPayload(
id: (string) $this->id,
amount: $this->total,
redirectUrl: url("/invoices/{$this->id}"), // static — model id is known here
);
}
Global Redirect Route (Config-based)
If no redirectUrl is provided in the PaymentPayload, the package will automatically use the redirect route defined in your config file (config('payment.redirect_route')). This route must have implicit route model binding for the transaction parameter.
Setup:
- Define a route in your
routes/web.phpwith transaction model binding:
Route::get('/transaction/{transaction}', function (Transaction $transaction) {
return view('transaction.show', ['transaction' => $transaction]);
})->name('front.transaction');
- Set the
PAYMENT_REDIRECT_ROUTEenvironment variable:
PAYMENT_REDIRECT_ROUTE=front.transaction
Now when you create a payment without an explicit redirectUrl, the package will automatically generate a URL like /transaction/123 (with the correct transaction id) as the redirect destination after payment.
Precedence:
- If
PaymentPayload::$redirectUrlis explicitly set, it takes priority over the config route. - If
PaymentPayload::$redirectUrlisnullandconfig('payment.redirect_route')is defined, the config route is used with the transaction model. - If both are empty, the callback controller redirects to
/(the application root).
Example — Invoiceable model with global config redirect:
class Invoice extends Model implements Invoiceable
{
use HasTransactions;
public function toPaymentPayload($meta = []): PaymentPayload
{
return new PaymentPayload(
id: (string) $this->id,
amount: $this->amount,
customer: [
'name' => 'John Doe',
'email' => 'john.doe@example.com',
'phone' => '0501234567'
],
meta: $meta
// redirectUrl is omitted here, so the config route will be used
// or you may use
//redirectUrl: route('invoice.show', $this->id)
);
}
}
After payment, the user will be redirected to your configured PAYMENT_REDIRECT_ROUTE route with the transaction model instance.
CheckoutResponse DTO
Returned by Payment::gateway()->create():
$response->success // bool — whether the checkout session was initiated
$response->transactionId // ?string — the local transaction id
$response->redirectUrl // ?string — gateway checkout URL to send the customer to
$response->response // array — raw gateway response
$response->toArray() // array — all fields as an array
When success is true, both redirectUrl and transactionId are guaranteed to be non-null (enforced in the constructor).
PaymentResponse DTO
Returned by Payment::gateway()->handleCallback():
$response->success // bool — whether the payment was confirmed
$response->status // string — final transaction status written to the database
$response->redirectUrl // ?string — where to redirect the user after callback
$response->response // array — raw gateway callback data
status must be one of the values defined in config('payment.statuses') (pending, paid, failed, refunded, cancelled). An InvalidArgumentException is thrown if an invalid value is provided.
The Invoiceable Contract
Implement Mosamy\Payment\Contracts\Invoiceable on any model that needs to be converted to a PaymentPayload:
use Mosamy\Payment\Contracts\Invoiceable;
use Mosamy\Payment\DTO\PaymentPayload;
class Order extends Model implements Invoiceable
{
public function toPaymentPayload(?array $meta = []): PaymentPayload
{
return new PaymentPayload(
id: (string) $this->id,
amount: $this->total,
currency: $this->currency,
customer: [
'name' => $this->user->name,
'email' => $this->user->email,
'phone' => $this->user->phone,
],
meta: array_merge([
'payment_method_id' => 2,
], $meta),
);
}
}
When your model also uses the HasTransactions trait, you can call $order->pay() directly (see below).
HasTransactions Trait
Add HasTransactions to any Eloquent model that initiates or owns payments:
use Mosamy\Payment\Traits\HasTransactions;
use Mosamy\Payment\Contracts\Invoiceable;
class Order extends Model implements Invoiceable
{
use HasTransactions;
// Must implement toPaymentPayload() when using ->pay()
public function toPaymentPayload(?array $meta = []): PaymentPayload { ... }
}
Relations
// All transactions belonging to this model
$order->transactions; // Collection<Transaction>
// The latest transaction
$order->transaction; // ?Transaction
Computed Attributes
$order->is_paid; // bool — latest transaction status is 'paid'
$order->has_pending_payment; // bool — latest transaction status is 'pending'
// Sum of all transactions with the given status (default: 'paid')
$order->totalAmount();
$order->totalAmount('failed');
Scopes
// Eager-load both relations at once
Order::withTransactions()->get();
// Filter models that have at least one transaction with the given status
Order::whereHasTransactions()->get();
Order::whereHasTransactions('failed')->get();
// Add a transactions_sum_amount column (filtered by status)
Order::withTotalAmount()->get();
Order::withTotalAmount('refunded')->get();
Quick Pay
When your model implements Invoiceable, you can initiate a payment in one call:
// Uses default gateway
$response = $order->pay();
// Specify gateway
$response = $order->pay(gateway: 'myfatoorah');
// Pass extra meta merged into toPaymentPayload()
$response = $order->pay(gateway: 'myfatoorah', meta: ['payment_method_id' => 6]);
pay() calls Payment::gateway($gateway)->payable($this)->create($this->toPaymentPayload($meta)) internally.
Transaction Model
Mosamy\Payment\Models\Transaction
| Method / Attribute | Description |
|---|---|
$transaction->status | pending | paid | failed | refunded | cancelled |
$transaction->gateway | Gateway key string |
$transaction->amount | Decimal amount |
$transaction->currency | ISO 4217 currency code |
$transaction->reference | Invoice reference |
$transaction->meta | Array of stored metadata (includes customer) |
$transaction->response | Array with invoice, checkout, and callback sub-keys |
$transaction->payable | Related model (polymorphic, nullable) |
$transaction->callbackUrl | Auto-generated route URL for this transaction |
Adding a Custom Gateway
- Create a class that extends
Mosamy\Payment\Contracts\Gateway:
namespace App\Payments;
use Mosamy\Payment\Contracts\Gateway;
use Mosamy\Payment\DTO\CheckoutResponse;
use Mosamy\Payment\DTO\PaymentPayload;
use Mosamy\Payment\DTO\PaymentResponse;
use Mosamy\Payment\Models\Transaction;
class MyCustomGateway extends Gateway
{
public function create(PaymentPayload $payload, Transaction $transaction): CheckoutResponse
{
// Call external API, get redirect URL ...
return new CheckoutResponse(
success: true,
transactionId: (string) $transaction->id,
redirectUrl: 'https://pay.example.com/invoice/...',
response: [],
);
}
public function handleCallback(array $payload, Transaction $transaction): PaymentResponse
{
// Verify signature, check status ...
return new PaymentResponse(
success: true,
status: 'paid',
redirectUrl: $transaction->response['invoice']['redirectUrl'] ?? '/',
response: $payload,
);
}
}
- Register the gateway in
config/payment.php:
'gateways' => [
// ...
'myprovider' => [
'class' => \App\Payments\MyCustomGateway::class,
'config' => [
'api_key' => env('MYPROVIDER_API_KEY'),
],
],
],
- Set your env:
PAYMENT_DEFAULT=myprovider
That's it. The route constraint, transaction creation, and callback flow are all handled automatically.
Gateway Reference
MyFatoorah
Required customer fields:
| Field | Format | Example |
|---|---|---|
name | string | "Ahmed Al-Zaidi" |
email | valid email | "a@example.com" |
phone | 05XXXXXXXX | "0512345678" |
Required meta fields:
| Field | Type | Description |
|---|---|---|
payment_method_id | integer | Payment method ID from MyFatoorah API |
Notes:
country_isocontrols which regional MyFatoorah endpoint is used.MobileCountryCodeis currently hardcoded to+966. If your application targets other regions, override by extending theMyFatoorahgateway class and overridingpayload().- Callback uses the same URL for success and error redirects. MyFatoorah appends
?paymentId=...on success.
Stripe
Required customer fields:
| Field | Format | Example |
|---|---|---|
email | valid email | "a@example.com" |
Optional items fields (falls back to a single line item from amount if omitted):
| Field | Type | Description |
|---|---|---|
name | string | Product name |
quantity | integer | Defaults to 1 |
unit_cost | float | Unit price |
currency | string | Overrides payload currency |
Notes:
- Stripe creates a Checkout Session; the customer is redirected to Stripe's hosted page.
- On success, Stripe appends
?session_id=...to the callback URL. - On cancel, Stripe appends
?canceled=1to the callback URL.
Tabby
Required customer fields:
| Field | Format | Example |
|---|---|---|
name | string | "Ahmed Al-Zaidi" |
email | valid email | "a@example.com" |
phone | string | "+966512345678" |
Optional meta fields:
| Field | Type | Description |
|---|---|---|
city | string | Shipping city |
address | string | Shipping address |
zip | string | Postal code |
lang | string | Language code (default: en) |
Notes:
- Tabby is a BNPL (Buy Now Pay Later) provider popular in the MENA region.
- Uses Tabby Checkout API v2. The customer is redirected to Tabby's hosted checkout page.
- Callback receives
?payment_id=...from Tabby. - Supports installments through their available products configuration.
Tamara
Required customer fields:
| Field | Format | Example |
|---|---|---|
name | string | "Ahmed Al-Zaidi" |
email | valid email | "a@example.com" |
phone | string | "+966512345678" |
Optional meta fields:
| Field | Type | Description |
|---|---|---|
country_code | string | ISO country code (default: SA) |
payment_type | string | PAY_BY_INSTALMENTS, PAY_BY_LATER etc. |
instalments | int | Number of instalments (default: 3) |
city | string | Shipping city |
address | string | Shipping address |
tax | float | Tax amount |
lang | string | Locale (default: en_US) |
Notes:
- Tamara is a BNPL provider for the MENA region.
- After checkout, the order must be authorised (handled automatically in
handleCallback). - Callback receives
?order_id=...from Tamara.
PayTabs
Required customer fields:
| Field | Format | Example |
|---|---|---|
name | string | "Ahmed Al-Zaidi" |
email | valid email | "a@example.com" |
phone | string | "0512345678" |
Optional meta fields:
| Field | Type | Description |
|---|---|---|
address | string | Billing address |
city | string | Billing city |
state | string | Billing state |
country_code | string | ISO country code (default: SA) |
zip | string | Postal code |
Config region values: saudi, egypt, oman, jordan, uae, global
Notes:
- PayTabs supports multiple MENA regions. Set
PAYTABS_REGIONto route to the correct endpoint. - Callback receives
?tranRef=...from PayTabs. - Response status
A= Authorised (paid),H= Hold,D= Declined,E= Error,V= Voided.
Paymob
Required customer fields:
| Field | Format | Example |
|---|---|---|
email | valid email | "a@example.com" |
Optional customer / meta fields:
| Field | Type | Description |
|---|---|---|
name | string | Customer full name (split into first/last) |
phone | string | Customer phone |
address | string | Billing street address |
city | string | Billing city |
state | string | Billing state |
zip | string | Postal code |
country_code | string | ISO country code (default: EG) |
Notes:
- Paymob uses a 3-step flow: authenticate → register order → generate payment key.
- The customer is redirected to Paymob's iframe with the payment token.
- Callback receives
success,is_pending, and other parameters from Paymob. - You need to create an integration and iframe in your Paymob dashboard.
PayPal
Required customer fields:
| Field | Format | Example |
|---|---|---|
email | valid email | "a@example.com" |
Optional meta fields:
| Field | Type | Description |
|---|---|---|
locale | string | PayPal locale (default: en-US) |
Notes:
- Uses PayPal REST API v2 (Orders). No SDK dependency required.
- Creates a checkout order, redirects to PayPal for approval, then captures on callback.
- On success, PayPal redirects with
?token=...&PayerID=.... - On cancel, the callback URL receives
?canceled=1. test_modetoggles between sandbox (api-m.sandbox.paypal.com) and live (api-m.paypal.com).
2Checkout
Required customer fields:
| Field | Format | Example |
|---|---|---|
name | string | "Ahmed Al-Zaidi" |
email | valid email | "a@example.com" |
Optional customer / meta fields:
| Field | Type | Description |
|---|---|---|
phone | string | Customer phone |
address | string | Billing address |
city | string | Billing city |
zip | string | Postal code |
country_code | string | ISO country code (default: US) |
lang | string | Language (default: en) |
Notes:
- Uses the 2Checkout (now Verifone) REST API v6.
- Falls back to hosted Buy-Link checkout if the API order creation doesn't return a 3DS redirect.
- Callback receives
?refno=...(reference number) from 2Checkout. test_modecontrols both the API test flag and the hosted checkout URL (sandbox vs secure).- Order statuses:
COMPLETE/AUTHRECEIVED→ paid,CANCELED→ cancelled,REFUND→ refunded.
HyperPay
Integration method: CopyAndPay (embedded payment widget). Server-to-Server integration is planned for a future release.
HyperPay's CopyAndPay flow is different from every other gateway in this package. Instead of redirecting the user to a hosted checkout page, HyperPay renders a payment widget (iframe) directly inside your own page. The flow has two distinct phases:
How CopyAndPay works
- Create a checkout session —
Payment::gateway('hyperpay')POSTs to the HyperPay API and receives checkout data (includingidandintegrity) stored intransaction->response['checkout']. A create request is treated as successful only when HyperPay returns result code000.200.100. - Show the payment widget — Your application redirects the user to a page of your own that includes the
<x-payment::hyperpay>Blade component. This page is identified by a named route whose route parameter must be the{transaction}model. - User pays — The widget submits the card data directly to HyperPay. HyperPay then returns to the package callback route (
/payments/callback/hyperpay/{transaction}) with aresourcePathparameter. - Callback resolved — The package controller calls
handleCallback(), which queries HyperPay's status endpoint usingresourcePath, maps the result code topaid / pending / cancelled / failed, and redirects the user to the URL configured in yourPaymentPayloador invoice model.
Step 1 — Configure
Add to .env:
HYPERPAY_BEARER_TOKEN=your-bearer-token
HYPERPAY_ENTITY_ID=your-entity-id
HYPERPAY_TEST_MODE=false
HYPERPAY_CHECKOUT_ROUTE=front.pay # see step 2
Add to config/payment.php (or rely on the published default):
'hyperpay' => [
'class' => \Mosamy\Payment\Gateways\HyperPay::class,
'config' => [
'bearer_token' => env('HYPERPAY_BEARER_TOKEN'),
'entity_id' => env('HYPERPAY_ENTITY_ID'),
'base_url' => env('HYPERPAY_TEST_MODE', false)
? 'https://eu-test.oppwa.com'
: 'https://eu-prod.oppwa.com',
'checkout_route' => env('HYPERPAY_CHECKOUT_ROUTE'),
],
],
Step 2 — Create the checkout page in your application
You need one route in your application that displays the payment widget. The route must accept a {transaction} parameter (route-model binding). The route name is what you put in checkout_route.
// routes/web.php
Route::get('pay/{transaction}', Pay::class)->name('front.pay');
The corresponding Livewire component (or regular controller):
use Mosamy\Payment\Models\Transaction;
class Pay extends Component
{
public Transaction $transaction;
public function mount(Transaction $transaction)
{
$this->transaction = $transaction;
}
}
The Blade view for that page must include the HyperPay component:
{{-- resources/views/front/pay.blade.php --}}
<x-payment::hyperpay :transaction="$transaction" />
The component renders the official HyperPay paymentWidgets.js script inline and a <form> that submits directly to $transaction->callback_url (the package callback route).
You can access the linked invoice / order on the same page via:
{{ $transaction->payable->amount }}
{{ $transaction->payable->status }}
Step 3 — Initiate payment
use Mosamy\Payment\Payment;
$response = Payment::gateway('hyperpay')
->payable($invoice)
->create($invoice->toPaymentPayload());
if ($response->success) {
return redirect($response->redirectUrl); // → your 'front.pay' route with the transaction id
}
Or using the HasTransactions quick-pay shorthand:
$response = $invoice->pay(gateway: 'hyperpay');
The CheckoutResponse::$redirectUrl already points to your checkout route with the transaction ID appended, so a simple redirect($response->redirectUrl) is all you need.
Changing the checkout route at runtime
If you need to override checkout_route dynamically (e.g. you serve multiple storefronts), call the static helper before initiating payment:
use Mosamy\Payment\Gateways\HyperPay;
HyperPay::setCheckoutRoute('storefront.pay');
$response = Payment::gateway('hyperpay')->payable($invoice)->create(...);
This writes the new route name into the live config for the duration of the request only.
Required customer fields:
| Field | Format | Example |
|---|---|---|
email | valid email | "mosamy@example.com" |
name | string (maps to givenName) | "Mohamed" |
surname | string | "Alotaibi" |
phone | string | "0512345678" |
address | string (maps to billing.street1) | "King Fahd Rd" |
city | string | "Riyadh" |
state | string | "Riyadh" |
postal_code | string (maps to billing.postcode) | "12345" |
country | Alpha-2 code, regex [A-Z]{2} | "SA" |
Required meta fields:
| Field | Type | Description |
|---|---|---|
merchantTransactionId | string | UUID-style merchant-side identifier, must be 12–14 characters long |
Notes:
- The only gateway in this package that uses an embedded widget instead of a redirect.
checkout_routemust be a named route in your application that accepts{transaction}as its only route parameter.- The
<x-payment::hyperpay>component requires bothresponse.checkout.idandresponse.checkout.integrityto be present on the transaction; it throwsInvalidArgumentExceptionif either is missing. - HyperPay payload validation requires
amount,currency, all listedcustomerfields, andmeta.merchantTransactionId; create fails early if any are missing. meta.merchantTransactionIdmust be between 12 and 14 characters.customer.countryis required and must be an ISO 3166-1 Alpha-2 code ([A-Z]{2}), e.g.SA,AE,EG.- The HyperPay API request uses flat dot-notation keys (
customer.givenName,customer.surname,customer.email,billing.street1,billing.city,billing.state,billing.postcode,billing.country) — this is handled internally by the gateway. - HyperPay request payload currently sends currency as
SAR. - Card brands rendered by the widget default to
VISA MASTER AMEX. To customise them, publish the package views and editcomponents/hyperpay.blade.php. - Callback status mapping: result codes
000.000.000 / 000.100.110–112→paid;000.200.000 / 800.400.500 / 100.400.500→pending;100.396.x / 100.397.x→cancelled; anything else →failed. - Server-to-Server integration is coming soon.
Support
For support, please contact dev.mohamed.samy@gmail.com.
Author
Created by Mohamed Samy