larevio / persian-providers
Laravel integrations for Iranian payment, SMS, inquiry, and exchange providers
Requires
- php: ^8.2
- laravel/framework: ^12.0 || ^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.4
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^3.0 || ^4.0
- pestphp/pest-plugin-arch: ^3.0 || ^4.0
- pestphp/pest-plugin-laravel: ^3.0 || ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
Suggests
- ext-intl: Required for Sabanovin scheduling (Jalali date formatting).
Provides
None
Conflicts
None
Replaces
None
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 |
✅ | ✅ | ✅ | Docs | |
zibal |
✅ | ✅ | — | Docs | |
idpay |
✅ | ✅ | — | Docs (archived) | |
sep |
✅ | ✅ | ✅ | Docs v3.6 | |
sepehr |
✅ | ✅ | ✅ | Request from provider | |
nextpay |
✅ | ✅ | ✅ | Docs | |
aqayepardakht |
✅ | ✅ | — | Docs | |
panapal |
✅ | ✅ | — | Docs (archived) | |
payping |
✅ | ✅ | ✅ | OpenAPI | |
paystar |
✅ | ✅ | — | Docs | |
vandar |
✅ | ✅ | — | Docs | |
pasargad |
✅ | ✅ | ✅ | Docs | |
sadad |
✅ | ✅ | — | Sample code | |
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 reversalVerify: 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 |
✅ | ✅ | ✅ | ✅ | ✅ | Docs | |
smsir |
✅ | ✅ | ✅ | ✅ | ✅ | Docs | |
ghasedak |
✅ | ✅ | ✅ | ✅ | ✅ | Docs | |
sabanovin |
✅ | ✅ | ✅ | ✅ | ✅ | Docs | |
melipayamak |
✅ | ✅ | — | — | ✅ | REST client | |
farapayamak |
✅ | ✅ | — | — | ✅ | REST client | |
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
lineunless 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,token10andtoken20for Kavenegar. - Use
supportsBatch(),supportsSchedule()andsupportsBalance()to check what a provider can do. header($text)andfooter($text)add a line before or after the message.footer()defaults to the opt-out textلغو 11.- Sabanovin scheduling needs the
intlextension. - OneSMS support follows its Postman collection v1.7. Numbers starting with
09are sent as989....
Inquiries
| Provider | Key | Mobile | IBAN | Card | Owner name | Card lookup | IBAN lookup | Identity | Postal code | Docs |
|---|---|---|---|---|---|---|---|---|---|---|
zohal |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | OpenAPI | |
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 |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Docs | |
bitpin |
✅ | ✅ | — | — | — | — | Docs | |
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()andwithdraw()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(), thenconfirmWithdrawal()with the code sent by SMS. Pass a TOTP code asextra: ['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.