Looking to hire Laravel developers? Try LaraJobs
This package is not available.

laravel-paystation maintained by xenon

Description
Laravel integration for the Paystation (Bangladesh) payment gateway - config, facade, redirect handling and payment verification.
Author
Last update
2026/09/03 22:30 (dev-master)
License
Links
Downloads
0

Comments
comments powered by Disqus

Laravel Paystation

Laravel integration for the Paystation payment gateway (Bangladesh).

Companion to the framework-agnostic xenon/paystation SDK. Same endpoints and same field contract, but built for Laravel: config file, facade, auto-discovery, a real RedirectResponse, local validation, and a fully fakeable HTTP layer.

Why this package instead of the plain SDK

xenon/paystation xenon/laravel-paystation
Redirect to checkout echo "<script>…</script>"; exit; returns RedirectResponse
Credentials array passed at every call site config/paystation.php + env
Sandbox / live not switchable environment config key
Bad payload generic gateway rejection per-field errors before any request
Testing constructs its own Guzzle client Http::fake()
Guzzle version pinned `^6.3 ^7.2`
Missing transaction ambiguous found() / successful()

The exit in the SDK is the important one: it ends the PHP process mid-request, so the session is never written, after middleware never runs, and nothing downstream of the call can observe the payment.

Requirements

  • PHP 8.2+
  • Laravel 10, 11, 12 or 13

Installation

composer require xenon/laravel-paystation

The service provider and the Paystation facade are auto-discovered. Publishing the config is optional (defaults are merged in), but recommended:

php artisan vendor:publish --tag=paystation-config

Then set your credentials:

PAYSTATION_ENV=sandbox
PAYSTATION_MERCHANT_ID=your-merchant-id
PAYSTATION_PASSWORD=your-password
PAYSTATION_CALLBACK_URL=https://yourdomain.com/paystation/callback

Taking a payment

use Xenon\LaravelPaystation\Facades\Paystation;

class CheckoutController
{
    public function pay(Order $order)
    {
        return Paystation::pay([
            'invoice_number' => $order->invoice_number,
            'payment_amount' => $order->total,
            'reference'      => (string) $order->id,
            'cust_name'      => $order->customer_name,
            'cust_phone'     => $order->customer_phone,
            'cust_email'     => $order->customer_email,
            'cust_address'   => $order->customer_address,
        ]);
    }
}

currency and callback_url come from config, so you only pass them to override. pay() returns a redirect straight to the hosted checkout page.

When you need the URL instead of a redirect

Useful for APIs, or when you want to persist the invoice number before handing the customer over:

$session = Paystation::createPayment($payload);

$order->update(['invoice_number' => $session->invoiceNumber]);

return response()->json(['checkout_url' => $session->paymentUrl]);

If you omit invoice_number, one is generated for you and returned on the session.

Verifying a payment

Paystation reports an unknown transaction with HTTP 200 and status: failed, so a missing transaction is a normal result here rather than an exception:

$result = Paystation::verify($invoiceNumber, $transactionId);

if (! $result->found()) {
    return back()->withErrors($result->message());   // gateway has no such transaction
}

if (! $result->successful()) {
    return back()->withErrors('Payment did not succeed: '.$result->transactionStatus());
}

$order->markPaid(
    transactionId: $result->transactionId(),
    method: $result->paymentMethod(),
    amount: $result->amount(),
);

found() means the gateway located the transaction. successful() additionally means the payment itself went through — a located transaction can still carry a failed or pending trx_status, so check the one you actually mean.

Also available: statusCode(), status(), message(), invoiceNumber(), payerMobile(), reference(), and toArray() for the raw payload.

Validation

The payload is validated locally before anything is sent. A bad payload throws InvalidPaymentParameters carrying per-field errors, ready to hand back to a form:

use Xenon\LaravelPaystation\Exceptions\InvalidPaymentParameters;

try {
    return Paystation::pay($payload);
} catch (InvalidPaymentParameters $e) {
    return back()->withErrors($e->errors());
}

Required: invoice_number, currency, payment_amount, reference, cust_name, cust_phone, cust_email, cust_address, callback_url. Optional passthrough: checkout_items, opt_aopt_d.

For a fluent build-up, use the payload object:

use Xenon\LaravelPaystation\Data\PaymentPayload;

$payload = PaymentPayload::make()
    ->set('payment_amount', $order->total)
    ->set('reference', (string) $order->id)
    ->set('checkout_items', $order->summary())
    ->withGeneratedInvoiceNumber('shop-');

return Paystation::pay($payload);

Exceptions

All of them extend PaystationException, so you can catch that one class for anything gateway related.

Exception Raised when
ConfigurationException credentials missing, or an environment with no configured endpoint
InvalidPaymentParameters payload rejected locally; carries errors()
TokenRequestException credentials could not be exchanged for a token
PaymentCreationException gateway declined to issue a payment URL; carries context()
PaystationException transport failure or non-JSON / error HTTP response

Multiple merchants and environments

The manager is immutable — overrides return a new instance and never mutate the shared singleton:

Paystation::environment('live')->pay($payload);

Paystation::withConfig([
    'merchant_id' => config('services.paystation.second_merchant'),
    'password'    => config('services.paystation.second_password'),
])->pay($payload);

Token caching

Each call needs a token, which normally costs an extra grant-token round trip. Caching is off by default because Paystation does not document how long a token stays valid — that matches the plain SDK behaviour. Once you have confirmed the lifetime, turn it on:

PAYSTATION_CACHE_TOKEN=true
PAYSTATION_TOKEN_TTL=1800

Cache keys are scoped per merchant and environment, so switching either never reuses the wrong token. Call Paystation::tokens()->forget() to drop a cached token.

Testing

No network access needed — the package uses the Laravel HTTP client, so fake it:

use Illuminate\Support\Facades\Http;

Http::fake([
    '*/grant-token'   => Http::response(['token' => 'test-token']),
    '*/create-payment' => Http::response([
        'status_code'  => 200,
        'status'       => 'success',
        'payment_url'  => 'https://pay.paystation.com.bd/checkout/test',
    ]),
]);

$response = Paystation::pay($payload);

$this->assertSame('https://pay.paystation.com.bd/checkout/test', $response->getTargetUrl());

Run this package's own suite with:

composer install
vendor/bin/phpunit

Configuration reference

Key Env Default
environment PAYSTATION_ENV sandbox
merchant_id PAYSTATION_MERCHANT_ID
password PAYSTATION_PASSWORD
endpoints.sandbox PAYSTATION_SANDBOX_URL https://api.paystation.com.bd
endpoints.live PAYSTATION_LIVE_URL https://api.paystation.com.bd
defaults.currency PAYSTATION_CURRENCY BDT
defaults.callback_url PAYSTATION_CALLBACK_URL
http.timeout PAYSTATION_TIMEOUT 10
http.connect_timeout PAYSTATION_CONNECT_TIMEOUT 5
http.retry.times PAYSTATION_RETRY_TIMES 1
http.retry.sleep PAYSTATION_RETRY_SLEEP 200
token.cache PAYSTATION_CACHE_TOKEN false
token.store PAYSTATION_CACHE_STORE default store
token.ttl PAYSTATION_TOKEN_TTL 1800
logging.enabled PAYSTATION_LOGGING false
logging.channel PAYSTATION_LOG_CHANNEL default channel

Paystation currently serves sandbox and live traffic from the same host and separates them by credentials, which is why both endpoints default to the same URL. Override them if Paystation tells you otherwise.

Logging records the endpoint and the gateway status only — credentials, tokens and customer details are never written.

License

MIT. See LICENSE.