onpayio / php-sdk
PHP SDK for OnPay.io API
Requires
- php: >=8.2
- ext-json: *
- league/oauth2-client: ^2.9
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^1.0.2 || ^2.0 || ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.5
- vimeo/psalm: ~6.17.0
Suggests
- guzzlehttp/guzzle: A PSR-18 client + PSR-17 factories the SDK will auto-discover.
- nyholm/psr7: A lightweight PSR-7/PSR-17 implementation to pair with a custom PSR-18 client.
Provides
None
Conflicts
None
Replaces
None
- dev-master / 2.x-dev
- 2.0.0
- 1.x-dev
- 1.0.39
- 1.0.38
- 1.0.37
- 1.0.36
- 1.0.35
- 1.0.34
- 1.0.33
- 1.0.30
- 1.0.29
- 1.0.28
- 1.0.27
- 1.0.26
- 1.0.25
- 1.0.24
- 1.0.23
- 1.0.22
- 1.0.21
- 1.0.20
- 1.0.19
- 1.0.18
- 1.0.17
- 1.0.16
- 1.0.15
- 1.0.14
- 1.0.13
- 1.0.12
- 1.0.11
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- 1.0.0-alpha2
- 1.0.0-alpha
- dev-events-endpoint
This package is auto-updated.
Last update: 2026-09-24 12:16:34 UTC
README
PHP SDK for the OnPay payment platform.
- API reference: https://onpay.io/docs/technical/api_v1.html
- Payment window reference: https://onpay.io/docs/technical/paymentwindow_v3.html
- Upgrading from 1.x: UPGRADE.md
Requirements
- PHP 8.2 or later.
- A PSR-18 HTTP client with PSR-17 factories, for example Guzzle. The SDK does not ship one. A common client such as Guzzle or Symfony's is discovered automatically; anything else you pass to the constructor (see Using your own HTTP client).
Installation
composer require onpayio/php-sdk guzzlehttp/guzzle
Replace guzzlehttp/guzzle with the PSR-18 client you use. Composer may ask whether to
allow the php-http/discovery plugin; either answer works, discovery needs no plugin at
runtime.
Getting started
1. Store the access token
The SDK hands you the OAuth token as a string and asks for it back on every request.
Implement OnPay\TokenStorageInterface to keep it somewhere durable and private: the
token gives full API access, so treat it like a database password.
final class FileTokenStorage implements \OnPay\TokenStorageInterface { public function __construct(private string $file) {} public function getToken(): ?string { $token = is_file($this->file) ? file_get_contents($this->file) : false; return $token === false ? null : $token; } public function saveToken(string $token): void { file_put_contents($this->file, $token); } }
2. Store the authorization state
The OAuth flow spans two requests: one that redirects the merchant to OnPay and one
that receives the authorization code. Implement OnPay\AuthStateStorageInterface to
carry the CSRF state and PKCE verifier between them, scoped to the current visitor.
A PHP session is the simplest place:
final class SessionAuthStateStorage implements \OnPay\AuthStateStorageInterface { public function saveState(string $state): void { $_SESSION['onpay_state'] = $state; } public function getState(): ?string { return $_SESSION['onpay_state'] ?? null; } public function saveCodeVerifier(string $codeVerifier): void { $_SESSION['onpay_verifier'] = $codeVerifier; } public function getCodeVerifier(): ?string { return $_SESSION['onpay_verifier'] ?? null; } public function clear(): void { unset($_SESSION['onpay_state'], $_SESSION['onpay_verifier']); } }
3. Authorize
session_start(); $api = new \OnPay\OnPayAPI( new FileTokenStorage(__DIR__ . '/../var/onpay-token.json'), [ 'client_id' => 'example.com', // the domain your integration runs on 'redirect_uri' => 'https://example.com/onpay/callback', // where OnPay sends the merchant back 'gateway_id' => 'YourGatewayId', 'platform' => 'my-shop-plugin/1.2.3', // optional: identifies your integration to OnPay ], new SessionAuthStateStorage(), ); // Step 1: send the merchant to OnPay to approve the integration. header('Location: ' . $api->authorize()); // Step 2: on the redirect_uri, exchange the returned code for a token. $api->finishAuthorize($_GET['code'], $_GET['state']);
After that the token is refreshed and re-saved automatically. isAuthorized() makes a
ping request to check that the stored token still works.
A static API token created in the OnPay management panel skips the flow entirely:
$api = new \OnPay\OnPayAPI(new \OnPay\StaticToken('YourStaticToken'), [ 'client_id' => 'example.com', ]);
4. Create a payment
Describe the payment with a PaymentWindow, create it through the API and send the
shopper to the returned payment window link. Amounts are in minor units.
use OnPay\API\Enum\PaymentMethod; use OnPay\API\PaymentWindow; use OnPay\API\PaymentWindow\PaymentInfo; $window = new PaymentWindow(); $window->setCurrency('DKK'); $window->setAmount('12345'); $window->setReference('order-1001'); // unique per payment, e.g. the order number $window->setWebsite('https://example.com'); $window->setAcceptUrl('https://example.com/onpay/accept'); $window->setDeclineUrl('https://example.com/onpay/decline'); $window->setCallbackUrl('https://example.com/onpay/callback'); $window->setMethod(PaymentMethod::CARD); $window->setLanguage('en'); $window->setTestModeEnabled(true); // Required when you have it (SCA and card-scheme rules): what you already know about the // shopper, for 3-D Secure. Do not collect extra data just for this. $info = new PaymentInfo(); $info->setName('Jane Doe'); $info->setEmail('jane@example.com'); $info->setBillingAddressCountry('208'); $window->setInfo($info); $payment = $api->payment()->createNewPayment($window); header('Location: ' . $payment->getPaymentWindowLink());
Every field the API accepts has a setter on PaymentWindow and PaymentInfo; the
API reference
describes them. Some payment methods need a cart. Its total, items plus shipping and
handling minus discount, must equal the payment amount:
use OnPay\API\PaymentWindow\Cart; use OnPay\API\PaymentWindow\CartItem; $cart = new Cart(); $cart->addItem(new CartItem('T-shirt', 9900, 1, 1980, sku: 'TS-001')); // name, price, quantity, tax $cart->setShipping(2445, 489); // price, tax $window->setCart($cart); // 9900 + 2445 = 12345
To store the card for later charges, create a subscription (see Stored cards) instead of a one-off payment. It should charge its first transaction as part of the signup, so the shopper authenticates the real amount and liability shifts to the issuer. Create one without a transaction only for a trial period or when replacing a stored card.
$window->setType('subscription'); $window->setSubscriptionWithTransaction(true);
5. Verify the result
OnPay signs the onpay_* parameters it appends to the accept URL and sends to the callback
URL (a GET request). Check the signature with your payment window secret before trusting
them. Declines sent to the decline URL are not signed.
$window = new \OnPay\API\PaymentWindow(); $window->setSecret('YourWindowSecret'); if (!$window->validatePayment($_GET)) { http_response_code(400); exit; }
Transactions
$transactions = $api->transaction(); $page = $transactions->getTransactions(page: 1, pageSize: 50, status: 'active'); foreach ($page->transactions as $transaction) { echo $transaction->transactionNumber, ' ', $transaction->status, PHP_EOL; } $page->pagination->totalPages; $transaction = $transactions->getTransaction('00000000-0000-0000-0000-000000000000'); // Capture everything that was authorised. $transactions->captureTransaction($transaction->uuid); // Partial capture or refund: state the total you want charged or refunded after the call, // not the amount to add. Repeating the call is then harmless, and an earlier partial // capture is accounted for. $transactions->captureTransaction($transaction->uuid, postActionChargeAmount: 5000); $transactions->refundTransaction($transaction->uuid, postActionRefundAmount: 2500); $transactions->cancelTransaction($transaction->uuid);
Stored cards (COF)
OnPay stores a card as a subscription, the card on file (COF) of the card-scheme rules, and that is the name the API and these methods use. Charging it later, without the shopper present, is a merchant-initiated transaction (MIT).
$subscriptions = $api->subscription(); $page = $subscriptions->getSubscriptions(status: 'active'); $subscription = $subscriptions->getSubscription('00000000-0000-0000-0000-000000000000'); // MIT: charge the stored card; returns the new transaction. $transaction = $subscriptions->createTransactionFromSubscription($subscription->uuid, 9900, 'order-1002'); $subscriptions->cancelSubscription($subscription->uuid);
Gateway
Configuration-time lookups. Cache the results; do not call these while processing a payment.
$api->gateway()->getInformation(); // gateway id $api->gateway()->getPaymentWindowDesigns(); // designs available to setDesign() $api->gateway()->getPaymentWindowIntegrationSettings(); // payment window secret
Errors
Every error the SDK raises extends OnPay\API\Exception\OnPayException:
| Exception | Meaning |
|---|---|
TokenException |
No usable token, or OnPay rejected it. Re-authorize. |
ConnectionException |
The HTTP request never got a response. |
ApiException |
OnPay answered with an error, or with a response the SDK could not read. |
MissingDataException, InvalidFormatException, InvalidCartException |
The PaymentWindow you built is incomplete or invalid. |
try { $api->transaction()->captureTransaction($uuid); } catch (\OnPay\API\Exception\TokenException $e) { // send the merchant through authorize() again } catch (\OnPay\API\Exception\OnPayException $e) { $request = $api->getLastHttpRequest(); // what was sent $response = $api->getLastHttpResponse(); // what came back }
Invalid constructor options throw a plain \InvalidArgumentException.
Logging
Failed requests are logged at warning (4xx) or error (5xx, transport failure), successful
ones at debug. Tokens and secrets are redacted first. Pass any PSR-3 logger; without one,
warnings and errors go to PHP's error_log().
$api = new \OnPay\OnPayAPI($tokenStorage, $options, $authStateStorage, logger: $logger);
Pass a Psr\Log\NullLogger to silence the SDK.
Using your own HTTP client
Any PSR-18 client works. When you inject one, timeouts, proxies and TLS settings are whatever you configured on it. When the SDK discovers Guzzle itself it creates the client with a 30-second timeout and a 5-second connect timeout; any other discovered client runs with its own defaults.
$api = new \OnPay\OnPayAPI( $tokenStorage, $options, $authStateStorage, httpClient: $client, // Psr\Http\Client\ClientInterface requestFactory: $requestFactory, // Psr\Http\Message\RequestFactoryInterface streamFactory: $streamFactory, // Psr\Http\Message\StreamFactoryInterface );
Factories you leave out are discovered from the installed packages.
Payment window as an HTML form
The payment window can also be opened by posting a signed form directly from the browser, without an API call. It needs the gateway id and payment window secret from the OnPay management panel, plus an accept URL. Prefer creating the payment through the API when you can.
$window = new \OnPay\API\PaymentWindow(); $window->setGatewayId('YourGatewayId'); $window->setSecret('YourWindowSecret'); $window->setCurrency('DKK'); $window->setAmount('12345'); $window->setReference('order-1001'); $window->setAcceptUrl('https://example.com/onpay/accept'); $window->setWebsite('https://example.com'); ?> <?php if ($window->isValid()): ?> <form method="post" action="<?= $window->getActionUrl() ?>" accept-charset="UTF-8"> <?php foreach ($window->getFormFields() as $name => $value): ?> <input type="hidden" name="<?= $name ?>" value="<?= htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8') ?>"> <?php endforeach ?> <button type="submit">Pay</button> </form> <?php endif ?>
The result is verified exactly as in Verify the result.