pongsit / scb
SCB Partners API (Thai QR bill payment) — framework agnostic
Requires
- php: >=7.4
- ext-curl: *
- ext-json: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
SCB Partners API (Thai PromptPay bill-payment QR) for PHP 7.4+.
No framework, no dependencies beyond ext-curl and ext-json — drop it into a
plain-PHP app, a Laravel app, or anything else.
Why this exists
Every project that takes money re-implements the same four calls, and the same
mistake keeps coming with it: trusting the webhook body. The callback endpoint
is public and SCB signs nothing, so anyone can POST {"amount": "5000.00"}.
This package never reads the amount from the request — it re-fetches the
transaction from SCB over an authenticated call and compares that amount to
what your application says is due.
Install
composer require pongsit/scb
Configure
use Pongsit\Scb\Config; $config = Config::fromArray([ 'clientId' => '...', 'clientSecret' => '...', 'merchantId' => '...', // biller id 'ppId' => '...', // PromptPay id on the QR 'ref3' => 'RAB', // as registered with SCB 'env' => 'sandbox', // sandbox | uat | production ]);
env decides the host. Nothing else changes between environments, so a project
can run against sandbox until the day it goes live.
Show a QR
use Pongsit\Scb\{Client, Qr}; $qr = new Qr(new Client($config)); $result = $qr->create( 365.00, // amount due 'AJNUNU', // ref1 — appears on the payer's slip (string) $orderId // ref2 — comes back in the webhook, must identify your row ); echo '<img src="' . Qr::dataUri($result['qrImage']) . '">';
ref1/ref2/ref3 must be 1–20 characters of A-Z0-9; the package uppercases
and validates them instead of letting SCB reject the call later.
Creating a QR moves no money — safe to call while testing.
Take the callback
Implement three methods so the package can check the payment without knowing anything about your schema:
use Pongsit\Scb\{SettlementStore, Transaction}; class OrderStore implements SettlementStore { public function expectedAmount($ref2) { $row = /* SELECT price FROM orders WHERE id = $ref2 */; return $row ? (float) $row['price'] : null; // null = I don't know this reference } public function isSettled($ref2) { return /* SELECT confirm FROM orders WHERE id = $ref2 */; } public function settle($ref2, Transaction $transaction) { // money is confirmed and the amount is right — do your thing } }
Then the endpoint SCB calls:
use Pongsit\Scb\{Client, Webhook}; $webhook = new Webhook(new Client($config), function ($message, $context) { error_log($message . ' ' . json_encode($context)); }); $result = $webhook->handle(null, new OrderStore()); $webhook->acknowledge($result); if ($result->needsAttention()) { // underpaid, or a reference we don't recognise — worth an alert }
handle() returns a WebhookResult with one of:
| status | meaning | settle() called |
|---|---|---|
settled |
amount confirmed and sufficient | yes |
already_settled |
SCB retried a callback | no |
underpaid |
SCB confirms less than is due | no |
unknown_reference |
expectedAmount() returned null |
no |
not_found |
SCB has no record of the transaction | no |
invalid_payload |
body was not usable JSON | no |
The package acknowledges every case with SCB's expected resCode 00. Retrying
will not turn an underpayment into a full one, and an unacknowledged callback is
retried forever.
Verify a payment yourself
use Pongsit\Scb\Verify; $verify = new Verify(new Client($config)); $txn = $verify->byReference('2026-07-23', 'AJNUNU', '142'); // bank agnostic $txn = $verify->byTransactionId($transactionId, $sendingBank); // bank code required
Prefer byReference(). byTransactionId() needs the payer's bank code, which
is not always 014 — a payer on another bank will not be found if you
hardcode it.
Rules the package enforces
- The amount always comes from SCB, never from the request body.
- Less than the expected amount is never settled.
- A reference the application does not recognise is never settled.
- A repeated callback settles once.
requestUIdis a fresh UUID per call, not a user id.- Credentials are never written to the log callback.
Tests
php tests/offline-test.php
No network, no database — covers the money rules above.