# GoPay Payments API v4 — full documentation The complete GoPay Payments API v4 documentation as one Markdown file: the integration guides, then the API reference. - One guide as Markdown: `/en/docs/.md`, for example `/en/docs/00-overview.md`. The Czech translation is under `/cs/docs/`. - The API reference as Markdown: `/en/api.md` (Czech: `/cs/api.md`). - The OpenAPI 3.1 specification: `/spec/en/payments.yaml` (Czech: `/spec/cs/payments.yaml`). ## Contents - Overview — `/en/docs/00-overview.md` - Security and credentials — `/en/docs/01-security.md` - Payment states and lifecycle — `/en/docs/02-payment-lifecycle.md` - Hosted integrations — `/en/docs/03-hosted-integrations.md` - Checkout integration — `/en/docs/04-checkout-integration.md` - Saved cards — `/en/docs/05-saved-cards.md` - Apple Pay — `/en/docs/06-apple-pay.md` - Google Pay — `/en/docs/07-google-pay.md` - QR Payment — `/en/docs/08-qr-payment.md` - Refunds — `/en/docs/09-refunds.md` - Payment links — `/en/docs/10-payment-links.md` - Recurrences — `/en/docs/11-recurrences.md` - Events and webhooks — `/en/docs/12-events-and-webhooks.md` - Errors — `/en/docs/98-errors.md` - Payments — `/en/api.md` --- ## Overview GoPay Payments API v4 is the HTTP interface for accepting payments through GoPay. The pages in this set explain how the operations fit together. The OpenAPI reference documents every request and response shape. ### Actors Four parties exchange data during an integration. The pages that follow refer to them by these names. - **Merchant's server.** The merchant's backend. Holds the long-lived merchant credentials and performs everything that must be authenticated as the merchant. - **Merchant's client.** The merchant's web frontend or mobile application. Receives a per-payment credential from the merchant's server when it has to act on a specific payment. - **GoPay client SDK.** Embedded in the merchant's client. Renders the card form, runs Apple Pay and Google Pay, keeps cardholder data outside the merchant's code, and makes the client-side API calls on the merchant's behalf. Distributed as TypeScript, Swift, and Kotlin packages. - **GoPay API.** Reachable at `https://gate.gopay.com/gp-gw/api/4.0` in production and `https://gw.sandbox.gopay.com/gp-gw/api/4.0` in sandbox. Credentials, OAuth 2.0 flows, scopes, and the card-data envelope are covered on the security page. ### API version The version segment in the base URL — `4.0` — identifies the REST contract. It is not the version of this documentation or of any GoPay SDK, and it does not follow semver: it moves by a minor point, `4.0` → `4.1`, and only when a change would break existing integrations. A change that breaks nothing is added to the current version, so a merchant who ignores it keeps working. When a new version is published, both versions run at the same time for a transition period. The previous version is deprecated at the end of that period, with the end date announced in advance. Merchants move to the new base URL at any point within it, and nothing has to change on the day the new version appears. ### Integration shapes Two integration shapes are supported. Each one corresponds to a distinct way the customer interacts with GoPay during checkout. The names are historical. **Checkout.** The merchant owns the checkout UI. The GoPay client SDK collects cardholder data, runs the wallets and, by default, charges the payment and drives it to a final state. Alternatively, the merchant's server charges the payment with the encrypted card the SDK returns. The merchant controls how the checkout looks, including the styling of the card form. Unlike with Redirect, the customer's browser does not load GoPay's hosted gateway, only the SDK and what the payment methods on the merchant's page require. See the checkout integration page. **Redirect.** The customer is sent to GoPay's hosted UX at `Payment Details.gw_url` — GoPay's hosted payment gateway, which offers every payment method the eshop has enabled. It is a destination the customer is sent to and cannot be embedded in the merchant's own checkout. The merchant's server creates the payment; GoPay drives the rest. The outcome is delivered by webhook and is also available by polling the payment; the payment stays fully v4-observable throughout. The merchant's client does not need the GoPay SDK. See the hosted integrations page. Merchants should not mix integration shapes on the same payment — the resulting flows are difficult to troubleshoot. The documented exception is combining a QR option with a card or wallet on the same payment, described on the QR Payment page. Concerns shared across shapes — payment lifecycle, saved cards, wallets, refunds, recurrences, payment links, events and webhooks, errors — have their own pages and are referenced from each shape's page. ### Site requirements Both development and production must run on a hostname over HTTPS. A bare IP address will not do, and `localhost` is not a substitute either — the parts of the integration that match the origin against a domain registered with GoPay, Apple Pay above all, reject it. For local development a self-signed certificate and a hosts-file entry are enough for everything the customer's browser touches — the `return_url` it is redirected to, and the origin Apple Pay matches against. Point a name such as `dev.example.com` at `127.0.0.1` in the hosts file, serve the site over HTTPS with a locally trusted certificate, and register that name with GoPay. Some browser-side pieces do work against `https://localhost`, but the hosts-file name is what also satisfies Apple Pay, so it is the setup worth standing up once. `notification_url` is the exception. GoPay's own servers deliver the notification, so it has to be an HTTPS endpoint they can reach — a hosts-file alias pointing at `127.0.0.1` exists only on the developer's machine. Use a tunnel to the local machine, or a deployed environment, for anything that has to receive notifications or webhooks. The sandbox is more tolerant than production, so a payment created successfully there is no evidence that the same request will be accepted in production, where a `return_url` that is not HTTPS is rejected with `403`. That check is on the scheme alone — passing it does not exempt an integration from the hostname requirement above. Apple Pay is stricter than the API itself and does not work without a registered HTTPS origin — see the Apple Pay page. A checkout page that sends a Content-Security-Policy must allow the GoPay hosts and the wallet hosts. The list is on the checkout integration page. ### Client SDKs The GoPay client SDK is published as three packages. - TypeScript (browser) — `@gopaycz/gopay-js-sdk-browser` · `npm install @gopaycz/gopay-js-sdk-browser` - Swift — [`GopaySDK`](https://github.com/gopaycommunity/gopay-ios-sdk) · Swift Package Manager `https://github.com/gopaycommunity/gopay-ios-sdk.git` or CocoaPods `pod 'GopaySDK'` - Kotlin — [`cz.gopay:sdk`](https://github.com/gopaycommunity/gopay-android-sdk) · Maven Central `implementation("cz.gopay:sdk:")` A checkout built on the browser package also runs in mobile browsers. The Swift and Kotlin packages are for native mobile applications, so a merchant without a native application does not need them. The Swift package requires iOS 13 or later, and the Kotlin package requires Android 7.0 (API level 24) or later. Each package renders the card form, charges the payment, handles the 3-D Secure step, and reads the payment and its QR code. With the Swift and Kotlin packages, the application polls the charge state itself. Apple Pay is available in the browser and Swift packages, and Google Pay in the browser and Kotlin packages. Per-language examples are on the checkout integration page and on the saved cards, Apple Pay, and Google Pay pages. ### Server-side libraries GoPay publishes server-side libraries for PHP and TypeScript. - PHP — [`gopaycommunity/gopay-php-api-v4`](https://github.com/gopaycommunity/gopay-php-api-v4) · `composer require gopaycommunity/gopay-php-api-v4` - TypeScript (server) — `@gopaycz/gopay-js-sdk` · `npm install @gopaycz/gopay-js-sdk` The PHP library requires PHP 8.1 or later and works with any PSR-18 HTTP client, such as Guzzle. For other languages, a server-side library may be generated from the OpenAPI specification using the [OpenAPI Generator](https://openapi-generator.tech/). The generated client mirrors the spec exactly and is regenerated whenever the spec changes. The examples below produce client libraries for Python, C#, and Java using the OpenAPI Generator Docker image. Generator-specific options may be adjusted from the per-generator documentation linked from each section. #### Python See the [Python generator documentation](https://openapi-generator.tech/docs/generators/python/) for the full option set. ```shell docker run --rm \ -v "${PWD}:/local" \ openapitools/openapi-generator-cli generate \ -i /local/Payments.yaml \ -g python \ -o /local/payments-python-client \ --additional-properties="\ library=asyncio,\ generateSourceCodeOnly=true,\ useOneOfDiscriminatorLookup=true,\ disallowAdditionalPropertiesIfNotPresent=false,\ enumUnknownDefaultCase=true,\ projectName=payments-client" ``` #### C\# See the [C# generator documentation](https://openapi-generator.tech/docs/generators/csharp/) for the full option set. ```shell docker run --rm \ -v "${PWD}:/local" \ openapitools/openapi-generator-cli generate \ -i /local/Payments.yaml \ -g csharp \ -o /local/payments-csharp-client \ --additional-properties="\ library=restsharp,\ equatable=true,\ netCoreProjectFile=true,\ nullableReferenceTypes=true,\ useOneOfDiscriminatorLookup=true,\ disallowAdditionalPropertiesIfNotPresent=false,\ packageName=GoPay.Payments" ``` #### Java See the [Java generator documentation](https://openapi-generator.tech/docs/generators/java/) for the full option set. ```shell docker run --rm \ -v "${PWD}:/local" \ openapitools/openapi-generator-cli generate \ -i /local/Payments.yaml \ -g java \ -o /local/payments-java-client \ --additional-properties="\ library=native,\ serializationLibrary=jackson,\ useJackson3=true,\ useJakartaEe=true,\ useOneOfInterfaces=true,\ useOneOfDiscriminatorLookup=true,\ disallowAdditionalPropertiesIfNotPresent=false,\ enumUnknownDefaultCase=true,\ legacyDiscriminatorBehavior=false,\ generateBuilders=true,\ generateConstructorWithAllArgs=true,\ groupId=com.gopay,\ artifactId=payments-client,\ invokerPackage=com.gopay.payments" ``` The output directory contains a generated `README.md` describing how to consume the resulting client. ### Operational contact Some settings on GoPay's side are not exposed through the API and are changed by emailing `integration@gopay.com`. The most common ones are Apple Pay domain registration, Google Pay merchant identifier submission, recurrence `ON_DEMAND` throttle adjustments, the default payment timeout per eshop, the notification endpoint and the events an eshop receives, sandbox provisioning, and tokenisation enablement. Each topic page links back here. ### Glossary - `goid` — identifier of an eshop within the merchant's account, carried in `/eshops/{goid}/…` paths. - `payment_secret` — per-payment credential returned in `Payment Details`, which the merchant's server passes to the GoPay client SDK. See the security page. - `shareable_key` — client-side key paired with `client_id` and configured in the GoPay client SDK. It is safe to embed in client builds. - JWE — JSON Web Encryption envelope in which the GoPay client SDK carries an encrypted card. It is single-use and valid for ten minutes. - card token — permanent token that stands in for a saved card and is charged as a `CARD_TOKEN` input. See the saved cards page. - network token — token issued by the card scheme in place of the card number. Its masked form is `masked_virtual_pan` on a saved card. - `fingerprint` — value on a saved card that stays the same across tokenisations of the same physical card. - payment instrument — the means that settles a payment, carried in `payment_instrument` on the charge request. Today it is always a payment card. - PCI DSS SAQ-A / SAQ-D — the card-industry self-assessment levels that apply when the GoPay client SDK collects card data (SAQ-A), or when the merchant's own code does (SAQ-D). See the security page. --- ## Security and credentials Every authenticated call against the GoPay Payments API is made by one of two actors: the merchant's server, or the merchant's client (a browser frontend or a mobile application) through the GoPay client SDK. Each actor authenticates differently, and the two are not interchangeable. A third concern — getting cardholder data into a charge call without the merchant's code ever seeing it — is handled by a separate envelope and is covered in the last section. ### Credentials Five credentials cover every authenticated call. Each one is introduced below at the point it is used; the list here is a quick reference. - `client_id` — public identifier of the merchant. - `client_secret` — server-side secret paired with `client_id`. Format: `cs_` followed by 32 alphanumeric characters, for example `cs_4f9a2b7e6d8c1a3e5b9d2f7c8a4e6b1d`. - `shareable_key` — client-side key paired with `client_id`, configured in the GoPay client SDK. Format: `sk_` followed by 32 alphanumeric characters, for example `sk_2d8e7c1a4f9b3e5d6a2c8f7b9e4d1a3c`. - `payment_secret` — per-payment credential returned on payment creation, which the merchant's server passes to the GoPay client SDK. Format: `ps_` followed by 32 alphanumeric characters, for example `ps_6c1a9f4e7d2b8c3a5e9d6f2c1a4b7e8d`. - `goid` — eshop identifier within the merchant. Carried as a path parameter on `/eshops/{goid}/…`. ### Server-side The merchant's server holds the long-lived merchant credentials and may call any operation in the API except those that are explicitly client-only. It is the only actor that authenticates as the merchant itself. #### Storage `client_secret` must stay on the merchant's server. It must not appear in client builds, in version control, in log output, or in any file reachable from the browser. `client_id` is public and may appear in client builds and browser-reachable code. #### Obtaining an access token The merchant's server authenticates itself by exchanging its credentials for a JSON Web Token. The exchange happens at `POST /oauth2/token`, using the `client_credentials` grant. The request is form-encoded. `client_id:client_secret` is presented as HTTP Basic; the requested scopes are listed in the body. ```http POST /gp-gw/api/4.0/oauth2/token HTTP/1.1 Host: gate.gopay.com Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ= Content-Type: application/x-www-form-urlencoded grant_type=client_credentials&scope=payment:read%20payment:write ``` At least one scope must be requested. The response is an `Access Token`, whose `access_token` field is a JWT. The available scopes are: - `payment:read` — reads on payments, charges, refunds, recurrences, and payment links. - `payment:write` — creating, charging, canceling, modifying, and refunding payments, and managing recurrences and payment links. - `card:read` — reads on saved card tokens. - `card:write` — saving and deleting card tokens. - `event:read` — reads on the eshop's event log. - `shared:read` — public resources, such as the keys that verify webhook signatures. #### Using the access token The JWT is presented on every subsequent API call as a Bearer token in the `Authorization` header. ```http GET /gp-gw/api/4.0/payments/123456 HTTP/1.1 Host: gate.gopay.com Authorization: Bearer eyJraWQiOiJzaWduLTIwMjYtMDIiLCJ0eXAiOiJKV1QiLCJhbGciOi… ``` The JWT is valid for fifteen minutes. When the JWT expires the merchant's server obtains a new one by repeating the exchange. The full claim set is documented on the `JWT` schema in the OpenAPI reference and includes `sub`, `scope`, `iat`, `exp`, and `domain`. The claims may be read — reading `exp` to schedule refresh and `scope` to confirm what was granted is supported and recommended. #### Reach of the merchant access token The merchant access token unlocks every operation. Operations grouped under SDK Services in the OpenAPI reference are called by the GoPay client SDKs; the merchant's server does not need them. The merchant's server also receives the per-payment credential that the merchant's client uses. ### Client-side The merchant's client cannot hold `client_secret`. It works through the GoPay client SDK, which holds the two client-side credentials and makes the client-side calls with them. #### Configuring the SDK `shareable_key` is a static credential issued to the merchant alongside `client_id`. It is safe to embed in published application builds and browser-reachable JavaScript. The merchant's client initialises the GoPay client SDK with `client_id` and `shareable_key` once, at startup. `shareable_key` alone cannot read or change payments. #### Handing the payment to the SDK `payment_secret` gives the GoPay client SDK access to one payment. It is created by GoPay when the payment is created and returned to the merchant's server in `Payment Details.payment_secret`, next to the payment's `id`. 1. The merchant's server delivers `id` and `payment_secret` to the merchant's client over its own authenticated HTTPS channel, such as an API response or server-rendered HTML. 2. The merchant's client passes both values to the SDK: `attachPayment()` in the browser, `startPaymentSession()` in the iOS and Android applications. 3. The SDK obtains a payment-scoped access token with them and renews it when needed. The merchant's code does not handle that token. The payment-scoped token carries one or both of two scopes: - `payment:read` — reading the payment. - `payment:charge` — charging the payment. `payment_secret` must not be stored, logged, or embedded in a URL. A URL ends up in browser history, access logs and analytics, so a page that needs the payment again — such as the page the customer returns to after 3-D Secure — receives a fresh delivery from the merchant's server. `payment_secret` does not expire. What the SDK may do with it depends on the state of the payment: an operation that the current state no longer allows, such as charging a payment that is already paid, is rejected, mostly with `409`. See the errors page. #### Reach of the client-side credentials Through the SDK, the merchant's client only ever calls operations that are reachable with `shareable_key` or `payment_secret`. All other operations — payment creation, issuing refunds, managing recurrences and payment links, saved-card management — are reserved for the merchant's server. #### Flow The end-to-end client-side flow from a payment creation to a status read. ```mermaid sequenceDiagram autonumber participant MS as Merchant's server participant API as GoPay API participant MC as Merchant's client participant SDK as GoPay client SDK MC->>SDK: initialise with client_id and shareable_key MS->>API: POST /oauth2/token (client_credentials) API-->>MS: merchant access token MS->>API: POST /eshops/{goid}/payments API-->>MS: Payment Details (id, payment_secret) MS->>MC: id, payment_secret MC->>SDK: attach the payment (id, payment_secret) SDK->>API: obtain payment-scoped access SDK->>API: GET /payments/{id} API-->>SDK: Payment Details ``` ### Card data encryption Cardholder data never reaches GoPay's API as plaintext. The GoPay client SDK produces a JWE (a JSON Web Encryption envelope) that wraps the encrypted card. The merchant's code receives the JWE only when it has to pass it on: to the SDK's charge call in the iOS and Android applications, or to the merchant's server when the server tokenises the card or charges the payment. In the browser, a card form that charges keeps the JWE to itself. The card-data flows are described on the checkout integration and saved cards pages; the constraints that bind the merchant are listed here. - The JWE is valid for ten minutes from production. - The JWE is single-use. It may be spent on one charge or one tokenisation, never both, never twice. - The JWE must not be stored beyond the immediate call that consumes it, must not be logged, and must not be cached. ### PCI DSS scope The PCI DSS scope of a v4 integration depends on whether the merchant's code ever sees cardholder data. - If the GoPay client SDK collects and encrypts cardholder data — the merchant's code only handles the resulting JWE or card token — the integration qualifies for SAQ-A. - If the merchant's code collects cardholder data directly and constructs the JWE itself instead of using the GoPay client SDK, the integration is in SAQ-D scope. Direct collection is supported only for merchants already operating under SAQ-D. --- ## Payment states and lifecycle The payment is the central object of the GoPay Payments API. It carries an amount, a currency, a customer, and callback URLs; it has a state that progresses from creation to a terminal outcome; and it owns a charge sub-process that runs whenever a payment instrument attempts to settle it. This page documents the state catalogue, the charge sub-process, the cancel operation, the polling and notification mechanisms, and the per-method timeouts. ### Creating a payment A payment is created with `POST /eshops/{goid}/payments`. The body is a `Payment Create Request`; the full shape is in the OpenAPI reference. The merchant's server authenticates with a merchant access token holding the `payment:write` scope; see the security page. The response is a `Payment Details` object. Three fields drive every subsequent step. - `id` — the payment identifier used in every operation on this payment. - `gw_url` — the address of GoPay's hosted payment gateway, offering every payment method the eshop has enabled. It cannot be embedded in the merchant's own checkout. Used by the Redirect integration shape; see the hosted integrations page. - `payment_secret` — the per-payment credential the merchant's server passes to the GoPay client SDK, so that the SDK can act on this payment. See the security page. The newly created payment is in the `CREATED` state. From this point the customer is either sent to the hosted gateway at `gw_url` or pays through a checkout integration, where the GoPay client SDK or the merchant's server charges it. ### Payment states The lifecycle of a payment is described by `Payment State`. - `CREATED` — the payment exists and is awaiting a charge attempt. No instrument has been engaged. - `PAYMENT_METHOD_CHOSEN` — a charge attempt is in flight; the outcome is not yet known. The name is historical and is best read as "pending". - `PAID` — the payment was charged successfully and the funds were authorised. - `CANCELED` — the customer abandoned the payment, the charge was rejected at authorisation, or the merchant canceled the payment before it was charged. - `TIMEOUTED` — the payment did not reach a terminal outcome within the applicable timeout. The per-instrument values are listed below. - `REFUNDED` — the payment was fully refunded. See the refunds page. - `PARTIALLY_REFUNDED` — part of the payment was refunded. A payment may move through `PARTIALLY_REFUNDED` more than once before reaching `REFUNDED`. See the refunds page. - `AUTHORIZED` — only relevant for the pre-authorisation flow. ```mermaid stateDiagram-v2 [*] --> CREATED CREATED --> PAYMENT_METHOD_CHOSEN CREATED --> CANCELED CREATED --> TIMEOUTED PAYMENT_METHOD_CHOSEN --> AUTHORIZED PAYMENT_METHOD_CHOSEN --> PAID PAYMENT_METHOD_CHOSEN --> CANCELED PAYMENT_METHOD_CHOSEN --> TIMEOUTED AUTHORIZED --> PAID AUTHORIZED --> CANCELED PAID --> REFUNDED PAID --> PARTIALLY_REFUNDED PARTIALLY_REFUNDED --> PARTIALLY_REFUNDED PARTIALLY_REFUNDED --> REFUNDED ``` ### Charging a payment This section applies to the checkout integration. For Redirect, the hosted gateway charges the payment itself; the merchant does not call the charge endpoint. See the hosted integrations page. A charge is the sub-process that attempts to settle a payment with a specific instrument. It is initiated with `POST /payments/{payment_id}/charge`. The body is a `Payment Charge Input` carrying the instrument. Only card payments are charged through the API today: a new card, a saved card, or an Apple Pay or Google Pay payment. See the checkout integration page. Other payment methods (online banking, BNPL, PayPal, mobile payments, crypto) are reachable only through the hosted gateway. The charge is asynchronous. The response is a `Payment Charge Response` whose `state` is one of: - `REQUESTED` — the charge has been accepted and is queued. - `PROCESSING` — the charge is in flight with the payment processor. - `ACTION_REQUIRED` — the customer must complete an external step. The response carries an `action` describing what is required. The action shapes and their handling are documented on the integration page for the instrument that produced them; for card payments, see the checkout integration page. - `SUCCEEDED` — the charge completed successfully. The payment is `PAID`. - `FAILED` — the charge did not succeed. The reason is delivered in `Payment Charge Status Response.fail_reason`. A charge reaches a terminal state at `SUCCEEDED` or `FAILED`. In a checkout integration, the GoPay client SDK follows the charge to that state: the browser package on its own, the iOS and Android packages through the payment session's charge-state read and 3-D Secure call, which the application drives. A payment is charged once. A second `POST /payments/{payment_id}/charge` against the same payment fails; no idempotency mechanism is exposed, see the errors page. A charge rejected at authorisation moves the payment to `CANCELED`. If the customer tries again, the merchant's server creates a new payment and hands it to the merchant's client in the same way as the first. ### Canceling a payment A payment that has not been charged can be canceled with `DELETE /payments/{payment_id}`. The merchant's server authenticates with a merchant access token holding the `payment:write` scope. A successful call returns `204`. The payment is not deleted. Its state moves to `CANCELED` and the payment stays readable through `GET /payments/{payment_id}`. Only a payment in `CREATED` can be canceled. Any other state returns `409` and leaves the payment unchanged. See the errors page. The state decides this, not the charge attempt. A charge rejected before it engages a payment instrument — one carrying an invalid card token, for example — leaves the payment in `CREATED`, so it can still be canceled. Once a charge moves the payment to `PAYMENT_METHOD_CHOSEN`, the outcome belongs to the charge sub-process. ### Polling and notifications The charge's terminal state is observed in two ways. `GET /payments/{payment_id}/charge` returns the current `Payment Charge Status Response`. The recommended polling cadence is one second. Polling continues until `state` reaches `SUCCEEDED`, `FAILED` or `ACTION_REQUIRED`. In the browser, the GoPay client SDK polls. In the iOS and Android applications, the application polls through the payment session. The merchant's server polls only when it reads the charge itself. The merchant's server is also notified through a webhook. When a charge reaches a terminal state, the payment produces an event — `payments.paid`, `payments.canceled` or `payments.timeouted` — and GoPay `POST`s it to the notification endpoint configured for the eshop. The merchant's server acknowledges with any `2xx` and re-fetches the payment. The event shape, the delivery contract and the retry behaviour are on the events and webhooks page. The webhook is the authoritative signal for server-side flows; polling is the natural choice when the merchant's client is waiting in-page for a charge to terminate. ### Return URL and notification URL Two URLs carry the payment outcome back to the merchant. They are not interchangeable. Both are supplied at payment creation on `Payment Callback`. `return_url` is where the customer's browser is sent at the end of the hosted flow. The redirect is a `GET` carrying `id` (and, for recurring children, `parent_id`) as query parameters. The body is empty. The merchant's server re-fetches the payment with `GET /payments/{payment_id}` to learn the outcome. In a checkout integration, the customer's browser is also sent to `return_url` after the 3-D Secure step. The GoPay client SDKs for iOS and Android watch for this address to close their 3-D Secure screen, so `return_url` must not carry a fragment (`#…`). Both URLs must be absolute HTTPS URLs with a hostname. They point at the merchant's site, which the site requirements on the overview page cover. A merchant with an iOS or Android application and no website uses the SDK's return address as `return_url`, as described on the checkout integration page. `notification_url` must additionally be reachable by GoPay's servers, which a local hosts-file alias is not. In production a `return_url` that is not HTTPS is rejected with `403` when the payment is created; the sandbox is more tolerant. `notification_url` receives a notification when the state of the payment changes: a `GET` request carrying the payment `id` as a query parameter, with an empty body. The merchant's server re-fetches the payment to learn the new state. Webhooks do not go to `notification_url`; they are sent to the endpoint configured for the eshop, as described on the events and webhooks page. The merchant's server should treat the webhook as the authoritative trigger. ### Timeouts When a payment is created, an initial timeout starts. If no charge is attempted before it expires, the payment transitions to `TIMEOUTED`. The default is one hour. The default may be adjusted per eshop through `integration@gopay.com`. Once a charge is attempted, a second, instrument-specific timeout takes over. This timeout is independent of how much of the initial window had elapsed. The per-instrument values are: | Instrument | Timeout | | --- | --- | | Card payment (any card input type) | 1 hour | | Online bank payment | 1 to 7 days, depending on the bank | | PSD2 bank payment (PIS) | 1 hour | | BNPL Twisto | 4 hours | | BNPL Skip Pay | 13 hours | | PayPal | 1 hour | | Mobile payment / Premium SMS | 1 hour | | Crypto | 48 hours | Requesting a QR code for a payment extends the initial timeout to four days. The payment state is not changed by the request. See the QR Payment page. --- ## Hosted integrations Hosted integrations hand the payment over to GoPay's hosted gateway, which offers the customer every payment method the eshop has enabled. The gateway is a destination the customer is sent to — it cannot be embedded in the merchant's own checkout. A merchant who needs the payment step inside their own UI can use the checkout integration, which today covers card, Google Pay, Apple Pay and QR payments and is being extended to further methods; every method the eshop has enabled is already available here on the gateway. In a hosted integration, the customer is sent to the GoPay-hosted payment UX to choose a payment method and complete the payment. The merchant's code never collects card data, never handles a wallet payload, and never calls the charge endpoint. ### Flow 1. The merchant's server obtains a merchant access token with the `payment:write` scope. See the security page. 2. The merchant's server creates the payment with `POST /eshops/{goid}/payments`. The body is a `Payment Create Request`; `Payment Callback.return_url` and `Payment Callback.notification_url` must be supplied. See the payment lifecycle page. 3. The merchant's server reads `Payment Details.gw_url` from the response and uses it to send the customer to the gateway, as described below. 4. The customer is taken to the gateway, picks a payment method, and completes the payment. At the end, the customer's browser is redirected to `return_url` with the payment `id` (and `parent_id` for recurring children) as query parameters. 5. The merchant's server reacts to the `payments.*` event delivered as a webhook and re-fetches the payment with `GET /payments/{payment_id}` to learn the new state. The webhook is the authoritative trigger; the return-URL handling is for the customer-facing page. See the events and webhooks page. The merchant should not call `POST /payments/{payment_id}/charge` for hosted payments. The hosted gateway charges the payment itself. ### Redirect The customer's browser is sent to `Payment Details.gw_url` with a normal full-page navigation. The customer leaves the merchant's site for the duration of the payment and returns through `return_url`. Three techniques are equivalent at the API level. The choice is a matter of how the merchant's server hands the URL to the browser. - HTML link. The merchant's server renders a page containing `…`. - HTTP redirection. The merchant's server returns a `302 Found` with `Location: `. - Client-side JavaScript. The merchant's client receives `gw_url` from its server (for example through an AJAX call) and assigns it to `window.location`. ```html Pay ``` ```http HTTP/1.1 302 Found Location: https://gate.gopay.com/gw/300000001 ``` ### Hosted gateway in a mobile application A merchant whose checkout runs inside a native mobile application should open `gw_url` in a system browser component — Chrome Custom Tabs on Android and `SFSafariViewController` on iOS. In-app web views (Android `WebView`, iOS `WKWebView`) do not run a full browser environment, so online banking redirects may not behave correctly. A merchant with a native checkout UI on mobile can use the checkout integration instead as far as it reaches — card, Google Pay, Apple Pay and QR today; anything beyond that still goes through `gw_url` in a system browser component. ### Sequence ```mermaid sequenceDiagram autonumber participant MS as Merchant's server participant MC as Merchant's client participant GW as GoPay hosted gateway participant API as GoPay API MS->>API: POST /eshops/{goid}/payments API-->>MS: Payment Details (id, gw_url) MS->>MC: gw_url MC->>GW: open gw_url Note over MC,GW: customer picks a method and completes the payment GW-->>MC: return_url with id API-->>MS: webhook with payments.* event MS->>API: GET /payments/{id} API-->>MS: Payment Details (terminal state) ``` --- ## Checkout integration In a checkout integration, the merchant owns the checkout UI and the GoPay client SDK takes the payment inside it. The SDK collects card data, runs the wallets, and charges the payment, including the 3-D Secure step. Cardholder data passes through the SDK only, never through merchant-written code, so the integration remains within PCI DSS SAQ-A scope. The hosted gateway is not used. Saving cards is covered on the saved cards page, the wallet setup on the Apple Pay and Google Pay pages, and the QR option offered next to the card form on the QR payment page. ### Flow The checkout follows the same steps whichever way the customer pays. 1. The merchant's server creates the payment with `POST /eshops/{goid}/payments`, authenticated with a merchant access token holding the `payment:write` scope. See the payment lifecycle page. 2. The merchant's server hands `Payment Details.id` and `Payment Details.payment_secret` to the merchant's client. See the security page. 3. The merchant's client attaches the payment to the GoPay client SDK. 4. The customer pays with a new card in the SDK card form, with a saved card, or with a wallet. 5. The SDK charges the payment and handles the 3-D Secure step if the card issuer requires one. 6. The merchant's client reads the outcome from the SDK. The merchant's server reacts to the `payments.*` event delivered as a webhook, which is the authoritative signal. See the events and webhooks page. ```mermaid sequenceDiagram autonumber participant MS as Merchant's server participant MC as Merchant's client participant SDK as GoPay client SDK participant API as GoPay API MS->>API: POST /eshops/{goid}/payments API-->>MS: Payment Details (id, payment_secret) MS->>MC: id, payment_secret MC->>SDK: attach the payment Note over MC,SDK: customer pays with a new card, a saved card or a wallet SDK->>API: POST /payments/{id}/charge API-->>SDK: Payment Charge Response opt state = ACTION_REQUIRED SDK->>MC: present the 3-D Secure step Note over MC: in the browser, the outcome is then read on the return_url page end loop until SUCCEEDED or FAILED SDK->>API: GET /payments/{id}/charge API-->>SDK: Payment Charge Status Response end SDK-->>MC: charge outcome API-->>MS: webhook with payments.* event MS->>API: GET /payments/{id} API-->>MS: Payment Details (terminal state) ``` The SDK may also hand the encrypted card back instead of charging, so that the merchant's server charges the payment. See Charging from the merchant's server below. A payment is charged once. If the charge ends in `FAILED`, a further attempt by the customer needs a new payment, handed to the merchant's client and attached in the same way. See the payment lifecycle page. ### SDK initialisation The merchant's client initialises the GoPay client SDK with `client_id` and `shareable_key` once, at application startup. Before a payment is charged, the merchant's client attaches it: it passes `Payment Details.id` and `Payment Details.payment_secret` to `attachPayment()` in the browser, or to `startPaymentSession()` in the iOS and Android applications. The SDK obtains the access it needs for that payment itself. See the security page. In the browser, the SDK works on one payment at a time. A later `attachPayment()` replaces the earlier payment. While a card form is mounted, `attachPayment()` is refused with `INVALID_ARGUMENT`, so the merchant's client calls `unmount()` on the card form before it attaches another payment. In the iOS and Android applications, the SDK keeps one payment session per payment, and sessions for several payments may be open at once. A second session for the same payment is refused while the first one is open. The look of the card form — colours, fonts and borders — is set through a theme when the form is created. The Swift and Kotlin card forms accept a subset of the browser theme's keys. In the browser, `setTheme()` changes the theme of a card form that is already mounted, and the card form accepts font names only, not font files or URLs, so only typefaces installed on the customer's device are available. #### Browser: initialise and attach a payment ```typescript import { createGoPayBrowserSDK } from '@gopaycz/gopay-js-sdk-browser'; // Initialise once with keys obtained server-side via serverSdk.getBrowserKeys() const sdk = createGoPayBrowserSDK({ environment: 'production', shareableKey: 'sk_live_…', clientId: 'your-client-id', }); // Before any payment-scoped operation, attach the payment. // paymentId and paymentSecret come from serverSdk.createPayment() on your server. await sdk.attachPayment({ paymentId: payment.id, paymentSecret: payment.payment_secret, }); ``` #### iOS: initialise and start a payment session ```swift import GopaySDK // Initialise once at app start. clientId and shareableKey are safe to embed in the app. GopaySDK.shared.initialize( with: GopaySDKConfig( environment: .production, clientId: "your-client-id", shareableKey: "sk_live_…" ) ) // Before any payment-scoped operation, start a session for the payment. // paymentId and paymentSecret come from serverSdk.createPayment() on your server. let session = try await GopaySDK.shared.startPaymentSession( paymentId: payment.id, paymentSecret: payment.paymentSecret ) // When the checkout is finished, close() wipes paymentSecret from memory. await session.close() ``` #### Android: initialise and start a payment session ```kotlin import cz.gopay.sdk.GopaySDK import cz.gopay.sdk.config.Environment import cz.gopay.sdk.config.GopayConfig // Initialise once in Application.onCreate(). clientId and shareableKey are safe to embed in the app. GopaySDK.initialize( GopayConfig( environment = Environment.PRODUCTION, clientId = "your-client-id", shareableKey = "sk_live_…" ) ) // Before any payment-scoped operation, start a session for the payment. // paymentId and paymentSecret come from serverSdk.createPayment() on your server. val session = GopaySDK.getInstance().startPaymentSession( paymentId = payment.id, paymentSecret = payment.paymentSecret ) // When the checkout is finished, close() wipes paymentSecret from memory. session.close() ``` ### Charging with a new card The customer enters the card in the SDK card form, and the SDK charges the payment with it. The card reaches the charge as an `ENCRYPTED_CARD` input: a JWE (a JSON Web Encryption envelope) produced inside the SDK. Because the SDK makes the charge, it also handles the 3-D Secure step and follows the charge to `SUCCEEDED` or `FAILED`. - In the browser, the card form is mounted with `flow: 'direct-charge'`. The SDK charges as soon as the customer submits the form, and the card form's `result` carries the outcome, unless a 3-D Secure redirect takes the customer off the page first. - In the iOS and Android applications, the card form returns the JWE to the application, which passes it straight to the payment session's charge call. The application then follows the charge with the polling loop shown below. The card is not saved. Saving a card during checkout is covered on the saved cards page. #### Browser: card form, charge, and polling ```typescript // Render the card form, charge, and poll until terminal state (direct-charge flow). // Requires sdk.attachPayment() to have been called first. // If 3DS is required, the SDK sends the whole page to the issuer (default redirect mode): // ctrl.result then never settles, and the outcome is read on the return_url page. const container = document.getElementById('card-form-container')!; const ctrl = await sdk.mountCardForm(container, { flow: 'direct-charge' }); // Without a 3DS redirect: resolves with PaymentChargeStatusResponse on SUCCEEDED; // rejects with GoPaySDKError on CHARGE_FAILED or CHARGE_TIMEOUT. const result = await ctrl.result; ``` #### iOS: card form, charge, and polling ```swift import SwiftUI import GopaySDK // Drives a charge to SUCCEEDED or FAILED. Each new 3DS redirect is presented // in the SDK's web view, and polling continues afterwards. func settle(_ session: PaymentSession, _ first: ChargePaymentResponse) async throws -> ChargePaymentResponse { var charge = first var presented = Set() for _ in 0..<30 { if charge.state == .succeeded || charge.state == .failed { break } if let redirect = charge.action?.redirectUrl, let url = URL(string: redirect), presented.insert(redirect).inserted { try await session.handle3dsVerification( redirectURL: url, returnURL: charge.returnUrl.flatMap(URL.init(string:)) ) } try await Task.sleep(nanoseconds: 1_500_000_000) charge = try await session.getChargeState() } return charge // still in progress after ~45 s — the webhook carries the final state } // Render the card form, charge, and poll until terminal state. // Requires a session from GopaySDK.shared.startPaymentSession(). struct CardCheckoutView: View { let session: PaymentSession @State private var isCardValid: Bool? var body: some View { VStack { GopayCardForm(validation: .live, isValid: $isCardValid) Button("Pay") { Task { // The card form returns a JWE; card data never leaves the SDK. let jwe = try await GopaySDK.shared.submitCardForm() let request = ChargePaymentRequest.encryptedCard( jwe, browserData: await BrowserData.deviceDefault() ) let result = try await settle(session, session.charge(request)) } } .disabled(isCardValid != true) } } } ``` #### Android: card form, charge, and polling ```kotlin import android.app.Activity import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import cz.gopay.sdk.model.ChargePaymentResponse import cz.gopay.sdk.model.ChargeState import cz.gopay.sdk.session.PaymentSession import cz.gopay.sdk.ui.CardEncryptionResult import cz.gopay.sdk.ui.PaymentCardForm import kotlinx.coroutines.delay import kotlinx.coroutines.launch // Drives a charge to SUCCEEDED or FAILED. Each new 3DS redirect is presented // in the SDK's WebView, and polling continues afterwards. suspend fun settle(activity: Activity, session: PaymentSession, first: ChargePaymentResponse): ChargePaymentResponse { var charge = first val presented = mutableSetOf() repeat(30) { if (charge.state == ChargeState.SUCCEEDED || charge.state == ChargeState.FAILED) return charge val redirect = charge.action?.redirectUrl if (redirect != null && presented.add(redirect)) { session.handle3dsVerification(activity, redirect, charge.returnUrl) } delay(1_500) charge = session.getChargeState() } return charge // still in progress after ~45 s — the webhook carries the final state } // Render the card form, charge, and poll until terminal state. // Requires a session from GopaySDK.getInstance().startPaymentSession(). @Composable fun CardCheckout(session: PaymentSession, onResult: (ChargePaymentResponse) -> Unit) { val activity = LocalContext.current as Activity val scope = rememberCoroutineScope() var submit by remember { mutableStateOf<(suspend () -> CardEncryptionResult)?>(null) } PaymentCardForm(onEncryptionComplete = {}, onFormReady = { submit = it }) Button(onClick = { scope.launch { // The card form returns a JWE; card data never leaves the SDK. val jwe = (submit?.invoke() as? CardEncryptionResult.Success)?.jwe ?: return@launch val charge = session.chargeWithEncryptedCard(activity, jwe) onResult(settle(activity, session, charge)) } }) { Text("Pay") } } ``` ### Charging with a saved card A card saved earlier is charged without the card form. The merchant's server reads the customer's card token from its own records and hands it to the merchant's client, and the SDK charges the payment with it as a `CARD_TOKEN` input. As with a new card, the SDK handles the 3-D Secure step and follows the charge to `SUCCEEDED` or `FAILED`. Card tokens are created on the merchant's server; see the saved cards page. #### Browser: charge with a saved card token ```typescript // Charge with a saved card token — no card form interaction required. // Requires sdk.attachPayment() to have been called first. await sdk.chargePayment({ payment_instrument: { payment_instrument: 'PAYMENT_CARD', input: { input_type: 'CARD_TOKEN', card_token: 'J7HjFNwzyBOHS+jwIMMktubTwoIRy6qB/4opvjGcKtjv…', }, }, }); // Poll until SUCCEEDED or FAILED. If 3DS is required, the SDK sends the whole page // to the issuer (default redirect mode), and the outcome is read on the return_url page. const result = await sdk.awaitChargeState(); ``` #### iOS: charge with a saved card token ```swift // Charge with a saved card token — no card form interaction required. // The token comes from your server, which stored it when the card was saved. // Requires a session from GopaySDK.shared.startPaymentSession(). let request = ChargePaymentRequest.cardToken( "J7HjFNwzyBOHS+jwIMMktubTwoIRy6qB/4opvjGcKtjv…", browserData: await BrowserData.deviceDefault() ) let result = try await settle(session, session.charge(request)) ``` #### Android: charge with a saved card token ```kotlin // Charge with a saved card token — no card form interaction required. // The token comes from your server, which stored it when the card was saved. // Requires a session from GopaySDK.getInstance().startPaymentSession(). val charge = session.chargeWithCardToken( activity = activity, cardToken = "J7HjFNwzyBOHS+jwIMMktubTwoIRy6qB/4opvjGcKtjv…" ) val result = settle(activity, session, charge) ``` ### Wallets Apple Pay and Google Pay enter the checkout through the SDK. The SDK runs the wallet's payment sheet and charges the payment with the result. In the browser it also follows the charge to a terminal state; in the iOS and Android applications, the application follows the charge in the same way as a card charge. The merchant's code does not handle the wallet payload and does not call the charge endpoint for a wallet payment. A card paid with a wallet cannot be saved for later reuse. The wallet setup, the availability checks and the per-platform examples are on the Apple Pay and Google Pay pages. ### 3-D Secure The card issuer may require the customer to confirm a card payment in a 3-D Secure step. The SDK detects the step and presents it. The merchant supplies the challenge preference and the `return_url`, and in the browser also the page the customer returns to. A completed 3-D Secure step does not mean that the charge succeeded. Authorisation runs after the step and may still fail; only `SUCCEEDED` confirms the payment. #### Challenge preference `Payment Card Charge Data.challenge_preference` carries the merchant's preference and accepts three values. - `AUTO` — GoPay decides whether a challenge is required. This is the default when the field is omitted. - `CHALLENGE_PREFERRED` — the merchant asks for strong customer authentication. The preference may still be ignored in some situations, for example for Apple Pay and some Google Pay payments. - `NO_CHALLENGE_PREFERRED` — the merchant asks to skip the challenge. This is not a guarantee; the card issuer can still require a challenge. The merchant passes the preference on the charge call: `challenge_preference` in the browser's `chargePayment()`, `challengePreference` on the charge calls of the iOS and Android payment session. The preference applies to every card input. #### In the browser By default, the SDK sends the whole page to the issuer's 3-D Secure page. The page unloads, so the result the merchant's client was waiting for never settles. When the step is complete, the customer's browser is sent to the `return_url` given when the payment was created. The return to `return_url` is a new page load. The merchant's server reads the charge with `GET /payments/{payment_id}/charge` under its own access token, and treats the payment as paid only after that check. `return_url` should carry nothing confidential, and never `payment_secret`; an order reference that the merchant's server resolves is enough. If the return page also shows the result in the browser, the merchant's server delivers a fresh `payment_secret` from its own authenticated endpoint, and the merchant's client attaches the payment again. #### Browser: return page ```typescript // On the return_url page. The merchant's server has already confirmed the outcome // with its own access token; this only shows the result to the customer. // paymentSecret comes from your own authenticated endpoint, never from the URL. const { paymentId, paymentSecret } = await fetch('/api/checkout/payment', { credentials: 'include', }).then((r) => r.json()); await sdk.attachPayment({ paymentId, paymentSecret }); const result = await sdk.awaitChargeState(); ``` #### In the iOS and Android applications When a charge state carries a 3-D Secure redirect, the application calls `handle3dsVerification()` on the payment session. The SDK presents the step in its own web view and returns when the issuer sends the customer to `return_url`. The payment session survives the step, and the application continues polling with `getChargeState()` until the charge reaches `SUCCEEDED` or `FAILED`. The `settle()` loop in the card form examples above does both. The SDK recognises the end of the step by the `return_url`, so `return_url` must not carry a fragment (`#…`). A merchant without a website creates the payment with `https://gopay.com/sdk/charge-return` as `return_url`. The SDK closes its 3-D Secure screen on this address and never loads it. ### Charging from the merchant's server The merchant's server may charge the payment itself, for example when it has to act on the card before the charge. The SDK then returns the encrypted card instead of charging. The merchant takes over two duties that the SDK otherwise performs: collecting the browser data, and handling the 3-D Secure step. 1. The customer enters the card in the SDK card form. In the browser, the card form is mounted with `flow: 'return-payload'`, and its `result` carries the JWE. In the iOS and Android applications, the card form always returns the JWE. 2. Immediately before the charge, the merchant's client collects the browser data through the SDK: `getBrowserData()` in the browser, `completeBrowserData()` on the payment session in the iOS and Android applications. It sends the browser data to the merchant's server together with the JWE. 3. The merchant's server calls `POST /payments/{payment_id}/charge` with an `ENCRYPTED_CARD` input carrying the JWE in `payload`, and with the browser data in `browser_data`, unchanged. A charge with a saved card carries a `CARD_TOKEN` input instead. 4. If the charge response, or a later poll of `GET /payments/{payment_id}/charge`, is in `ACTION_REQUIRED`, the customer must complete the 3-D Secure step. The merchant either sends the customer's browser to `Emv 3DS Action.redirect_url` itself, or hands the step back to the SDK: `awaitChargeState()` on the attached payment in the browser, `handle3dsVerification()` on the payment session in the iOS and Android applications. The redirect must follow at once, because the issuer keeps the step open only for a short time. The outcome is then read as described under 3-D Secure above. The JWE is single-use and valid for ten minutes, so the merchant's client forwards it without delay. See the security page. ```mermaid sequenceDiagram autonumber participant MC as Merchant's client participant SDK as GoPay client SDK participant MS as Merchant's server participant API as GoPay API MC->>SDK: render card form SDK-->>MC: JWE MC->>SDK: collect browser data SDK-->>MC: browser_data MC->>MS: JWE, browser_data MS->>API: POST /payments/{id}/charge (ENCRYPTED_CARD, browser_data) API-->>MS: Payment Charge Response opt state = ACTION_REQUIRED MS-->>MC: Emv 3DS Action.redirect_url MC->>SDK: hand the 3-D Secure step to the SDK, or redirect the customer directly end ``` The charge request body is a `Payment Charge Input` carrying a `PAYMENT_CARD` payment instrument. ```http POST /gp-gw/api/4.0/payments/300000001/charge HTTP/1.1 Host: gate.gopay.com Authorization: Bearer eyJraWQiOiJzaWduLTIwMjYtMDIiLCJ0eXAiOiJKV1QiLCJhbGciOi… Content-Type: application/json { "payment_instrument": { "payment_instrument": "PAYMENT_CARD", "input": { "input_type": "ENCRYPTED_CARD", "payload": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkI…" }, "browser_data": { "language": "cs-CZ", "timezone": -60, "screen_width": 434, "screen_height": 965, "color_depth": 24, "user_agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36", "accept_header": "{\"accept-language\":\"cs;q=0.5\",\"accept-encoding\":\"gzip, deflate, br, zstd\",\"accept\":\"application/json, text/plain, */*\"}", "javascript_enabled": true, "ip": "192.0.2.42" }, "challenge_preference": "AUTO" } } ``` For a charge with a saved card, the `input` block is a `Card Token Input`. ```json { "input_type": "CARD_TOKEN", "card_token": "J7HjFNwzyBOHS+jwIMMktubTwoIRy6qB/4opvjGcKtjv…" } ``` The response is a `Payment Charge Response`. Its `state` values are listed on the payment lifecycle page; its `action` field carries the 3-D Secure redirect when the state is `ACTION_REQUIRED`. #### Browser data `browser_data` is forwarded to the card issuer during 3-D Secure authentication. Its `ip`, `user_agent` and `accept_header` fields describe the customer's connection, so they must originate in the customer's browser or device, collected immediately before the charge and not cached. The merchant's server must not fill these fields in from the request it received. The address it sees is the one its own proxy or CDN reports, and a charge issued from a queue or a retry has no customer request to read at all. A server that substitutes its own values produces a payload that looks valid and then fails authentication. A charge that runs later carries the values captured when the customer submitted, stored alongside the pending charge. #### Browser: charge from the merchant's server ```typescript // The merchant's server charges: the card form returns the encrypted card instead. // Requires sdk.attachPayment() so that the 3DS step can be handed back to the SDK. const container = document.getElementById('card-form-container')!; const ctrl = await sdk.mountCardForm(container, { flow: 'return-payload' }); const { encryptedPayload } = await ctrl.result; // Browser data is collected in the customer's browser, immediately before the charge. const browserData = await sdk.getBrowserData(); // Your own endpoint: the server charges with both values, unchanged. await fetch('/api/charge', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ encryptedPayload, browserData }), }); // The SDK redirects the page if 3DS is required (the outcome is then read // on the return_url page); otherwise it polls until SUCCEEDED or FAILED. const result = await sdk.awaitChargeState(); ``` #### iOS: charge from the merchant's server ```swift // The merchant's server charges: the card form returns the JWE and the app sends it on. // Requires a session from GopaySDK.shared.startPaymentSession(). let jwe = try await GopaySDK.shared.submitCardForm() // Browser data is collected on the device, immediately before the charge. let browserData = try await session.completeBrowserData(await BrowserData.deviceDefault()) // Your own endpoint: the server charges with both values, unchanged. try await backend.charge(paymentId: paymentId, jwe: jwe, browserData: browserData) // settle(): presents a 3DS step if one is required, then polls to a terminal state. let result = try await settle(session, session.getChargeState()) ``` #### Android: charge from the merchant's server ```kotlin // The merchant's server charges: the card form returns the JWE and the app sends it on. // Requires a session from GopaySDK.getInstance().startPaymentSession(). val jwe = (submit?.invoke() as? CardEncryptionResult.Success)?.jwe ?: return // Browser data is collected on the device, immediately before the charge. val browserData = session.completeBrowserData(BrowserData.deviceDefault(activity)) // Your own endpoint: the server charges with both values, unchanged. backend.charge(paymentId, jwe, browserData) // settle(): presents a 3DS step if one is required, then polls to a terminal state. val result = settle(activity, session, session.getChargeState()) ``` ### Content-Security-Policy A checkout page that sends a Content-Security-Policy must allow the hosts the SDK and the wallets use. ``` connect-src https://*.gopay.com frame-src https://secure.gopay.com ``` `connect-src` covers the GoPay API and the other GoPay services the SDK contacts. `frame-src` covers the card form. Apple Pay needs `https://applepay.cdn-apple.com` in five directives. ``` script-src https://applepay.cdn-apple.com script-src-elem https://applepay.cdn-apple.com img-src https://applepay.cdn-apple.com frame-src https://applepay.cdn-apple.com font-src https://applepay.cdn-apple.com ``` If only the script directives allow it, the Apple Pay button appears and the payment sheet then fails. Google Pay loads from `https://pay.google.com`, which the policy must also allow. If the policy refuses a wallet's script, the SDK reports the wallet as unavailable. --- ## Saved cards A saved card is a permanent token that stands in for a customer's card on subsequent charges. The token is created from an encrypted card payload produced by the GoPay client SDK and exchanged for a `Permanent Card Token Details` object on the merchant's server. The token may then be carried on any later charge through the `CARD_TOKEN` input variant. A card is saved in one of three ways: outside any payment, during a checkout before the payment is charged with the new token, or during a checkout only if the customer chooses to save it. Charging with a card saved earlier is covered on the checkout integration page. ### Enablement Tokenisation is disabled by default and must be enabled for the merchant before any of the operations on this page are available. Enablement is requested through `integration@gopay.com`. ### Saving a card without a payment A card may be saved outside any payment, for example on the customer's account page. No `payment_secret` is involved; the merchant's client initialises the SDK with `client_id` and `shareable_key` only. The SDK card form returns a JWE on submission: in the browser, the card form is mounted with `flow: 'return-payload'`; in the iOS and Android applications, the card form always returns the JWE. The merchant's client forwards the JWE to the merchant's server. The merchant's server exchanges the JWE for a permanent token at `POST /cards/tokens`. The request body is a `Token Create Request` carrying the JWE in `payload`; the response is a `Permanent Card Token Details` object. Tokenisation requires the `card:write` scope on the merchant access token and is server-side only. The JWE is single-use and is consumed by the tokenisation; the same JWE cannot also be used to charge a payment. ```http POST /gp-gw/api/4.0/cards/tokens HTTP/1.1 Host: gate.gopay.com Authorization: Bearer eyJraWQiOiJzaWduLTIwMjYtMDIiLCJ0eXAiOiJKV1QiLCJhbGciOi… Content-Type: application/json { "payload": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkI…" } ``` ```json { "card_id": "8007127320", "masked_pan": "406821******1234", "masked_virtual_pan": "489537******6287", "expiration_month": "01", "expiration_year": "30", "scheme": "VISA", "corporate": false, "fingerprint": "73c8d0a48d91def897612b54e630997745e1faad43045e732189cfe4acf4961b", "token": "J7HjFNwzyBOHS+jwIMMktubTwoIRy6qB/4opvjGcKtjv…", "card_art_url": "https://card.art/pic.png", "brand": "GOLD", "service_type": "DEBIT", "status": "ACTIVE" } ``` ```mermaid sequenceDiagram autonumber participant MC as Merchant's client participant SDK as GoPay client SDK participant MS as Merchant's server participant API as GoPay API MC->>SDK: initialise with client_id and shareable_key, render card form SDK-->>MC: JWE MC->>MS: forward JWE MS->>API: POST /cards/tokens (JWE) API-->>MS: Permanent Card Token Details (card_id, token, …) ``` #### Browser: tokenise a card without a payment ```typescript // Standalone tokenisation — no payment attached, no paymentSecret required. import { createGoPayBrowserSDK } from '@gopaycz/gopay-js-sdk-browser'; const sdk = createGoPayBrowserSDK({ environment: 'production', shareableKey: 'sk_live_…', clientId: 'your-client-id', }); const container = document.getElementById('card-form-container')!; const ctrl = await sdk.mountCardForm(container, { flow: 'return-payload' }); const { encryptedPayload } = await ctrl.result; // Forward encryptedPayload to your server. // Server calls: serverSdk.tokenizeEncryptedCard(encryptedPayload) ``` #### iOS: tokenise a card without a payment ```swift // Standalone tokenisation — no payment session, no paymentSecret required. // The card form needs only the clientId and shareableKey passed to GopaySDK.shared.initialize(with:). import SwiftUI import GopaySDK struct SaveCardView: View { @State private var isCardValid: Bool? var body: some View { VStack { GopayCardForm(validation: .live, isValid: $isCardValid) Button("Save card") { Task { let jwe = try await GopaySDK.shared.submitCardForm() // Forward jwe to your server. // Server calls: serverSdk.tokenizeEncryptedCard(jwe) } } .disabled(isCardValid != true) } } } ``` #### Android: tokenise a card without a payment ```kotlin // Standalone tokenisation — no payment session, no paymentSecret required. // The card form needs only the clientId and shareableKey passed to GopaySDK.initialize(). import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.* import cz.gopay.sdk.ui.CardEncryptionResult import cz.gopay.sdk.ui.PaymentCardForm import kotlinx.coroutines.launch @Composable fun SaveCardForm(onJwe: (String) -> Unit) { val scope = rememberCoroutineScope() var submit by remember { mutableStateOf<(suspend () -> CardEncryptionResult)?>(null) } PaymentCardForm(onEncryptionComplete = {}, onFormReady = { submit = it }) Button(onClick = { scope.launch { when (val result = submit?.invoke()) { // Forward result.jwe to your server. // Server calls: serverSdk.tokenizeEncryptedCard(jwe) is CardEncryptionResult.Success -> onJwe(result.jwe) is CardEncryptionResult.Error -> { /* show result.message */ } null -> Unit } } }) { Text("Save card") } } ``` ### Saving a card during checkout The card the customer enters at checkout may be saved first and the payment then charged with the new token. The JWE from the card form is spent on the tokenisation, so the charge uses the token, not the JWE. 1. The customer enters the card in the SDK card form, which returns the JWE as described above. 2. The merchant's client forwards the JWE to the merchant's server, which creates the token with `POST /cards/tokens`. 3. The merchant's server hands the token back to the merchant's client, and the SDK charges the payment with it as a `CARD_TOKEN` input. The SDK handles the 3-D Secure step, as for any charge it makes. See Charging with a saved card on the checkout integration page. Alternatively, the merchant's server charges with the token itself. It then collects the browser data and handles the 3-D Secure step, as described under Charging from the merchant's server on the checkout integration page. ```mermaid sequenceDiagram autonumber participant MC as Merchant's client participant SDK as GoPay client SDK participant MS as Merchant's server participant API as GoPay API MC->>SDK: render card form SDK-->>MC: JWE MC->>MS: forward JWE MS->>API: POST /cards/tokens (JWE) API-->>MS: Permanent Card Token Details (token) MS-->>MC: token MC->>SDK: charge with the token SDK->>API: POST /payments/{id}/charge (CARD_TOKEN) API-->>SDK: Payment Charge Response ``` #### Browser: save a card and charge with it ```typescript // Save the card first, then let the SDK charge with the new token. // Requires sdk.attachPayment() to have been called first. const container = document.getElementById('card-form-container')!; const ctrl = await sdk.mountCardForm(container, { flow: 'return-payload' }); const { encryptedPayload } = await ctrl.result; // Your own endpoint: the server tokenises the JWE and returns the token. const { cardToken } = await fetch('/api/cards', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ encryptedPayload }), }).then((r) => r.json()); await sdk.chargePayment({ payment_instrument: { payment_instrument: 'PAYMENT_CARD', input: { input_type: 'CARD_TOKEN', card_token: cardToken }, }, }); const result = await sdk.awaitChargeState(); ``` #### iOS: save a card and charge with it ```swift // Save the card first, then let the SDK charge with the new token. // Requires a session from GopaySDK.shared.startPaymentSession(). let jwe = try await GopaySDK.shared.submitCardForm() // Your own endpoint: the server tokenises the JWE and returns the token. let cardToken = try await backend.saveCard(jwe: jwe) let request = ChargePaymentRequest.cardToken( cardToken, browserData: await BrowserData.deviceDefault() ) // settle(): the polling loop from "iOS: card form, charge, and polling" on the checkout integration page. let result = try await settle(session, session.charge(request)) ``` #### Android: save a card and charge with it ```kotlin // Save the card first, then let the SDK charge with the new token. // Requires a session from GopaySDK.getInstance().startPaymentSession(). val jwe = (submit?.invoke() as? CardEncryptionResult.Success)?.jwe ?: return // Your own endpoint: the server tokenises the JWE and returns the token. val cardToken = backend.saveCard(jwe) val charge = session.chargeWithCardToken(activity = activity, cardToken = cardToken) // settle(): the polling loop from "Android: card form, charge, and polling" on the checkout integration page. val result = settle(activity, session, charge) ``` ### Letting the customer choose The merchant may let the customer decide whether the card is saved. The choice is collected in the merchant's checkout UI, next to the SDK card form; the card form does not offer it. The card form returns the JWE in both cases, and the merchant's client decides what to do with it after the customer submits. - If the customer chooses to save the card, the merchant's client forwards the JWE to the merchant's server, as in Saving a card during checkout above. - If the customer declines, the SDK charges the payment with the JWE as an `ENCRYPTED_CARD` input, and the card is not saved. A JWE is spent on one of the two, never on both. In the iOS and Android applications, the choice selects between a card token and the encrypted card on the payment session's charge call. #### Browser: save the card only if the customer chooses ```typescript // The card form returns the JWE; the customer's choice decides where it goes. // Requires sdk.attachPayment() to have been called first. const ctrl = await sdk.mountCardForm(container, { flow: 'return-payload' }); const { encryptedPayload } = await ctrl.result; const input = saveCardCheckbox.checked ? { input_type: 'CARD_TOKEN', card_token: await saveCard(encryptedPayload) } // your own endpoint : { input_type: 'ENCRYPTED_CARD', payload: encryptedPayload }; await sdk.chargePayment({ payment_instrument: { payment_instrument: 'PAYMENT_CARD', input } }); const result = await sdk.awaitChargeState(); ``` ### Reading a saved card `GET /cards/tokens/{card_id}` returns the current `Permanent Card Token Details` for a saved card. The endpoint requires the `card:read` scope on the merchant access token and is server-side only. The merchant must persist the association between `card_id` and customer; the API does not list saved cards. ### Deleting a saved card `DELETE /cards/tokens/{card_id}` permanently deletes a saved card. The endpoint requires the `card:write` scope on the merchant access token and is server-side only. A deleted token is not recoverable; if the customer wants the same card saved again, the card must be saved again. ### Card states `Card Token Status` describes the state of a saved card. - `ACTIVE` — the token is valid and may be used on a charge. - `SUSPENDED` — the token has been temporarily suspended by the card issuer and cannot be used. Neither the merchant nor GoPay can put a card into `SUSPENDED`; only the issuer can. The issuer may later move the card back to `ACTIVE` without the merchant's involvement. - `DELETED` — the token has been deleted and cannot be used. ### Card updates The instrument behind a token may change while the `card_id` stays the same. The card issuer may reissue the card with a new PAN or expiration date — a network reissue, a replacement after loss or theft, an expiry renewal — and the corresponding `Permanent Card Token Details` fields (`masked_pan`, `expiration_month`, `expiration_year`, and the network-token fields) are updated in place. Subsequent charges with the same `card_id` continue to work. The merchant learns of an update through the `cards.updated` event. The merchant's server re-fetches the token with `GET /cards/tokens/{card_id}` to read the new fields. See the events and webhooks page. ### Token scope A `card_id` is scoped to the merchant, not to a single eshop. The same token is usable across every `goid` belonging to that merchant. `Customer.customer_id` on the payment is a label only; GoPay does not currently use it to scope tokens. The merchant is responsible for tying a `card_id` to a customer in its own systems. ### Single-token rule Only one token per (merchant, card) pair exists at any time. Re-tokenising the same physical card returns the existing `card_id` with whatever fields the existing token already carries, including any updates the issuer has pushed in the meantime. The `fingerprint` field is stable across tokenisations of the same physical card and may be used to detect a re-tokenisation before the call is made. To obtain a fresh token for the same card — for example, to discard a `SUSPENDED` token — the existing token must be deleted first. ### Merchant obligations When saved cards are offered to the customer, the merchant must: - provide an interface for the customer to view their saved cards; - provide a way for the customer to delete a saved card. The merchant may additionally offer a way to add a card outside the checkout, in which case Saving a card without a payment above applies. At card-save time, the merchant must satisfy one of the following: - inform the customer that the card will be saved - have the customer choose whether to save the card - restrict card saving to logged-in customers --- ## Apple Pay Apple Pay is offered to the customer through the GoPay client SDK. The SDK shows the Apple Pay sheet, charges the payment with the result, and reports the charge outcome to the merchant's client. The merchant's code does not handle the Apple Pay payload and does not call the charge endpoint for an Apple Pay payment. This page covers what the SDK does at charge time, the operational setup on the web and in the merchant's mobile application, and the availability checks. The checkout flow itself and the 3-D Secure handoff are covered on the checkout integration page; Apple Pay rides the same end-to-end shape. ### Runtime The payment is attached to the GoPay client SDK before Apple Pay is offered, as described on the checkout integration page. - In the browser, the merchant's client mounts the Apple Pay button with `mountApplePayButton`. The SDK draws the button. When the customer pays, the SDK runs the Apple Pay session, charges the payment, and drives the charge to a terminal state. - In the iOS application, the merchant's application draws its own Apple Pay button and calls `chargeWithApplePay` on the payment session. The SDK presents the Apple Pay sheet and charges the payment with its result. The application then reads the charge state in the same way as for a card charge. A card paid with Apple Pay cannot be saved for later reuse. #### Browser: Apple Pay button and charge ```typescript // Render Apple Pay button, charge payment, and poll until terminal state. // Requires sdk.attachPayment() to have been called first. const container = document.getElementById('apple-pay-button-container')!; const ctrl = await sdk.mountApplePayButton(container, { onUnavailable: () => { /* Apple Pay not available — hide the button or show a fallback */ }, onCancel: () => { /* User dismissed the sheet — update your UI */ }, }); // Resolves with PaymentChargeStatusResponse on SUCCEEDED. const result = await ctrl.result; // Call ctrl.unmount() when the component is torn down. ``` #### iOS: Apple Pay charge ```swift // Charge with Apple Pay, then poll until terminal state. // Requires a session from GopaySDK.shared.startPaymentSession(). // The app draws its own Apple Pay button (PKPaymentButton) and calls this from its action. guard GopaySDK.canUseApplePay() else { // Apple Pay not available — hide the button or show a fallback return } do { // Presents the Apple Pay sheet and charges the payment with its result. let charge = try await session.chargeWithApplePay() // settle(): the polling loop from "iOS: card form, charge, and polling" on the checkout integration page. let result = try await settle(session, charge) } catch is CancellationError { // User dismissed the 3-D Secure step — the charge state is still read with getChargeState() } catch { // Charge failed, or the user dismissed the Apple Pay sheet — update your UI } ``` ### Web setup Before Apple Pay on the web can be used, the merchant provides the production and sandbox eshop URLs to GoPay at `integration@gopay.com`. The Apple Pay button must only be offered on the URLs registered with GoPay; offering Apple Pay on any other URL is not supported. The registered URL must be an absolute HTTPS URL with a hostname. Apple Pay does not work on `localhost` or on a bare IP address, neither in the sandbox nor in production — unlike the rest of the API, which is more tolerant in the sandbox. For local development, the merchant registers a hosts-file name served over HTTPS with a locally trusted certificate; see the site requirements on the overview page. A page that sends a Content-Security-Policy must allow `https://applepay.cdn-apple.com` in five directives, not only in the script directives. The list is on the checkout integration page. ### App setup Apple Pay inside the merchant's iOS application requires the merchant to enrol the application with Apple and exchange a payment-processing certificate with GoPay. 1. The merchant registers in the Apple Developer Portal. 2. The merchant requests a Certificate Signing Request from GoPay at `integration@gopay.com`. 3. The merchant creates an Apple Merchant Identifier in the Apple Developer Portal, enables the Apple Pay capability for the application, and links the Merchant Identifier to the application. 4. The merchant uploads the CSR to Apple as the Payment Processing certificate for that Merchant Identifier. 5. The merchant sends the resulting certificate file and the Apple Merchant Identifier to GoPay at `integration@gopay.com`. After the certificate is in place with GoPay, no Apple Pay-specific step happens on the merchant's server at charge time. ### Availability The GoPay client SDK checks whether Apple Pay is available before it offers it. - In the browser, `mountApplePayButton` calls the `onUnavailable` callback instead of drawing the button when the customer's device or browser cannot pay with Apple Pay. A checkout that shows one payment method at a time asks `getApplePayAvailability()` before it renders the Apple Pay step. The call does not need an attached payment, so it answers before the payment is created. - In the iOS application, `GopaySDK.canUseApplePay()` answers the same question. The SDK loads Apple's Apple Pay script itself. The merchant's page does not include it, and an existing `