Home / Docs / PayStation Stable
PayStation
Status: stable · Driver: paystation · Refunds: no API (refund from the merchant panel) · Sync: yes
PayStation (Service Hub Ltd) is a hosted checkout covering cards (Visa, Mastercard) and the bKash, Nagad, Rocket and Upay wallets.
"Stable-beta" means the request and response shapes below were checked against PayStation's documentation and its public sandbox, but not against a live merchant account. See Not yet verified.
Setup
PAYSTATION_MERCHANT_ID=104-1234567890
PAYSTATION_PASSWORD=your-store-password
PAYSTATION_SANDBOX=true
// config/bdpaygate.php → gateways
'paystation' => [
'driver' => 'paystation',
'merchant_id' => env('PAYSTATION_MERCHANT_ID'),
'password' => env('PAYSTATION_PASSWORD'),
'sandbox' => env('PAYSTATION_SANDBOX', true),
// 'pay_with_charge' => true, // sent as pay_with_charge=1, see below
],
PayStation issues separate merchant IDs for sandbox and live. Register at https://merchant.paystation.com.bd/registration; the docs are at https://paystation.com.bd/documentation.
| Environment | API host |
|---|---|
| Sandbox | https://sandbox.paystation.com.bd |
| Live | https://api.paystation.com.bd |
How a payment is verified
charge()callsPOST /initiate-payment(form fields) withmerchantId,password, this attempt'sgateway_referenceasinvoice_number, the amount aspayment_amount(for example"150.00"), and your own reference inreference. The package's payment id goes inopt_a.- PayStation answers with
status_code: "200"and apayment_url. Any otherstatus_codeis an error, even though the HTTP status is 200 (for example1008"Duplicate invoice number.",1001"Invalid Credential."). The package throwsGatewayExceptionwith PayStation's message and code. - The customer comes back to
/bdpaygate/paystation/callback?status=Successful|Failed|Canceled&invoice_number=…&trx_id=…. These query parameters are unsigned, so the package ignores everything exceptinvoice_number. - The package asks
POST /transaction-status(headermerchantId, bodyinvoice_number) and acts ondata.trx_status:
trx_status (any casing) |
Result |
|---|---|
success |
Completed, once invoice_number and the amount match. trx_id is stored as gateway_transaction_id. |
failed |
Failed, or Cancelled if the customer's callback said Canceled. |
processing |
Stays pending (the customer has not finished paying). |
refund |
Stays pending, and the response is logged for you to review. |
any status_code other than "200", or a non-JSON body |
Stays pending. bdpaygate:sync asks again later. |
A Failed or Cancelled payment still becomes Completed if a later status check says success.
Amount check
The package compares request_amount (or payment_amount when request_amount is missing) with the order amount. If pay_with_charge is off, payment_amount must also match exactly.
Turn pay_with_charge on only if your PayStation account adds the processing charge to what the customer pays. The amount actually paid is then allowed to be higher than the order amount.
Refunds
PayStation documents no refund API, so this driver does not implement SupportsRefunds, and BdPayGate::refund() throws UnsupportedOperationException. Refund from the PayStation merchant panel.
Limits and quirks
- Business errors come back as HTTP 200 with a string
status_code. The driver never treats HTTP 200 alone as success. trx_statuscasing is inconsistent: the field description sayssuccess, but the samples showSuccessandFailed. The driver lowercases and trims it before comparing.- The status endpoint is authenticated by the
merchantIdheader alone, with no password and no token. That is how PayStation documents it. Treat what it returns as non-secret. payment_amountis documented as an integer, but the sandbox accepts and echoes two decimal places, and the driver always sends two decimals.cust_emailis mandatory. If the customer has no email, the driver sendsnoemail@example.com.- PayStation's own PHP sample turns off TLS certificate checks. This package never does.
- The public docs site sits behind Cloudflare and may block automated fetches. Read it in a browser.
Not yet verified
-
IPN / webhook. PayStation's current documentation describes only the browser redirect to
callback_url. Any server-to-server notification is configured per merchant and its payload is undocumented. If PayStation sends one to/bdpaygate/paystation/ipnwith aninvoice_number, it is verified through/transaction-statusexactly like the redirect. Runbdpaygate:syncon a schedule either way:Schedule::command('bdpaygate:sync')->everyTenMinutes(); -
pay_with_chargesemantics. The documentation says only "Will the merchant bear the payment charge or not". Confirm with PayStation which direction1means for your account. -
Live behaviour. The shapes come from the documentation and the sandbox, not from a live account.
-
"Invalid Token." One documented error mentions a token, which suggests an older token-based API exists. This driver uses only the documented
merchantIdand password endpoints. -
Status v2.
POST /v2/transaction-status(by PayStationtrxId) exists but is not used. Lookups are always by our owninvoice_number.
Sandbox run (2026-10-15)
Verified against the real PayStation sandbox through a Laravel 13 app: a 120 BDT payment paid with bKash inside PayStation's checkout, confirmed by POST /transaction-status. The live sandbox reports trx_status: "successful", not the documented "success"; the driver accepts both. No refund API and no separate IPN exist (checked against the current docs): the callback to callback_url is the only notification, and it is never trusted on its own.