Search by

larevio / persian-providers

larevio

Laravel integrations for Iranian payment, SMS, inquiry, and exchange providers

Package info

github.com/larevio/persian-providers

pkg:composer/larevio/persian-providers

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.0 2026-11-07 17:09 UTC

This package is auto-updated.

Last update: 2026-11-07 17:22:20 UTC


README

Iranian payment gateways, SMS services, identity inquiries and crypto exchanges for Laravel.

Installation · Amounts · Payments · SMS · Inquiries · Exchanges · Errors

Persian Providers supports 25 Iranian services through four helpers: payment(), sms(), inquiry() and exchange().

Installation

Requires PHP 8.2+ with Laravel 12, or PHP 8.3+ with Laravel 13.

composer require larevio/persian-providers
php artisan vendor:publish --tag="persianproviders-config"

The service provider is discovered automatically. Credentials are read from environment variables, listed in the configuration file:

PERSIAN_PROVIDERS_PAYMENT=zarinpal
ZARINPAL_TOKEN=your-merchant-id

PERSIAN_PROVIDERS_SMS=smsir
SMSIR_TOKEN=your-api-key
SMSIR_LINE=your-sender-line

PERSIAN_PROVIDERS_INQUIRY=zohal
ZOHAL_TOKEN=your-token

PERSIAN_PROVIDERS_EXCHANGE=nobitex
NOBITEX_TOKEN=your-token

You can also pass settings for a single call. They are merged over the configured values:

payment('zarinpal', ['token' => $merchantId]);
sms('smsir', ['token' => $apiKey, 'line' => $senderLine]);
inquiry('zohal', ['token' => $token]);
exchange('nobitex', ['token' => $token]);

Common settings

Setting Default Scope
timeout 15 seconds (PERSIAN_PROVIDERS_TIMEOUT) Global or per provider
connect_timeout 5 seconds (PERSIAN_PROVIDERS_CONNECT_TIMEOUT) Global or per provider
proxy PERSIAN_PROVIDERS_PROXY Global or per provider. Set false to turn it off for one provider
sandbox false Zarinpal, IDPay and AqayePardakht

Amounts

Payment amounts are in rials. The fluent builder takes the unit along with the amount:

use Larevio\PersianProviders\Enums\Currency;

payment()->amount(150_000, Currency::IRT);   // 150,000 tomans = 1,500,000 rials
payment()->amount(1_500_000, Currency::IRR); // same amount

Gateways that work in tomans round down to whole tomans, so 12,505 rials is charged as 1,250 tomans.

Unit sent to the provider Gateways
Rials Zarinpal (with currency=IRR), Zibal, IDPay, SEP, Sepehr, PayStar, Vandar, Pasargad, Sadad, Damavand
Tomans NextPay (with currency=IRT), AqayePardakht, PanaPal, PayPing

Amounts in VerifiedPayment and ReversedPayment are in rials.

Payments

Gateway Key Create Verify Reverse Docs
Zarinpal zarinpal ✅ ✅ ✅ Docs
Zibal zibal ✅ ✅ — Docs
IDPay idpay ✅ ✅ — Docs (archived)
SEP (Saman) sep ✅ ✅ ✅ Docs v3.6
Sepehr sepehr ✅ ✅ ✅ Request from provider
NextPay nextpay ✅ ✅ ✅ Docs
AqayePardakht aqayepardakht ✅ ✅ — Docs
PanaPal panapal ✅ ✅ — Docs (archived)
PayPing payping ✅ ✅ ✅ OpenAPI
PayStar paystar ✅ ✅ — Docs
Vandar vandar ✅ ✅ — Docs
Pasargad pasargad ✅ ✅ ✅ Docs
sadad ✅ ✅ — Sample code
Damavand damavand ✅ ✅ ✅ Docs

1. Create the payment and redirect

use Larevio\PersianProviders\Enums\Currency;

$pending = payment('zibal')
    ->amount($order->total_tomans, Currency::IRT)
    ->order($order->id)
    ->description("Order #{$order->id}")
    ->mobile($customer->mobile)
    ->callback(route('payment.callback'))
    ->create();

// Save these before redirecting. Verification uses your stored values, not the callback's.
$order->update(['payment_id' => $pending->id, 'gateway' => 'zibal']);

$pending->redirect(); // Auto-submitting form for POST gateways, otherwise a 302.

To build the redirect yourself, for example in an SPA, $pending->toArray() returns the method, URL and form fields.

2. Verify on the callback

$verified = payment($order->gateway)
    ->id($order->payment_id)
    ->amount($order->total_tomans, Currency::IRT)
    ->order($order->id)
    ->extra($request->all())
    ->verify();

if ($verified === false) {
    // The callback says the payment failed or was cancelled. No request was sent.
}

DB::transaction(function () use ($order, $verified) {
    // Fulfil the order once. Verifying again returns $verified->alreadyVerified === true.
});

Pass the callback request to extra(). Callback data can't change the amount, order or credentials used for verification.

Gateway-specific requirements
Gateway Order ID extra() Notes
IDPay Create, verify — —
SEP Create, verify Verify: RefNum Reject a RefNum you have already used. Set use_get_method for GET redirects and use_neo_ipg for Neo-PG terminals
Sepehr Create, verify Verify and reverse: digitalreceipt Reject a digitalreceipt you have already used. Reversal must be enabled by Sepehr for your terminal
NextPay Create — —
PayStar Create Verify: card_number, tracking_code —
Pasargad Create, verify Create: Jalali invoiceDate Can be reversed after verify() until your contract's window closes. To settle it right away, call PaymentProvider::make('pasargad')->confirm($id, $rials, $orderId)
Sadad Create, verify — —
Damavand Create, verify Verify: Amount —
PayPing — Create: isReversible => true to allow reversal
Verify: paymentRefId from the callback's JSON data field
If verify throws ProviderException with HTTP 202 or 502, call it again

Reversal

if ($payment->supportsReverse()) {
    $reversed = $payment->reverse(); // Reverses the full amount.
}

Zarinpal, PayPing and Sepehr allow reversal for 30 minutes and NextPay for 20. SEP, Pasargad and Damavand windows depend on the provider or your contract. Zarinpal also requires your server IP to be registered.

SMS

Provider Key Send / Bulk Pattern Batch Schedule Balance Docs
Kavenegar kavenegar ✅ ✅ ✅ ✅ ✅ Docs
SMS.ir smsir ✅ ✅ ✅ ✅ ✅ Docs
Ghasedak ghasedak ✅ ✅ ✅ ✅ ✅ Docs
Sabanovin sabanovin ✅ ✅ ✅ ✅ ✅ Docs
MeliPayamak melipayamak ✅ ✅ — — ✅ REST client
FaraPayamak farapayamak ✅ ✅ — — ✅ REST client
OneSMS onesms ✅ ✅ — — ✅ No public docs
sms('smsir')->to('09120000000')->message('Your order is ready.')->send();

sms('kavenegar')
    ->to('09120000000')
    ->templateId('verify')
    ->sendPattern(['token' => '1234']);

$submission = sms('ghasedak')->sendBatch([
    ['phone' => '09120000000', 'text' => 'Your order is ready.'],
    ['phone' => '09120000001', 'text' => 'Your appointment is tomorrow.'],
]);

$scheduled = sms('kavenegar')
    ->to(['09120000000', '09120000001'])
    ->message('Appointment reminder')
    ->schedule(now()->addHour());

$balance = sms('smsir')->balance();
  • Messages go out from the configured line unless you pass another one. SMS.ir, Sabanovin, MeliPayamak, FaraPayamak and OneSMS need a line.
  • Pattern variables are named for Sabanovin, SMS.ir and Ghasedak, positional for MeliPayamak, FaraPayamak and OneSMS, and token, token2, token3, token10 and token20 for Kavenegar.
  • Use supportsBatch(), supportsSchedule() and supportsBalance() to check what a provider can do.
  • header($text) and footer($text) add a line before or after the message. footer() defaults to the opt-out text لغو 11.
  • Sabanovin scheduling needs the intl extension.
  • OneSMS support follows its Postman collection v1.7. Numbers starting with 09 are sent as 989....

Inquiries

Provider Key Mobile IBAN Card Owner name Card lookup IBAN lookup Identity Postal code Docs
Zohal zohal ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ OpenAPI
Jibit jibit ✅ ✅ — — — ✅ — — OpenAPI
Column Methods
Mobile matchMobile($mobile, $nationalCode)
IBAN matchIban($iban, $nationalCode, $birthday)
Card matchCard($card, $nationalCode, $birthday)
Owner name matchCardOwner($card, $name), matchIbanOwner($iban, $name)
Card lookup cardToIban($card), cardToAccount($card), cardOwnerInquiry($card)
IBAN lookup ibanInquiry($iban)
Identity nationalIdentityInquiry($nationalCode, $birthday)
Postal code postalCodeInquiry($postalCode)
$matched = inquiry('zohal')->matchMobile($mobile, $nationalCode);
$account = inquiry('zohal')->ibanInquiry($iban);

The match methods return false when the details don't match. If the provider can't answer, for example because the card is unknown, ProviderException is thrown with the provider's code, such as CARD_NOT_FOUND.

BankAccount::bank() and VerifiedPayment::bank() return a Bank enum, detected from the IBAN or card number. It has fa() for the Persian name and color() for a brand color. Bank icons are published with php artisan vendor:publish --tag="persianproviders-assets" to public/vendor/persianproviders/icons/{bank}.png, where {bank} is the lowercase case name, for example mellat.png.

Exchanges

Exchange Key Rates Deposit address Profile Orders Crypto withdrawal Rial withdrawal Docs
Nobitex nobitex ✅ ✅ ✅ ✅ ✅ ✅ Docs
Bitpin bitpin ✅ ✅ — — — — Docs
Wallex wallex ✅ ✅ ✅ ✅ — ✅ Docs
Column Methods
Rates getRate($from, $to)
Deposit address getWalletAddress($currency, $network), getNetworks($currency) (Bitpin)
Profile getProfile(), getBankAccounts()
Orders order($side, $from, $to, $amount, $price)
Crypto withdrawal withdraw(...), confirmWithdrawal($id, $otp)
Rial withdrawal withdrawFiat($accountId, $rials)
use Larevio\PersianProviders\Enums\Currency;

$rate = exchange('nobitex')->getRate(Currency::BTC, Currency::IRT);
$rate->last;     // "21513079017.5" tomans per BTC
$rate->bestSell; // lowest ask
$rate->bestBuy;  // highest bid
  • Rates are decimal strings, not floats.
  • order() and withdraw() take decimal strings or integers in the exchange's market units.
  • withdrawFiat() takes rials.
  • Bitpin and Wallex need the deposit network.
  • A Nobitex crypto withdrawal takes two calls: withdraw(), then confirmWithdrawal() with the code sent by SMS. Pass a TOTP code as extra: ['totp' => ...].

Errors

The package's exceptions implement Larevio\PersianProviders\Exceptions\PersianProvidersException. It also throws the standard InvalidArgumentException for bad input and BadMethodCallException for operations a provider doesn't support.

Exception Meaning Retry?
ConfigurationException Unknown provider, or a missing or invalid credential No. Fix the configuration
InvalidArgumentException Your input was rejected before sending No
TransportException (TimeoutException) No HTTP response was received Only after checking the outcome. The provider may have processed the request
ProviderException (AuthenticationException for HTTP 401/403) The provider rejected the request. $provider, $providerCode and $httpStatus are set Depends on the code
InvalidResponseException The response was malformed or missing a required field Only after checking the outcome
PaymentVerificationException The provider confirmed a payment whose amount, order, reference or terminal doesn't match yours No. Don't fulfil the order
use Larevio\PersianProviders\Exceptions\PaymentVerificationException;
use Larevio\PersianProviders\Exceptions\ProviderException;
use Larevio\PersianProviders\Exceptions\TransportException;

try {
    $verified = $payment->verify();
} catch (PaymentVerificationException $e) {
    report($e);                 // Mismatch: investigate, and reverse the payment if needed.
} catch (TransportException $e) {
    // Outcome unknown. Call verify again; if it went through, alreadyVerified is true.
} catch (ProviderException $e) {
    $e->providerCode;           // e.g. -51 from Zarinpal
}

Security

  • Verify each payment on your server with the amount and order ID you stored. Don't trust callback data.
  • Store the provider reference and check it before fulfilling, so an order can't be fulfilled twice.
  • After a TransportException, check the outcome before retrying a payment, withdrawal or SMS.
  • To report a vulnerability, follow SECURITY.md.

Testing

Fake provider requests with Laravel's HTTP client:

Http::fake(['gateway.zibal.ir/*' => Http::response(['result' => 100, 'trackId' => 123])]);

Contributing

See CONTRIBUTING.md.

License

MIT. Provider names and logos belong to their owners.