PHPackages                             digitaltunnel/moyasar - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. digitaltunnel/moyasar

ActiveLibrary[Payment Processing](/categories/payments)

digitaltunnel/moyasar
=====================

Moyasar payment gateway SDK for Laravel — credit card, Apple Pay, Samsung Pay, STC Pay, tokenization, webhooks.

v1.0.1(1mo ago)049MITPHPPHP ^8.2CI failing

Since May 29Pushed 1mo agoCompare

[ Source](https://github.com/digital-tunnel/moyasar)[ Packagist](https://packagist.org/packages/digitaltunnel/moyasar)[ RSS](/packages/digitaltunnel-moyasar/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (11)Versions (3)Used By (0)

Moyasar for Laravel
===================

[](#moyasar-for-laravel)

 [![Latest Version](https://camo.githubusercontent.com/3d2cfb75c7f7e493c5891c54447856fd354e053007b41d6dee1d96678a0e8dbc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6469676974616c74756e6e656c2f6d6f79617361722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/digitaltunnel/moyasar) [![Tests](https://camo.githubusercontent.com/4739c18b7fc2689fe8c37ca4889bdeaba305197569eb1d92bbde2212ec08ee26/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6469676974616c2d74756e6e656c2f6d6f79617361722f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/digital-tunnel/moyasar/actions/workflows/tests.yml) [![Quality](https://camo.githubusercontent.com/e088631990355d257272fe87a61d41bc73bb7646ed2798bc3a3d3be444a5711c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6469676974616c2d74756e6e656c2f6d6f79617361722f7175616c6974792e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307175616c697479267374796c653d666c61742d737175617265)](https://github.com/digital-tunnel/moyasar/actions/workflows/quality.yml) [![Downloads](https://camo.githubusercontent.com/0b01acd73192947ea2bd9353f993023f51dc03d8200e52661527c4977ce62525/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6469676974616c74756e6e656c2f6d6f79617361722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/digitaltunnel/moyasar) [![License](https://camo.githubusercontent.com/850509f149eb4c06a3b732deece0b8c49e8d45841902e11646a3aa75cbd24995/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6469676974616c74756e6e656c2f6d6f79617361722e7376673f7374796c653d666c61742d737175617265)](LICENSE)

A **typed, testable, batteries-included** SDK for the [Moyasar](https://moyasar.com) payment gateway with first-class Laravel integration. Credit cards, Apple Pay, Samsung Pay, STC Pay, tokenization, invoices, and webhooks — all covered out of the box.

```
use DigitalTunnel\Moyasar\Facades\Moyasar;

$payment = Moyasar::payments()->create(
    CreatePaymentRequest::make()
        ->amountInMajorUnits(150.00)         // 150.00 SAR
        ->callbackUrl(route('checkout.return'))
        ->source(CreditCardSource::make('Sara A.', '4111111111111111', 12, 2030, '123'))
);

return $payment->requires3ds()
    ? redirect($payment->transactionUrl())
    : back();
```

Why this package
----------------

[](#why-this-package)

- 🎯 **Typed end to end** — readonly DTOs and enums, not loose arrays.
- 🧱 **Fluent builders** for every payment source and the create-payment / create-invoice requests.
- 🔁 **Webhooks that just work** — auto-registered route outside the `web` group (no CSRF dance), signature verification, idempotent de-duplication, optional queued processing.
- 🧾 **Invoices API** — hosted payment pages / payment links in two lines.
- 🛡️ **Secure-by-default checkout** — a `verifyCallback()` helper that re-fetches and validates the payment before you fulfil an order.
- 🧪 **Genuinely testable** — a `MoyasarFake` double, plus 160+ tests, PHPStan level 6, and ~100% type coverage in this very package.

---

Requirements
------------

[](#requirements)

PHP8.2, 8.3, 8.4Laravel11, 12, 13Installation
------------

[](#installation)

```
composer require digitaltunnel/moyasar
```

Then run the installer — it publishes the config and prints next steps:

```
php artisan moyasar:install            # add --migrations for the local mirror tables
php artisan migrate                    # only if you published migrations
```

Add your keys to `.env`:

```
MOYASAR_SECRET_KEY=sk_test_...
MOYASAR_PUBLISHABLE_KEY=pk_test_...
MOYASAR_WEBHOOK_SECRET=whsec_...
```

The webhook route registers automatically **outside** the `web` middleware group, so CSRF never applies — you don't need to touch `VerifyCsrfToken::$except`. Confirm it's live:

```
php artisan route:list --name=moyasar.webhook
# POST  webhooks/moyasar ... VerifyWebhookSignature
```

Configuration
-------------

[](#configuration)

Every option is environment-driven. Defaults shown.

Env varConfig keyDefaultPurpose`MOYASAR_SECRET_KEY``moyasar.secret_key`—Backend Basic-auth key. **Never expose.**`MOYASAR_PUBLISHABLE_KEY``moyasar.publishable_key`—Safe for the browser (tokenization, Apple Pay).`MOYASAR_BASE_URL``moyasar.base_url``https://api.moyasar.com/v1`Override only for a mock/proxy.`MOYASAR_TIMEOUT``moyasar.http.timeout``30`HTTP timeout (seconds).`MOYASAR_RETRY_TIMES``moyasar.http.retry.times``0`Transient-failure retries (`0` = off).`MOYASAR_RETRY_SLEEP_MS``moyasar.http.retry.sleep_ms``200`Delay between retries.`MOYASAR_WEBHOOK_ENABLED``moyasar.webhook.enabled``true`Auto-register the inbound route.`MOYASAR_WEBHOOK_PATH``moyasar.webhook.path``webhooks/moyasar`Inbound webhook path.`MOYASAR_WEBHOOK_NAME``moyasar.webhook.name``moyasar.webhook`Route name.`MOYASAR_WEBHOOK_SECRET``moyasar.webhook.secret`—Shared secret token to verify deliveries.`MOYASAR_WEBHOOK_QUEUE``moyasar.webhook.queue``false``false`=sync, `true`=default queue, or a connection name.`MOYASAR_PERSIST_PAYMENTS``moyasar.persistence.payments.enabled``true`Mirror payments to `moyasar_payments`.`MOYASAR_PERSIST_TOKENS``moyasar.persistence.tokens.enabled``true`Mirror tokens to `moyasar_tokens`.`MOYASAR_LOG_WEBHOOK_EVENTS``moyasar.persistence.webhook_events.enabled``true`Log + de-dup webhooks via `moyasar_webhook_events`.> **Note** All amounts are integers in the smallest currency unit (halalah). `100.00 SAR = 10000`. Use `->amountInMajorUnits(100.00)` or `Amount::toMinorUnits(100.00)` to avoid the classic off-by-100 bug.

---

End-to-end checkout (credit card + 3DS)
---------------------------------------

[](#end-to-end-checkout-credit-card--3ds)

The complete, **secure** flow — create, redirect to 3DS, then verify on return before fulfilling.

```
use DigitalTunnel\Moyasar\Facades\Moyasar;
use DigitalTunnel\Moyasar\Requests\CreatePaymentRequest;
use DigitalTunnel\Moyasar\Sources\CreditCardSource;

// 1) Create the payment and send the customer to the 3DS challenge.
public function checkout(Request $request, Order $order)
{
    $payment = Moyasar::payments()->create(
        CreatePaymentRequest::make()
            ->amount($order->total_halalah)                // integer halalah
            ->currency('SAR')
            ->givenId((string) Str::uuid())                // idempotency key
            ->callbackUrl(route('checkout.return'))
            ->description("Order #{$order->id}")
            ->metadata(['order_id' => (string) $order->id])
            ->source(
                CreditCardSource::make(
                    name: $request->string('name'),
                    number: $request->string('number'),
                    month: $request->integer('month'),
                    year: $request->integer('year'),
                    cvc: $request->string('cvc'),
                )->with3ds()                               // default; ->without3ds() to skip
            )
    );

    return redirect($payment->transactionUrl());
}
```

```
use DigitalTunnel\Moyasar\Exceptions\CallbackVerificationException;

// 2) Moyasar redirects back to callback_url with ?id=&status=&message=.
//    NEVER trust those query params — re-fetch and verify server-side.
public function return(Request $request)
{
    $order = Order::findOrFail(/* from your own state/session */);

    try {
        $payment = Moyasar::payments()->verifyCallback(
            id: $request->string('id'),
            expectedAmount: $order->total_halalah,
            expectedCurrency: 'SAR',
        );
    } catch (CallbackVerificationException $e) {
        report($e);                                        // $e->payment is attached
        return redirect()->route('checkout.failed');
    }

    $order->markPaid($payment->id);

    return redirect()->route('checkout.success');
}
```

> Prefer webhooks as your **source of truth** for fulfilment (see below). `verifyCallback()`gives the customer an instant, trustworthy result on return; the webhook guarantees you eventually reconcile even if the browser never comes back.

---

Payment methods
---------------

[](#payment-methods)

### Apple Pay

[](#apple-pay)

```
use DigitalTunnel\Moyasar\Sources\ApplePaySource;

// Merchant-validation handshake (from a backend proxy):
$session = Moyasar::applePay()->requestSession(
    validationUrl: $request->input('validationURL'),
    displayName: config('app.name'),
    domainName: $request->getHost(),
);
return response()->json($session->toArray());

// Charge the Apple Pay token:
Moyasar::payments()->create(
    CreatePaymentRequest::make()
        ->amount(10000)->givenId((string) Str::uuid())
        ->source(ApplePaySource::make($request->input('token')))
);
```

### Samsung Pay

[](#samsung-pay)

```
use DigitalTunnel\Moyasar\Sources\SamsungPaySource;

Moyasar::payments()->create(
    CreatePaymentRequest::make()->amount(10000)->givenId((string) Str::uuid())
        ->source(SamsungPaySource::make($samsungToken))
);
```

### STC Pay (with OTP follow-up)

[](#stc-pay-with-otp-follow-up)

```
use DigitalTunnel\Moyasar\Sources\StcPaySource;

$payment = Moyasar::payments()->create(
    CreatePaymentRequest::make()->amount(10000)->givenId((string) Str::uuid())
        ->source(StcPaySource::make(mobile: '0512345678'))
);

// After the user enters the OTP they received — returns a hydrated Payment:
$payment = Moyasar::payments()->verifyStcPayOtp($payment->transactionUrl(), $otp);

if ($payment->isPaid()) {
    // fulfilled
}
```

### Tokenization (card-on-file)

[](#tokenization-card-on-file)

```
use DigitalTunnel\Moyasar\Sources\TokenSource;

// Charge a previously-saved token (tokenize on the client with the publishable key).
Moyasar::payments()->create(
    CreatePaymentRequest::make()
        ->amount(10000)->callbackUrl(route('checkout.return'))
        ->source(TokenSource::make('token_abc')->cvc('123')->with3ds())
);

Moyasar::tokens()->find('token_abc');   // status is a typed TokenStatus enum
Moyasar::tokens()->delete('token_abc');
```

---

Invoices (hosted payment pages)
-------------------------------

[](#invoices-hosted-payment-pages)

```
use DigitalTunnel\Moyasar\Requests\CreateInvoiceRequest;

$invoice = Moyasar::invoices()->create(
    CreateInvoiceRequest::make()
        ->amountInMajorUnits(250.00)
        ->description('Annual subscription')
        ->callbackUrl(route('checkout.return'))
        ->expiresAt(now()->addDays(3))
        ->metadata(['plan' => 'pro'])
);

return redirect($invoice->paymentUrl());   // hosted Moyasar page

Moyasar::invoices()->find($invoice->id);
Moyasar::invoices()->list(['status' => 'paid']);
Moyasar::invoices()->bulkCreate([$requestA, $requestB]);
Moyasar::invoices()->cancel($invoice->id);
```

Lifecycle &amp; reconciliation
------------------------------

[](#lifecycle--reconciliation)

```
Moyasar::payments()->find('pay_...');
Moyasar::payments()->update('pay_...', metadata: ['tag' => 'vip']);
Moyasar::payments()->refund('pay_...', amount: 5000);   // partial; omit for full
Moyasar::payments()->capture('pay_...');                 // for manual-capture auths
Moyasar::payments()->void('pay_...');

// Page through results, or stream every match without managing pages:
$page = Moyasar::payments()->list(['status' => 'paid', 'metadata' => ['order_id' => '42']]);

foreach (Moyasar::payments()->each(['status' => 'paid']) as $payment) {
    // walks every page lazily until there are no more
}
```

---

Webhooks
--------

[](#webhooks)

Listen for the typed events anywhere you register listeners:

```
use DigitalTunnel\Moyasar\Events\PaymentPaid;

Event::listen(PaymentPaid::class, function (PaymentPaid $event) {
    // $event->payment is a Payment DTO; $event->webhook is the full WebhookPayload
    Order::findByMoyasarId($event->payment->id)?->markPaid();
});
```

Available events (each carries `$payment` + `$webhook`):

`PaymentPaid` · `PaymentFailed` · `PaymentAuthorized` · `PaymentCaptured` · `PaymentRefunded`· `PaymentVoided` · `PaymentAbandoned` · `PaymentVerified` · `WebhookReceived` (catch-all).

**Idempotent by design.** Moyasar retries non-2xx deliveries up to 6 times. With the `moyasar_webhook_events` table enabled, each event is stored by id and claimed *before*dispatch, so a retried (or forged) delivery never re-fires your listeners. Keep listeners idempotent, and for heavy work either queue your listeners (`ShouldQueue`) or offload the whole pipeline:

```
MOYASAR_WEBHOOK_QUEUE=redis      # process inbound webhooks on the "redis" connection
```

### Register your webhook

[](#register-your-webhook)

```
php artisan moyasar:webhook register          # uses route('moyasar.webhook') + your secret
php artisan moyasar:webhook list
php artisan moyasar:webhook delete wh_abc
```

…or from code: `Moyasar::webhooks()->create(url: ..., events: [...], sharedSecret: ...)`.

### Custom route

[](#custom-route)

Set `MOYASAR_WEBHOOK_ENABLED=false` and wire it yourself:

```
Route::post('my-path', \DigitalTunnel\Moyasar\Http\Controllers\WebhookController::class)
    ->middleware([\DigitalTunnel\Moyasar\Http\Middleware\VerifyWebhookSignature::class]);
```

---

Local persistence (opt-in)
--------------------------

[](#local-persistence-opt-in)

If you published the migrations, three Eloquent models mirror Moyasar state for reporting:

```
use DigitalTunnel\Moyasar\Models\MoyasarPayment;

MoyasarPayment::where('status', 'paid')->sum('amount');

// Associate a mirror row with your own model:
MoyasarPayment::upsertFromDto($event->payment)->payable()->associate($order)->save();
```

`MoyasarToken` and `MoyasarWebhookEvent` work the same way. Rows are written automatically on API calls and inbound webhooks unless you disable the relevant `persistence.*` flag.

---

Money helpers
-------------

[](#money-helpers)

```
use DigitalTunnel\Moyasar\Support\Amount;

Amount::toMinorUnits(100.00);   // 10000
Amount::toMajorUnits(10000);    // 100.0
Amount::format(10000, 'SAR');   // "100.00 SAR"

$payment->amountInMajorUnits(); // 100.0
```

Exception handling
------------------

[](#exception-handling)

```
use DigitalTunnel\Moyasar\Exceptions\InvalidRequestException;

try {
    Moyasar::payments()->create($request);
} catch (InvalidRequestException $e) {
    $e->fieldErrors();              // ['source.number' => ['is invalid'], ...]
    $e->firstErrorFor('source.cvc');
}
```

The full hierarchy:

```
MoyasarException                       (base — extends RuntimeException)
├── ApiException                       (any non-2xx — statusCode, errorType, responseBody)
│   ├── AuthenticationException        (401)
│   ├── AccountInactiveException       (403 + account_inactive_error)
│   ├── NotFoundException              (404)
│   ├── InvalidRequestException        (400/422 — + fieldErrors)
│   ├── RateLimitException             (429)
│   └── ServerException                (5xx)
├── ConnectionException                (network/timeout)
├── CallbackVerificationException      (verifyCallback mismatch — has ->payment)
└── InvalidWebhookSignatureException   (renders HTTP 403)

```

Testing
-------

[](#testing)

Use `MoyasarFake` to stub the API in your own feature tests:

```
use DigitalTunnel\Moyasar\Testing\MoyasarFake;

it('creates a pending payment', function () {
    MoyasarFake::fakePaymentCreated();

    $this->post('/checkout', [...])->assertRedirect();

    MoyasarFake::assertCreated();
});

it('surfaces validation errors', function () {
    MoyasarFake::fakeValidationError(['source.cvc' => ['is invalid']]);
    // ...
});
```

Helpers: `fakePaymentCreated()`, `fakeValidationError()`, `fakeAuthenticationError()`, `webhookFixture()`, `assertCreated()`, `assertRefunded()`, `assertSentCount()`, `assertNothingSent()`.

Running the package test suite
------------------------------

[](#running-the-package-test-suite)

```
composer install
composer test        # Pest
composer analyse     # PHPStan (level 6)
composer lint:test   # Pint
composer ci          # all of the above
```

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md). Security issues: [SECURITY.md](SECURITY.md).

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance89

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

57d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/66947877?v=4)[Hossam H Monir](/maintainers/HossamMonir)[@hossammonir](https://github.com/hossammonir)

---

Top Contributors

[![hossammonir](https://avatars.githubusercontent.com/u/66947877?v=4)](https://github.com/hossammonir "hossammonir (2 commits)")

---

Tags

laravelpaymentsApple Paypayment gatewayZATCAtokenizationMoyasar3dsSamsung Paystc-pay

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/digitaltunnel-moyasar/health.svg)

```
[![Health](https://phpackages.com/badges/digitaltunnel-moyasar/health.svg)](https://phpackages.com/packages/digitaltunnel-moyasar)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k30.2M151](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77922.3M186](/packages/laravel-mcp)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M133](/packages/roots-acorn)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
