Search by

pongsit / scb

pongsit

SCB Partners API (Thai QR bill payment) — framework agnostic

Package info

github.com/ppppongsit/scb

pkg:composer/pongsit/scb

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

v0.1.2 2026-07-23 06:37 UTC

This package is auto-updated.

Last update: 2026-10-09 10:17:44 UTC


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.
  • requestUId is 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.