PHPackages                             scanandpay/php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. scanandpay/php

ActiveLibrary[Payment Processing](/categories/payments)

scanandpay/php
==============

Official Scan &amp; Pay PHP SDK — accept PayTo PayID payments via QR.

v0.4.1(4w ago)01Apache-2.0PHPPHP ^8.1CI passing

Since May 5Pushed 3w agoCompare

[ Source](https://github.com/scan-and-pay/scanandpay-php)[ Packagist](https://packagist.org/packages/scanandpay/php)[ Docs](https://docs.scanandpay.com.au)[ RSS](/packages/scanandpay-php/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (2)Versions (7)Used By (0)

scanandpay/php
==============

[](#scanandpayphp)

Official PHP SDK for [Scan &amp; Pay](https://docs.scanandpay.com.au) — generate PayTo PayID QR codes and payment links from any PHP backend (Laravel, Symfony, Magento, plain PHP, …), then react to our signed webhook when a payment is confirmed. No WordPress or framework required.

Your backend mints a session, we run the payment surface, our webhook confirms back to you.

How the pieces fit
------------------

[](#how-the-pieces-fit)

```
Your backend     ──  createSession()   ─▶  Scan & Pay API     (mint QR + pay URL)
Customer phone   ──  scans QR          ─▶  pay.scanandpay.com.au   (we collect payment)
Scan & Pay       ──  signed webhook    ─▶  Your backend       (payment confirmation)

```

You never handle funds, banking credentials, or PayID resolution — the SDK generates the link, the customer pays on our hosted surface, and you receive a verified webhook telling you the order is paid.

Install
-------

[](#install)

```
composer require scanandpay/php
```

Requires PHP 8.1+ with the `curl`, `json`, and `hash` extensions.

Quickstart
----------

[](#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-format)

`amount` is always **float dollars** (e.g. `19.90` for $19.90). This matches the Scan &amp; 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, ...);       // ✗ ValidationException
```

For display, use `$session->amount` directly:

```
$display = number_format($session->amount, 2);  // "19.90"
```

Metadata
--------

[](#metadata)

Attach a free-form key/value bag to any session. We echo it back unchanged in the webhook payload + `getStatus` response, so you can correlate the payment with your own order/customer/cart records.

```
$session = $client->createSession(
    amount: 19.90,
    platformOrderId: 'order_456',
    payId: 'merchant@example.com.au',
    merchantName: 'Acme Coffee',
    metadata: [
        'customer_id' => 'cus_42',
        'cart_id'     => 'cart_99',
    ],
);

// Later in your webhook handler:
$event->metadata['customer_id']; // 'cus_42'
```

Limits: max **50 keys**, max **500 chars** per key + value (validated client-side before any network call). Don't put secrets here — metadata isn't encrypted at rest in any special way.

API versioning
--------------

[](#api-versioning)

The SDK pins itself to a date-stamped API contract via the `Scanpay-Version`header (e.g. `2026-05-07`). When we evolve the wire format, your already-installed SDK keeps running against the contract it was built for. Upgrade at your own pace.

```
HttpClient::API_VERSION;  // '2026-05-07'
HttpClient::VERSION;      // '0.3.0'
```

Idempotency
-----------

[](#idempotency)

Every `createSession` call sends an `Idempotency-Key` header. The SDK generates a UUIDv7 by default; pass your own to make retries safe across process restarts.

```
$client->createSession(
    amount: 19.90,
    platformOrderId: 'order_456',
    payId: 'merchant@example.com.au',
    merchantName: 'Acme Coffee',
    idempotencyKey: 'order_456:attempt_1',
);
```

Retries
-------

[](#retries)

Transient failures (5xx, network) are retried 3× with exponential backoff and full-half jitter (≈250 → 500 → 1000ms). 4xx responses surface immediately. POSTs retry **only when an Idempotency-Key header is present** — the SDK auto-sends one for `createSession` so this is transparent for the standard path.

Tune by injecting a configured `HttpClient`:

```
use ScanAndPay\HttpClient;

$http = new HttpClient(
    apiSecret: getenv('SCANANDPAY_API_SECRET'),
    baseUrl: 'https://api.scanandpay.com.au',
    timeoutSeconds: 30,
    retries: 5,
    baseMs: 100,
);
$client = new ScanAndPay(
    merchantId: 'm', apiSecret: 's', http: $http,
);
```

Webhook secret rotation
-----------------------

[](#webhook-secret-rotation)

Rotate the merchant webhook secret in the dashboard; the SDK accepts both the new and old values during the 30-day grace window:

```
$client = new ScanAndPay(
    merchantId: 'm',
    apiSecret: 's',
    webhookSecret: $current,           // signs new outbound deliveries
    previousWebhookSecret: $previous,  // accepted on inbound until grace expires
);
```

`WebhookVerifier::verify()` tries `current` first, falls back to `previous`, then throws `WebhookSignatureException`.

Errors
------

[](#errors)

All thrown errors extend `ScanAndPay\Exceptions\ScanAndPayException`. HTTP-status-mapped subclasses extend `ApiException`:

ClassHTTPSurfaces when`ValidationException`—Bad input rejected before any HTTP call`AuthenticationException`401API rejected `X-Scanpay-Key``AuthorizationException`403Key valid, action not permitted`NotFoundException`404Session/merchant doesn't exist`IdempotencyConflictException`409Key in flight or reused with different body`RateLimitException`429Exhausted rate limit (no retry on 4xx)`ApiException`other 4xx400, 405, 422, … (no retry)`ServerException`5xxAfter retries exhausted`NetworkException`—Transport failure after retries exhausted`WebhookSignatureException`—Webhook signature/timestamp/replay check failedCredentials
-----------

[](#credentials)

Sign in to the merchant dashboard at [business.scanandpay.com.au](https://business.scanandpay.com.au), open **Settings → Integrations**, and copy:

- `Merchant ID`
- `API Base URL`
- `API Secret`
- `Webhook Secret`

Store them in environment variables — never commit them to source control.

Production integration checklist
--------------------------------

[](#production-integration-checklist)

Use the SDK from your backend only. Your frontend can display the returned payment URL or QR widget, but it must never receive the API Secret or Webhook Secret.

1. **Create your local order first.** Store the cart, customer, amount, currency, and a durable `platformOrderId` in your own database with a pending payment status.
2. **Create one Scan &amp; Pay session for that order.** Call `$client->createSession()` from your backend using the same `platformOrderId`. Pass an idempotency key based on your order id if the request may be retried by a queue worker, PHP-FPM retry, or browser refresh.
3. **Render the payment step as pending.** Show the returned payment session to the customer 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.
4. **Expose a small status endpoint.** If you use `scanandpay_checkout()`, point `pollUrl` at your own backend endpoint. That endpoint should fetch status with the SDK and return only the public status fields your UI needs.
5. **Verify the webhook on raw request bytes.** Use `$client->webhooks()->verify($signature, $rawBody)` before trusting any webhook payload. A parsed or re-serialized body will fail signature verification.
6. **React to our payment confirmation webhook.** Treat `$event->isPaid()`as the signal to mark your order paid in your own database. The Place Order click and QR render only mean payment has started — money has not moved until the webhook arrives.
7. **Keep test and live credentials separate.** Use environment variables or your secret manager, and 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/response fields, webhook verification rules, idempotency guidance, test-mode behaviour, and error handling. Keep your database schema, internal routes, cloud project names, merchant secrets, and admin tooling private.

Integration pitfalls
--------------------

[](#integration-pitfalls)

Things that bite first-time integrators (we've hit each one ourselves):

1. **Create the order BEFORE minting the session.** The session binds to your `platformOrderId` and the webhook references the same id when the payment is confirmed. Persist the order in your DB (status `pending`) first, then call `$client->createSession([...])`.
2. **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 &amp; 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.
3. **Webhook is the source of truth.** Mark the order paid only when the verified webhook arrives with `$event->isPaid()` (or `event.status === 'confirmed'` if you're inspecting the raw payload). The Place Order click on your site does NOT mean money has moved.

Documentation
-------------

[](#documentation)

- **API reference:**
- **Webhook payload + signing:**
- **OpenAPI spec:**

Local development
-----------------

[](#local-development)

```
composer install
composer test          # PHPUnit
composer lint          # php-cs-fixer dry-run
composer format        # php-cs-fixer apply
```

Versioning
----------

[](#versioning)

SemVer. Pre-1.0 — expect minor breaking changes between versions until 1.0.

Licence
-------

[](#licence)

Apache-2.0.

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance94

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 88.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

6

Last Release

28d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/f158b13c4c2a01b99c550361a78f29136b0c42d0e09ee42e6bf0e93c3c26385e?d=identicon)[scanandpay](/maintainers/scanandpay)

---

Top Contributors

[![kirmehmetali](https://avatars.githubusercontent.com/u/213948505?v=4)](https://github.com/kirmehmetali "kirmehmetali (8 commits)")[![scan-and-pay](https://avatars.githubusercontent.com/u/232486550?v=4)](https://github.com/scan-and-pay "scan-and-pay (1 commits)")

---

Tags

qrpaymentsaustraliapayidpaytoscanandpay

###  Code Quality

TestsPHPUnit

Code StylePHP CS Fixer

### Embed Badge

![Health badge](/badges/scanandpay-php/health.svg)

```
[![Health](https://phpackages.com/badges/scanandpay-php/health.svg)](https://phpackages.com/packages/scanandpay-php)
```

###  Alternatives

[srmklive/paypal

PayPal REST API client for Laravel and standalone PHP.

1.1k4.0M28](/packages/srmklive-paypal)[amzn/amazon-pay-sdk-php

Amazon Pay SDK (PHP)

20612.2M3](/packages/amzn-amazon-pay-sdk-php)[unicodeveloper/laravel-paystack

A Laravel Package for Paystack

6531.0M11](/packages/unicodeveloper-laravel-paystack)[ignited/laravel-omnipay

Integrates Omnipay with Laravel and provides an easy configuration.

5231.1M12](/packages/ignited-laravel-omnipay)[recurly/recurly-client

The PHP client library for the Recurly API

1746.5M8](/packages/recurly-recurly-client)[chargebee/chargebee-php

ChargeBee API client implementation for PHP

788.3M9](/packages/chargebee-chargebee-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
