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

Zarinpal Laravel Package

pishran/zarinpal

Laravel package for integrating the Zarinpal payment gateway. Send payment requests, redirect users to Zarinpal, and verify transactions with a clean, simple API, plus sandbox support for testing. A lightweight wrapper that streamlines online payments in Iranian Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the config:

composer require pishran/zarinpal
php artisan vendor:publish --provider="Pishran\Zarinpal\ZarinpalServiceProvider"

Add the merchant ID to .env:

ZARINPAL_MERCHANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

The package auto-registers its service provider and Zarinpal facade on modern Laravel. Look first at config/zarinpal.php, then the package source under vendor/pishran/zarinpal/src if the README and your installed version disagree.

The first use case is a classic redirect flow: create a payment request, redirect the user to Zarinpal, then verify the authority on the callback.

use Pishran\Zarinpal\Zarinpal;

public function pay()
{
    $payment = Zarinpal::request(
        amount: 100000, // Rial
        callbackUrl: route('payment.callback'),
        description: 'Order #123'
    )->pay();

    return redirect()->away($payment['url']);
}

public function callback(Request $request)
{
    abort_unless($request->query('Status') === 'OK', 402);

    $verify = Zarinpal::verify(
        amount: 100000,
        authority: $request->query('Authority')
    );

    if (($verify['code'] ?? null) === 100) {
        $refId = $verify['ref_id'];
        // Mark the order as paid using $refId.
    }

    return redirect()->route('orders.show', $order);
}

For local development use the sandbox entry point rather than the production gateway:

$payment = Zarinpal::sandbox()
    ->request($amount, $callbackUrl, $description)
    ->pay();

Implementation Patterns

Persist before redirecting. Never rely on the callback alone to reconstruct the order. Store a payment/order row with user_id, amount, authority, status, ref_id, and the raw gateway response before sending the user away.

$payment = Zarinpal::request($order->amount_rial, route('payment.callback'), "Order #{$order->id}")
    ->withMobile($order->user->mobile)
    ->withEmail($order->user->email)
    ->pay();

$order->payments()->create([
    'authority' => $payment['authority'],
    'amount'    => $order->amount_rial,
    'status'    => 'pending',
]);

Look up by authority, not session. Users often return in a different browser state, after session expiry, or with the tab restored. The Authority query parameter is the reliable correlation key.

$payment = Payment::where('authority', $request->query('Authority'))->firstOrFail();

Verify server-side and idempotently. The callback is a hint, not proof. Always call verify() and treat code 100 as success. Code 101 means the authority was already verified; treat that as success too if your stored payment is still pending.

$verify = Zarinpal::verify($payment->amount, $payment->authority);

if (in_array($verify['code'] ?? null, [100, 101], true)) {
    $payment->update([
        'status' => 'paid',
        'ref_id' => $verify['ref_id'] ?? $payment->ref_id,
    ]);
}

Keep gateway code behind a small service. Controllers should not know Zarinpal response shapes. A PaymentGateway interface with a ZarinpalGateway implementation makes testing and future gateway changes straightforward.

Use metadata for reconciliation. Send your internal order ID through the package’s additional-data/metadata option when available. This makes Zarinpal dashboard records and support requests much easier to match.

Zarinpal::request($amount, $callbackUrl, $description)
    ->withAdditionalData(['order_id' => $order->id])
    ->pay();

Sandbox in tests, never production. Prefer faking the outbound HTTP layer if the package uses Laravel’s HTTP client. If not, wrap the facade in your own interface and fake that in tests. Do not let CI hit the real gateway.

Handle cancellation and failure as first-class states. Status can be something other than OK, and Authority can be missing. Update the payment row to cancelled or failed and show a useful message rather than a generic 500.

Gotchas and Tips

The amount unit is the most common bug. Zarinpal expects Rial by default. Ten thousand Toman is 100,000 Rial. Decide once whether you store Toman or Rial, name the column accordingly (amount_rial), and convert only at the gateway boundary. If you use withCurrency('IRT'), keep that currency consistent between request and verify.

Status=OK is not payment confirmation. It only means the user completed the redirect. Always verify with the same amount and authority you sent.

Pass the exact original amount to verify(). A mismatch produces a verification error even when the user actually paid. Read the amount from your persisted payment record, not from the request.

Authority expires quickly. Treat the authority as short-lived (roughly 15 minutes). Verify immediately in the callback and have a scheduled reconciliation job for payments that remain pending.

Do not trust the callback URL from the request. Build it with route() or a configured absolute URL. It must be publicly reachable in production; localhost callbacks will not work with the real gateway. Use sandbox mode or a tunnel for local testing.

Check your merchant/domain configuration. A merchant ID tied to another domain, an inactive merchant, or an IP-restricted merchant produces gateway errors that look like code bugs. Compare the raw response from Zarinpal before debugging your controller.

Clear config cache after changing .env. php artisan config:clear (or config:cache in deployment) is a frequent source of “it still uses the old merchant ID” confusion.

Callback routes are GET. CSRF is not the issue for the standard redirect callback, but if you add any POST endpoint around payments, exclude it correctly in VerifyCsrfToken or Laravel 11’s bootstrap/app.php.

Log the raw gateway response. When pay() or verify() returns an unexpected shape, log the full response and the authority. Do not log card data or full user PII.

Guard against duplicate callbacks and double submits. Disable the pay button client-side, but also make the callback idempotent. Use a transaction and a paid check before fulfilling the order.

Wrap network calls. Gateway timeouts and HTML error pages happen. Catch connection exceptions around pay() and verify(), log them, and show a retry path instead of a stack trace. Consider reducing the HTTP timeout so users are not stuck waiting.

Do not retry payment requests blindly. Retrying a request() call can create multiple authorities for one order. If you must retry, do it deliberately and keep the latest authority on the payment record.

Refunds are outside this package’s happy path. The package is focused on request/verify. For refunds, use Zarinpal’s dashboard or the underlying API directly, and record refund state in your own database.

Facade alias for older Laravel. On older versions you may need to register Pishran\Zarinpal\ZarinpalServiceProvider and the Zarinpal alias manually in config/app.php. On current Laravel, package discovery handles it.

Extension points: publish and edit config/zarinpal.php, swap the facade binding in a service provider for your own wrapper, and use the sandbox entry point in non-production environments. Keep the merchant ID in .env, never in JavaScript or committed config.

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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table