vandarpay/cashier
A Laravel integration for the Vandar payment gateway, providing a fluent, Cashier-style API to manage payments, verify transactions, and handle common gateway workflows in PHP applications. It helps developers add Vandar payment support to Laravel projects with minimal setup and clean, readable code...
Install and publish
composer require vandarpay/cashier
php artisan vendor:publish --provider="Vandar\Cashier\CashierServiceProvider"
php artisan migrate
The package ships a config file (typically config/vandar.php or config/cashier.php) and a transactions/direct-debit migration. Publish both before running anything — the migration is what backs callback reconciliation.
Environment
VANDAR_API_KEY=your-api-key
VANDAR_BUSINESS=your-business-slug
VANDAR_CALLBACK_URL=https://app.test/payment/callback
The API key and business identifier are issued in the Vandar panel. Callback URLs are often allowlisted on the gateway side, so register the exact production and staging URLs there before testing — this is the single most common reason a "correct" integration silently fails to return.
Where to look first
config/vandar.php — endpoint base URLs, sandbox flag, amount unit, callback path.src/ — this is an 11-star package; the source is short and is the real documentation. Read the payment/token classes end to end before trusting any blog post.README.md examples — treat the method names there as authoritative, not the ones you remember from Cashier/Stripe.First use case: one-off IPG payment
The typical day-one flow: user clicks "pay", you create a payment record, redirect to the gateway, and the gateway POSTs back to your callback. In rough outline:
// Initiate
$payment = $user->checkout($amountInRial) // confirm exact signature in src/
->withCallback(route('payment.callback'))
->create();
return redirect($payment->redirectUrl());
// Callback
$result = Cashier::verify($request->token);
if ($result->isSuccessful()) {
// mark order paid, idempotently
}
Get the redirect-and-verify round trip working with a 1,000 Rial payment in sandbox before touching direct debit. Verification is the part that actually matters — a redirect alone proves nothing.
Payment initiation behind an action/service class. Keep gateway calls out of controllers. A CreatePaymentAction that persists a transaction row before calling the gateway gives you a local record keyed by your own reference even if the HTTP call times out — which is exactly the case you'll need to debug later.
Callback controller, outside auth. Put the callback route in routes/web.php with CSRF excluded for that URI, and do not assume a session. Vandar's callback arrives as a browser POST from the gateway; the user's session cookie usually survives, but you should not depend on it. Resolve the order from the token/transaction row you stored, not from auth()->user().
Idempotent fulfilment. The callback can be replayed (user refreshes, gateway retries). Wrap fulfilment in a DB transaction with a status guard:
DB::transaction(function () use ($token) {
$tx = Transaction::where('token', $token)->lockForUpdate()->firstOrFail();
if ($tx->status === 'paid') return; // already handled
Cashier::verify($token); // server-to-server check
$tx->markPaid();
Order::find($tx->order_id)->fulfil();
});
Never trust the callback query string. The POST parameters tell you which payment to look up; the verify call tells you whether it succeeded. Always verify server-side before shipping goods.
Direct debit as a separate workflow. IPG is a one-shot redirect/verify. Direct debit is a token-based mandate: you request a token/mandate for the customer, they approve it once, and then you can initiate withdrawals against that token without them being present. Model it as its own table/entity (direct_debit_tokens or whatever the migration names it) linked to your User. There is no scheduler in the package — you own the cron/queue job that decides when to withdraw. Treat it as "stored payment authority", not "subscription billing".
Wrap it, don't scatter it. Because the package is small and likely to change shape, put it behind your own interface (PaymentGateway) with request(), verify(), withdraw(). Swapping gateways later then means one adapter, and you can fake the gateway in tests without mocking the package's internals.
Testing. Point the config's base URL at a local stub (or use Http::fake()), and assert on your own transaction rows rather than the package's return objects. Snapshot one real sandbox response body into a fixture — the field names (status, transId, amount) are what your assertions should key off.
Amount units are the classic bug. Vandar's IPG API works in Rial; Iranian businesses and users think in Toman. A 100,000 Toman cart is 1,000,000 Rial. Decide on one unit at your domain boundary (recommend: store Rial, format Toman in views) and convert in exactly one place. Mismatched units produce payments that succeed with the wrong amount — worse than failures, because they're hard to notice.
Config caching. After changing VANDAR_* values, run php artisan config:clear. In production with config:cache, env edits do nothing until you re-cache. This alone accounts for a large share of "it works locally" tickets.
Verify responses are not exceptions. Gateway verify calls usually return a payload with a status flag rather than throwing on a declined payment. Check the flag; don't rely on try/catch to detect failure. Conversely, do wrap the call in try/catch for transport errors and log the raw response body — you will need it.
Log the full exchange. Request payload (minus the API key), HTTP status, and response body, tagged with your transaction reference. When a customer says "it took my money", this log is the only thing that resolves it quickly.
Callback URL must be absolute and public. route() inside a queued job can produce the wrong host. Hardcode the callback URL in config or set APP_URL correctly everywhere. Also confirm the URL is registered in the Vandar panel, including the port if you're tunnelling for local dev.
Sandbox flag. If the config has a sandbox/development switch, verify it's off in production and that the base URL actually changed — a sandbox flag that only alters redirect URLs but not API hosts gives you fake successes.
Don't build recurring billing on top of IPG. Users must be redirected each time. If you need unattended charging, that's the direct-debit path, and it requires the customer's explicit mandate — plan the UX for mandate capture and for mandate revocation.
Small-package risk. With 11 stars, expect thin docs, few maintainers, and possible breaking changes in patch releases. Pin an exact version ("vandarpay/cashier": "1.x.y"), commit the lockfile, and be prepared to fork. The upside: the surface area is small enough to read in an afternoon, so you're never truly blocked.
Extension points. If the package exposes events (payment initiated/verified), listen rather than editing the package — it makes upgrades survivable. If it doesn't, your own action classes are the extension point.
Currency formatting. Store and compute in integers; format with Latin digits for API payloads and Persian digits only in the UI. Mixing Persian numerals into an amount field sent to the gateway will fail or, worse, be parsed as a different number.
How can I help you explore Laravel packages today?