PHP SDK
Official PHP SDK for Scan & Pay — accept PayTo PayID payments via QR code from any PHP backend (Laravel, Symfony, Magento, plain PHP, …).
Install
composer require scanandpay/phpRequires PHP 8.1+ with the curl, json, and hash extensions.
Quickstart
use ScanAndPay\ScanAndPay;
use ScanAndPay\Exceptions\WebhookSignatureException;
$client = new ScanAndPay(
merchantId: getenv('SCANANDPAY_MERCHANT_ID'),
apiSecret: getenv('SCANANDPAY_API_SECRET'),
webhookSecret: getenv('SCANANDPAY_WEBHOOK_SECRET'), // optional
baseUrl: getenv('SCANANDPAY_API_BASE_URL') ?: 'https://api.scanandpay.com.au',
);
// 1. Create a session at checkout. Amount is float dollars.
$session = $client->createSession(
amount: 19.90, // $19.90
platformOrderId: 'order_456',
payId: 'merchant@example.com.au',
merchantName: 'Acme Coffee',
);
// 2. Render the QR widget on the page.
echo scanandpay_checkout($session, pollUrl: '/scanandpay/status');
// 3. In your webhook handler, verify and consume the event.
try {
$event = $client->webhooks()->verify(
signature: $_SERVER['HTTP_X_SCANPAY_SIGNATURE'] ?? '',
body: file_get_contents('php://input'),
);
if ($event->isPaid()) {
// Mark order paid using $event->orderId, $event->txId, ...
}
} catch (WebhookSignatureException $e) {
http_response_code(401);
exit('Invalid webhook');
}Amount format
amount is always float dollars (e.g. 19.90for $19.90). This matches the Scan & Pay API directly — no multiplication or division needed.
$client->createSession(amount: 19.90, ...); // ✓ $19.90
$client->createSession(amount: 0.50, ...); // ✓ $0.50
$client->createSession(amount: 1000.00, ...); // ✓ $1,000.00
$client->createSession(amount: -1, ...); // ✗ ValidationException
$client->createSession(amount: 0, ...); // ✗ ValidationExceptionFor display, use $session->amount directly:
$display = number_format($session->amount, 2); // "19.90"Error handling
All thrown errors extend ScanAndPay\\Exceptions\\ScanAndPayException.
| Class | When |
|---|---|
| ValidationException | Bad input rejected before any HTTP call. |
| AuthenticationException | API rejected API Secret (rotate your keys). |
| ApiException | Non-2xx response from the API. |
| NetworkException | Transport failure after exhausting retries. |
| WebhookSignatureException | Webhook signature or timestamp check failed. |
Production integration checklist
Use the SDK from your backend only. The browser can display the returned QR or pay URL, but it must never receive your API Secret or Webhook Secret.
- Create your local order first. Store cart, customer, amount, currency, and a durable
platformOrderIdin your database with a pending payment status. - Create one Scan & Pay session for that order. Call
$client->createSession()from your backend using the same order id. Pass an idempotency key based on your order id when retries are possible. - Render the payment step as pending. Show the returned session with
scanandpay_checkout(), your own QR renderer, or a redirect to the returned pay URL. Do not send the customer to a success page yet. - Expose a small status endpoint. Point
pollUrlat your backend. It should fetch status with the SDK and return only the public status fields your UI needs. - Verify webhooks on raw request bytes. Use
$client->webhooks()->verify($signature, $rawBody)before trusting the payload. - Mark paid from the verified webhook.
$event->isPaid()is the source of truth for fulfilment. - Keep test and live credentials separate. Never commit merchant credentials into source control, frontend bundles, mobile apps, logs, screenshots, or support tickets.
Safe to share publicly: package install commands, SDK method names, request and response fields, webhook verification rules, idempotency guidance, test-mode behaviour, and error handling. Keep database schemas, internal routes, cloud project names, merchant secrets, and admin tooling private.
Integration pitfalls
Three things that bite first-time integrators (we've hit each one ourselves):
- Create the order BEFORE minting the session. The session binds to your
platformOrderIdand the webhook references the same id when the payment is confirmed. Persist the order in your DB (statuspending) first, then call$client->createSession([...]). - Don't finalise the order on Place Order. A common pattern in checkout code (WooCommerce included) is a hardcoded list — "if this gateway is stripe / paypal / etc. defer; otherwise finalise immediately". If that list is missing
scanandpay, the checkout flashes a success screen and the QR widget never renders. Always treat Scan & Pay as a deferred / async-confirmation gateway, just like Stripe Checkout or PayPal — the customer needs to see the QR and complete the scan-and-pay in their banking app before the order can be considered paid. - Webhook is the source of truth. Mark the order paid only when the verified webhook arrives with
$event->isPaid()(orevent.status === 'confirmed'if you're inspecting the raw payload). The Place Order click on your site does NOT mean money has moved.
Resources
Need help with your integration?
Book a session with Mehmet directly — SDK setup, terminal configuration, or any technical questions.