Server-side PHP SDK for new GoPay Payments API v4.
Requires PHP ≥ 8.1. Transport-agnostic — works with any PSR-18 HTTP client.
See MIGRATION.md for a full breakdown. The SDK is v4-only — not source-level compatible with gopay/payments-sdk v1.
Key changes:
- Gateway URL changed — update your
Configinitialization - Legacy
gw_urlredirect removed — replaceheader('Location: gw_url')withchargePayment(). 3DS challenges still redirect viagetAction()->getRedirectUrl() - Recurrences redesigned — now standalone entities, not attached to a payment
- 11 v3 methods removed — pre-auth capture/void, EET, account statement, payment instruments
composer require gopaycommunity/gopay-php-api-v4For the HTTP client you need a PSR-18 implementation. Guzzle 7 is the most common choice:
composer require guzzlehttp/guzzleAny PSR-18-compatible client works (Symfony HttpClient, Buzz, …).
use GoPay\Payments\GoPayClient;
use GoPay\Payments\Config;
use GoPay\Payments\Environment;
// 1. Initialize the client
$sdk = new GoPayClient(new Config(
environment: Environment::Sandbox,
shareableKey: 'YOUR_SHAREABLE_KEY', // optional — for browser SDK initialisation
));
// 2. Authenticate (stored internally; token refreshes automatically)
$sdk->authenticate('YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET', 'payment:write payment:read');
// 3. Create a payment
$payment = $sdk->createPayment('YOUR_GOID', [
'amount' => 1000, // 10.00 CZK (in minor units / haléře)
'currency' => 'CZK',
'order_number' => 'ORDER-001',
'customer' => ['email' => 'customer@example.com'],
'callback' => [
'notification_url' => 'https://yourshop.com/notify',
'return_url' => 'https://yourshop.com/return',
],
]);
// 4. Charge using a card token from the browser iframe
// (browser SDK's mountCardForm() → user enters card → iframe returns token)
$charge = $sdk->chargePayment($payment->getId(), [
'payment_instrument' => [
'payment_instrument' => 'PAYMENT_CARD',
'input' => [
'input_type' => 'CARD_TOKEN',
'card_token' => $cardToken, // from the browser SDK iframe
],
],
]);
// 5a. No 3DS needed — poll for final state
if ($charge->getAction() === null) {
$final = $sdk->awaitChargeState($payment->getId());
echo $final->getState(); // 'SUCCEEDED'
}
// 5b. 3DS required — redirect the customer
if ($charge->getAction()?->getRedirectUrl() !== null) {
header('Location: ' . $charge->getAction()->getRedirectUrl());
exit;
}
gw_url— backward compatibility. ThePaymentDetailsobject contains agw_urlfield. Do not redirect the customer to it by default. It exists for backward-compat with old redirect-based flows for payment methods not yet implemented on v4. This SDK's flow is always:createPayment()→chargePayment().
use GoPay\Payments\Config;
use GoPay\Payments\Environment;
$config = new Config(
environment: Environment::Production, // Environment::Sandbox (default)
baseUrl: null, // override base URL (e.g. staging); null = use environment
debugLoggingEnabled: false, // log request/response to error_log (default false)
onError: null, // callable(\Throwable): void — called before throwing
shareableKey: null, // shareable key for browser SDK initialisation
);| Parameter | Type | Default | Description |
|---|---|---|---|
environment |
Environment |
Sandbox |
API environment |
baseUrl |
?string |
null |
Override the resolved URL (e.g. for staging) |
debugLoggingEnabled |
bool |
false |
Log to error_log |
onError |
?callable |
null |
Invoked before every thrown exception |
shareableKey |
?string |
null |
Shareable key for getBrowserKeys() |
| Environment | Base URL |
|---|---|
Environment::Sandbox |
https://api.sandbox.gopay.com/api/merchant/payments/4.0 |
Environment::Production |
https://api.gopay.com/api/merchant/payments/4.0 |
By default, php-http/discovery auto-discovers an installed PSR-18 client. You can inject your own:
use GoPay\Payments\GoPayClient;
use GoPay\Payments\Config;
$sdk = new GoPayClient(
config: new Config(),
httpClient: $myPsr18Client, // Psr\Http\Client\ClientInterface
requestFactory: $myRequestFactory, // Psr\Http\Message\RequestFactoryInterface
streamFactory: $myStreamFactory, // Psr\Http\Message\StreamFactoryInterface
);// Authenticate (client_credentials grant)
$sdk->authenticate(string $clientId, string $clientSecret, string $scope): void
// Check if a token is stored
$sdk->isAuthenticated(): bool
// Clear tokens
$sdk->logout(): void
// Store shareable key for browser SDK
$sdk->setShareableKey(string $key): void
// Return shareable_key + client_id for browser SDK init (safe to expose to the browser)
$sdk->getBrowserKeys(): array{shareable_key: string, client_id: string}// Create a payment session
$sdk->createPayment(string $goid, array $params): PaymentDetails
// Get payment status
$sdk->getPaymentStatus(string $paymentId): PaymentDetails
// Charge a payment (card token / Google Pay / Apple Pay)
$sdk->chargePayment(string $paymentId, array $params): PaymentChargeResponse
// Get charge state (poll manually)
$sdk->getChargeState(string $paymentId): PaymentChargeStatusResponse
// Poll charge state until terminal (throws on FAILED / timeout)
// WARNING: blocks the PHP process — see "Production deployment" below.
$sdk->awaitChargeState(
string $paymentId,
int $timeoutSeconds = 30,
int $pollIntervalMs = 1_000,
): PaymentChargeStatusResponse
// Google Pay configuration (pre-filled paymentDataRequest)
$sdk->getGooglePayInfo(string $paymentId): array
// Apple Pay configuration (applepayVersion, applePayPaymentRequest)
$sdk->getApplePayInfo(string $paymentId): array
// Apple Pay merchant validation (server-side; forward validationURL from browser)
$sdk->validateApplePayMerchant(string $paymentId, ?array $body = null, ?string $origin = null): array
// QR payment information (recipient details + base64 QR image)
$sdk->getQrPaymentInfo(string $paymentId, ?string $format = null): QRPaymentDetails// Get stored card details
$sdk->getCardDetails(string $cardId): PermanentCardTokenDetails
// Delete a stored card
$sdk->deleteCard(string $cardId): void
// Tokenize JWE payload from the browser iframe (returns permanent token)
$sdk->tokenizeEncryptedCard(string $payload): PermanentCardTokenDetails// Create a recurring payment agreement
$sdk->createRecurrence(string $goid, array $params): RecurrenceDetails
// Get recurrence status
$sdk->recurrenceStatus(string $recId): RecurrenceDetails
// Stop a recurrence permanently
$sdk->stopRecurrence(string $recId): void
// Start a recurrence (triggers first charge)
$sdk->startRecurrence(string $recId, ?array $params = null): PaymentDetails
// Create the next instalment
$sdk->recurrenceNext(string $recId, ?array $params = null): PaymentDetails// Refund a payment (full or partial)
$sdk->refundPayment(string $paymentId, array $params): RefundDetails
// List all refunds for a payment
$sdk->listRefunds(string $paymentId): list<RefundDetails>
// Get a single refund
$sdk->getRefund(string $refundId): RefundDetails// Create a shareable payment link
$sdk->createPaymentLink(string $goid, array $params): LinkDetails
// Get link status
$sdk->linkStatus(string $linkId): LinkDetails
// Disable a link
$sdk->disableLink(string $linkId): voidAll API methods return typed objects. Use the provided getters to access fields:
$payment = $sdk->createPayment($goid, [...]);
echo $payment->getId(); // unique payment ID
echo $payment->getState(); // 'CREATED', 'PAID', etc.
echo $payment->getAmount(); // amount in minor units (int)
$charge = $sdk->chargePayment($payment->getId(), [...]);
echo $charge->getState(); // 'SUCCEEDED', 'AUTHENTICATION_PENDING', etc.
echo $charge->getAction()?->getRedirectUrl(); // 3DS URL (null if no redirect needed)
$card = $sdk->tokenizeEncryptedCard($jwePayload);
echo $card->getToken(); // permanent card token for future charges
echo $card->getMaskedPan(); // '411111******1111'$charge = $sdk->chargePayment($paymentId, $params);
if ($charge->getAction()?->getRedirectUrl() !== null) {
// 3DS authentication required — redirect the customer
header('Location: ' . $charge->getAction()->getRedirectUrl());
exit;
}
// No 3DS — charge is complete or in processing; poll for result
$final = $sdk->awaitChargeState($paymentId);
echo $final->getState(); // 'SUCCEEDED'After 3DS the customer is redirected to your return_url. At that point use
getPaymentStatus() and PaymentPoller to determine the outcome:
use GoPay\Payments\PaymentPoller;
// On your return_url handler:
$paymentId = $_GET['payment_id'];
do {
sleep(2);
$payment = $sdk->getPaymentStatus($paymentId);
} while (PaymentPoller::isPending($payment->getState()));
if (PaymentPoller::isSuccessful($payment->getState())) {
// PAID or AUTHORIZED
echo 'Payment succeeded: ' . $payment->getState();
} else {
// CANCELED or TIMEOUTED
echo 'Payment did not complete: ' . $payment->getState();
}PaymentPoller groups payment states into three buckets:
| Group | States | Meaning |
|---|---|---|
| Pending | CREATED, PAYMENT_METHOD_CHOSEN |
Still in progress — keep polling |
| Successful | PAID, AUTHORIZED |
Completed successfully |
| Failed | CANCELED, TIMEOUTED |
Did not complete |
Post-success states (REFUNDED, PARTIALLY_REFUNDED) are terminal — isTerminal() returns true for them.
// 1. Create the payment session (same as any other payment)
$payment = $sdk->createPayment($goid, [
'amount' => 1990,
'currency' => 'CZK',
'order_number' => 'ORDER-001',
'customer' => ['email' => 'customer@example.com'],
'callback' => [
'notification_url' => 'https://yourshop.com/notify',
'return_url' => 'https://yourshop.com/return',
],
]);
// 2. Retrieve QR code and recipient details
$qr = $sdk->getQrPaymentInfo($payment->getId()); // 'png' (default) or 'svg'
$imageBase64 = $qr->getQrCode(); // base64-encoded image
// 3. Render to the customer
echo '<img src="data:image/png;base64,' . $imageBase64 . '" alt="QR payment">';
echo 'Amount: ' . $qr->getAmount() . ' ' . $qr->getCurrency();
// 4. Poll until the customer pays (webhook-preferred; polling shown for completeness)
use GoPay\Payments\PaymentPoller;
do {
sleep(3);
$status = $sdk->getPaymentStatus($payment->getId());
} while (PaymentPoller::isPending($status->getState()));
echo PaymentPoller::isSuccessful($status->getState()) ? 'Paid' : 'Not paid';All SDK methods throw on error:
| Exception | When |
|---|---|
GoPaySdkException |
Config errors, auth failures, timeout, argument errors |
GoPayHttpException |
Non-2xx API responses (status + body available) |
use GoPay\Payments\Exception\GoPaySdkException;
use GoPay\Payments\Exception\GoPayHttpException;
use GoPay\Payments\Exception\ErrorCode;
try {
$payment = $sdk->createPayment($goid, $params);
} catch (GoPayHttpException $e) {
echo $e->status; // e.g. 422
var_dump($e->body); // decoded JSON or raw string
} catch (GoPaySdkException $e) {
echo $e->errorCode->value; // e.g. 'AUTH_TOKEN_MISSING'
echo $e->getMessage();
}| Code | Meaning |
|---|---|
AuthTokenMissing |
No token; call authenticate() first |
AuthRefreshFailed |
Token refresh HTTP error |
AuthInvalidResponse |
Token response missing required fields |
AuthCredentialsMissing |
No stored client credentials |
AuthUnauthorized |
Still 401 after token refresh |
NetworkError |
Transport-level failure, including timeouts |
ChargeTimeout |
awaitChargeState() timed out |
ChargeFailed |
Charge reached FAILED state |
UnexpectedResponse |
API responded with an unexpected body shape |
InvalidConfig |
Bad configuration |
InvalidArgument |
Empty required argument |
$sdk = new GoPayClient(new Config(
onError: function (\Throwable $e): void {
// Fires before every throw — use for logging/monitoring
$logger->error('GoPay error', ['exception' => $e]);
},
));GoPayClient stores the OAuth2 access token in memory inside a single instance.
In conventional PHP-FPM / mod_php deployments each HTTP request is a new process,
so every new GoPayClient(...) + authenticate() call makes a fresh round-trip to
the GoPay token endpoint (typically 50–200 ms).
Tokens are valid for several minutes. To avoid re-fetching on every request, store the raw token in a shared cache (APCu, Redis, Memcached) and restore it before making API calls:
use GoPay\Payments\GoPayClient;
use GoPay\Payments\Config;
use GoPay\Payments\Environment;
function getGoPayClient(): GoPayClient {
$cacheKey = 'gopay_token_' . md5(CLIENT_ID . SCOPE);
$sdk = new GoPayClient(new Config(environment: Environment::Production));
$cached = apcu_fetch($cacheKey, $success);
if ($success && is_array($cached)) {
// pseudo-code — getHttp() is not yet public; see note below
// $sdk->getHttp()->getTokenStore()->setToken($cached['token'], $cached['expires_in']);
// $sdk->getHttp()->getTokenStore()->setClientCredentials(CLIENT_ID, CLIENT_SECRET, SCOPE);
} else {
$sdk->authenticate(CLIENT_ID, CLIENT_SECRET, SCOPE);
}
return $sdk;
}A first-class
TokenCacheInterface(injectable intoConfig) is planned for a future release. Until then, the pattern above or a singleton per worker process is the recommended approach.
awaitChargeState() uses usleep() and blocks the PHP worker process for up to
$timeoutSeconds (default 30 s). Under concurrent load this can exhaust the worker
pool. Prefer the webhook-driven pattern for production web servers:
- GoPay POSTs a notification to your
notification_url. - Your handler calls
getChargeState()once and records the result. - Return HTTP 200 immediately.
Use awaitChargeState() only in CLI scripts or environments with ample worker headroom.
The PHP SDK handles the server side; the GoPay browser SDK handles the card form in an iframe.
- Browser:
mountCardForm()→ user enters card → iframe submits → returns{ token, card_id }. - Server:
chargePayment($paymentId, ['payment_instrument' => ['payment_instrument' => 'PAYMENT_CARD', 'input' => ['input_type' => 'CARD_TOKEN', 'card_token' => $token]]]).
For browser SDK initialisation, pass the result of getBrowserKeys() to the page:
// Server-side (PHP)
$keys = $sdk->getBrowserKeys();
// $keys = ['shareable_key' => '...', 'client_id' => '...']<!-- Browser page -->
<script>
GoPayBrowserSDK.init({
clientId: '<?= htmlspecialchars($keys['client_id']) ?>',
shareableKey: '<?= htmlspecialchars($keys['shareable_key']) ?>'
});
</script>When the iframe is configured in return-payload mode, it returns a JWE compact serialization string instead of calling /cards/tokens itself. Forward it from the browser to your server, then exchange it for a permanent token:
// Browser posts the JWE payload to your server
$jwePayload = $_POST['payload'];
$card = $sdk->tokenizeEncryptedCard($jwePayload);
// Now charge with the permanent token
$sdk->chargePayment($paymentId, [
'payment_instrument' => [
'payment_instrument' => 'PAYMENT_CARD',
'input' => ['input_type' => 'CARD_TOKEN', 'card_token' => $card->getToken()],
],
]);Runnable scripts in examples/ demonstrate the full payment flow against the GoPay
sandbox. See examples/README.md for setup and usage.
# Run all checks (code style + PHPStan + tests)
composer ci
# Individual steps
composer cs # php-cs-fixer check
composer cs:fix # auto-fix code style
composer phpstan # PHPStan level 10
composer test # PHPUnitThe PHP model classes in src/Generated/ are auto-generated from the GoPay OpenAPI spec. To regenerate them:
# Requires Docker (no local Java/Node needed) and an internet connection
composer codegenThis fetches the latest spec from https://api-docs.gopay.com/spec/en/payments.yaml, runs the OpenAPI generator in Docker, and copies the output into src/Generated/. Review the diff before committing — in particular check that the namespace (GoPay\Payments\Generated\Model) and ModelInterface compatibility are preserved.
Do not edit files in src/Generated/ by hand. If a model class is wrong, fix the upstream spec.
MIT