Google Pay
Accept Google Pay through Fynex via hosted checkout, or by submitting the signed token from Google's JS SDK to /initialize-payment (Fynex decrypts) — or, if you operate your own DIRECT-mode keys, by sending the already-decrypted payload.
Fynex supports three ways to accept Google Pay:
- Hosted Checkout — Fynex renders the Google Pay button on its own checkout page. Your servers never see the token. Lowest PCI scope and the recommended path for most integrations.
- Encrypted Direct API — you render the Google Pay button on your own page, capture the signed token from Google's JS SDK, and submit it as-is (still encrypted) to
POST /payments-api/v1/initialize-payment. Fynex hands the token to the upstream processor for server-side decryption. You don't hold private keys. - Decrypted Direct API — you decrypt the Google Pay token on your server using your own DIRECT-mode key pair, then send Fynex the decrypted card data. Use this only if you already operate ECv2 keys.
Caution
Availability today (2026-05-13)
Direct API is not yet running on production (
api.fynex.ai); contact Fynex support before you cut over.
Choosing an integration model
Hosted Checkout (recommended)
- You call
POST /payments-api/v1/checkout, redirect the customer, receive the outcome by webhook or polling. - Fynex handles
isReadyToPay, the GP button, the encrypted token, decryption, and the 3DS step-up. - Lowest PCI scope.
- No Google Pay Console registration required on your side — Fynex's platform merchant ID is used.
Encrypted Direct API
- You render the Google Pay button using the Google Pay JS SDK on your own page.
- You forward
paymentData.paymentMethodData.tokenizationData.token(a signed string) verbatim toPOST /initialize-payment. - Fynex hands the signed token to the upstream processor, which decrypts it server-side.
- You don't run ECv2 decryption and don't hold private keys.
- Recommended when Hosted Checkout doesn't fit your UX.
Decrypted Direct API
- You register your own Google Pay merchant in the Google Pay Business Console and own the DIRECT-mode key pair.
- Your frontend calls
paymentsClient.loadPaymentData(...)withtokenizationSpecification.type = 'DIRECT'; your backend decrypts the JWE. - You send the decrypted payload (PAN, cryptogram, expiry, …) to
POST /initialize-payment. - For integrators who already run ECv2 decryption pipelines.
If you're not sure which to pick, use Hosted Checkout. If you need a non-hosted UI without running decryption yourself, use Encrypted Direct API.
Why Encrypted is the default Direct API recommendation
Both Direct API flows authorize the same card payment. The split between Encrypted and Decrypted is really a split between who decrypts the Google Pay wallet token, and that one decision changes the integration cost dramatically:
| Concern | Encrypted | Decrypted |
|---|---|---|
| Card-data PCI scope | No PAN exposure — opaque ciphertext only (typically SAQ A-EP) | PAN/cryptogram pass through your process memory (SAQ D-Merchant) |
| Private-key management | None | You generate, store, rotate an ECv2 EC P-256 key pair |
| Server-side crypto code | None — forward the signed string verbatim | You implement ECv2 decryption (signed-then-encrypted, AES-CTR + HMAC-SHA256) |
| Google Pay Console setup | Use Fynex's registered gateway (fondyeu) — fast |
Register your own merchant + public key — slower |
| Breach blast radius | Attacker can't decrypt without the gateway's key | Attacker can read decrypted PANs from memory / heap dumps |
| Time to integrate | Forward one string, get a payment handle | Build + audit a crypto pipeline |
The Decrypted flow only earns its overhead when you already carry a full PCI DSS compliance assessment (ROC) with on-premise key management and want explicit control over the PAN — typically a large merchant doing per-network routing or a payment orchestrator. Anyone smaller should prefer Encrypted.
Hosted Checkout
The seller-facing API is just POST /payments-api/v1/checkout — exactly the same call you would make for a card-only hosted checkout. Google Pay availability is a property of the deploy, not the request.
curl -sS -X POST "$FYNEX_API/payments-api/v1/checkout" \
-H "Authorization: Bearer $FYNEX_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"externalOrderRef": "ORDER-1042",
"amount": 49.99,
"currencyCode": "GBP",
"countryCode": "GB",
"description": "Order #1042",
"returnUrls": {
"success": "https://merchant.example/orders/1042/success",
"failure": "https://merchant.example/orders/1042/failure"
}
}'
Response (truncated to the field you care about):
{
"sessionId": "6f9b84e1-3b83-4fb9-9f42-a8ac27d11d6b",
"checkoutUrl": "https://pay.fynex.ai/checkout/6f9b84e1-3b83-4fb9-9f42-a8ac27d11d6b",
"expiresAt": "..."
}
Redirect the customer to checkoutUrl. The page that loads will display:
- A card-entry form (always).
- A Google Pay button — only if all of the following are true:
- Fynex has Google Pay enabled for the deploy your checkout session was created on.
- The customer's browser supports Google Pay (
PaymentsClient.isReadyToPay()returns true). - The customer has at least one saved card in their Google account that Google Pay can tokenize.
- An Apple Pay button — only if Apple Pay is enabled on the deploy. Apple Pay availability requires Apple Developer certificates plus an upstream-processor-issued payment processing certificate to be wired in. Until that's done the button stays hidden.
See Hosted Checkout for the full request reference, return-URL behavior, and webhook delivery.
Customer-side flow
Customer arrives at
https://pay.fynex.ai/checkout/{session_id}. Fynex serves an HTML page rendered server-side; the page includes the Google Pay JS SDK and a hidden GP button.The page runs
isReadyToPay()in the customer's browser. If the result is positive and the deploy has Google Pay configured, the button becomes visible. Otherwise the page hides it and shows only the card form — there's no "broken button" state.Customer clicks Google Pay. The native Google Pay sheet opens; the customer picks a card and authorizes.
The page POSTs the encrypted GP token to
/api/v1/checkout/google-pay-submitalong with thesessionId. The handler:- Resolves the authoritative
amount,currencyCode, and upstream-provideraccountIdserver-side from the CheckoutSession + GenericPayment that back the session. Body-supplied amount/currency are ignored to defeat a class of browser-extension / XSS attacks that would otherwise pay $0.01 for a $19.99 order. - Sends the encrypted JWE token to the upstream card processor via its Single-Use Token endpoint, using credentials that the processor has enrolled the seller's Google Pay merchant against. The token never leaves the Fynex backend in plaintext.
- Receives back a
paymentHandleTokenandmerchantRefNum.
- Resolves the authoritative
The page completes the payment through the shared checkout contract — the same initialize/finalize path a card payment takes, using the
paymentHandleTokenandmerchantRefNumfrom step 4. There is no separatecomplete-google-pay-paymentcall: that legacy route bypassed the platform's antifraud and compliance gates and has been removed. The outcome is mirrored onto the payment (lifecycleStatuscapturedon success, withproviderPaymentIdset).Customer is redirected to your
returnUrls.successor.failureURL, exactly as for a card payment.
Note
Steps 4 and 5 are internal to the Fynex-hosted page. As a seller integration you never call those endpoints; you only see the final state via webhooks,
GET /payments-api/v1/payments/{payment_id}, or thereturnUrlsredirect.
Encrypted Direct API
This is the recommended Direct API path. You take ownership of the Google Pay button but not of the token decryption — Fynex passes the signed token to the upstream processor, which decrypts it server-side. You don't need private keys, ECv2 libraries, or PCI scope on the card data.
Prerequisites
- Your seller account has
google_payinallowedPaymentMethods(check viaGET /payments-api/v1/payment-methods). - A Google Pay merchant ID in the Google Pay Business Console, with domains whitelisted where you render the button.
- Checkout currency supported (
USD,EUR,GBP).
Step 1: Get a token from Google
In your frontend, load the Google Pay JS SDK and request a payment data with PAYMENT_GATEWAY tokenization (Fynex's registered gateway is fondyeu — that exact string is what Google's tokenization spec requires):
const paymentsClient = new google.payments.api.PaymentsClient({ environment: 'PRODUCTION' });
const paymentDataRequest = {
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
allowedCardNetworks: ['VISA', 'MASTERCARD', 'AMEX', 'DISCOVER'],
},
tokenizationSpecification: {
type: 'PAYMENT_GATEWAY',
parameters: {
gateway: 'fondyeu',
gatewayMerchantId: '<your-fynex-merchant-id>',
},
},
}],
transactionInfo: {
totalPriceStatus: 'FINAL',
totalPrice: '49.99',
currencyCode: 'GBP',
},
merchantInfo: {
merchantId: '<your-google-pay-merchant-id>',
merchantName: 'Your Store',
},
};
const paymentData = await paymentsClient.loadPaymentData(paymentDataRequest);
// paymentData.paymentMethodData.tokenizationData.token is the JSON-encoded
// signed-and-encrypted payload — pass it through verbatim.
const signedToken = paymentData.paymentMethodData.tokenizationData.token;
Step 2: Call /initialize-payment with the signed token
POST /payments-api/v1/initialize-payment (headers: Authorization: Bearer <seller_token>, Idempotency-Key: <UUID v4>):
{
"externalOrderRef": "ORDER-100106",
"amount": 49.99,
"paymentType": "card",
"paymentMethod": "google_pay",
"currencyCode": "GBP",
"countryCode": "GB",
"autoSettlement": true,
"googlePayData": {
"paymentToken": {
"apiVersion": 2,
"apiVersionMinor": 0,
"paymentMethodData": {
"type": "CARD",
"description": "Visa •••• 1234",
"info": {
"cardNetwork": "VISA",
"cardDetails": "1234"
},
"tokenizationData": {
"type": "PAYMENT_GATEWAY",
"token": "<paymentData.paymentMethodData.tokenizationData.token verbatim>"
}
}
}
},
"billingDetails": {
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"addressLine1": "1 Example Street",
"city": "London",
"countryCode": "GB",
"postalCode": "SW1A1AA"
},
"returnLinks": [
{ "rel": "default", "href": "https://merchant.example/payments/return", "method": "GET" }
]
}
Validation rules:
- Exactly one of
tokenizationData.token(encrypted) ortokenizationData.decryptedToken(decrypted, see below) must be present. Providing both returns400. tokenizationData.typeis required and must match the payload kind:"PAYMENT_GATEWAY"whentokenis set,"DIRECT"whendecryptedTokenis set. Empty or mismatched values return400.- Top-level
billingDetailsis required for AVS scoring. Minimum:countryCodeandpostalCode. You may use the aliasescountry/zip. Omitting them returns a502with body{"error":"upstream card processor returned 400"}.
Response and capture flow are the same as the decrypted variant — see Common response shape below.
Decrypted Direct API
Use this only if you already operate your own Google Pay DIRECT-mode key pair and prefer to decrypt server-side. Fynex receives the decrypted card data and forwards it to the upstream processor's decrypted-handle endpoint.
Prerequisites (in addition to Encrypted)
- Your own Google Pay merchant in the Google Pay Business Console.
- A DIRECT-mode key pair you registered with Google. Fynex never holds your private key.
- A server-side ECv2 decryption library.
Step 1: Get and decrypt a token from Google
Same JS SDK call as the Encrypted variant, but with tokenizationSpecification.type = 'DIRECT' and your publicKey:
tokenizationSpecification: {
type: 'DIRECT',
parameters: {
protocolVersion: 'ECv2',
publicKey: '<your-base64-encoded-public-key>',
},
}
Decrypt paymentData.paymentMethodData.tokenizationData.token on your server using the Google Pay payment-data cryptography spec. The decrypted payload looks like:
{
"messageId": "AH2EjtfX...",
"messageExpiration": "1715620000000",
"paymentMethod": "CARD",
"paymentMethodDetails": {
"authMethod": "CRYPTOGRAM_3DS",
"pan": "4111111111111111",
"expirationMonth": 12,
"expirationYear": 2028,
"cryptogram": "AgAAAAAABk4...",
"eciIndicator": "05"
}
}
Step 2: Call /initialize-payment with the decrypted payload
{
"externalOrderRef": "ORDER-100106",
"amount": 49.99,
"paymentType": "card",
"paymentMethod": "google_pay",
"currencyCode": "GBP",
"countryCode": "GB",
"autoSettlement": true,
"googlePayData": {
"paymentToken": {
"apiVersion": 2,
"apiVersionMinor": 0,
"paymentMethodData": {
"type": "CARD",
"description": "Visa •••• 1234",
"info": { "cardNetwork": "VISA", "cardDetails": "1234" },
"tokenizationData": {
"type": "DIRECT",
"decryptedToken": {
"messageId": "AH2EjtfX...",
"messageExpiration": "1715620000000",
"paymentMethod": "CARD",
"paymentMethodDetails": {
"authMethod": "CRYPTOGRAM_3DS",
"pan": "4111111111111111",
"expirationMonth": 12,
"expirationYear": 2028,
"cryptogram": "AgAAAAAABk4...",
"eciIndicator": "05"
}
}
}
}
}
},
"billingDetails": { "...": "same as Encrypted" },
"returnLinks": [ { "rel": "default", "href": "https://merchant.example/payments/return", "method": "GET" } ]
}
Server-side validation for the decrypted variant:
panis required.cryptogramis required whenauthMethodisCRYPTOGRAM_3DS.PAN_ONLYtokens carry no network cryptogram and are accepted on this flow without one.expirationMonthandexpirationYearare required.eciIndicatoris recommended forCRYPTOGRAM_3DSbut not enforced by Fynex.
Common response shape
Both Direct API models share the same response. POST /initialize-payment returns 202 Accepted:
{
"paymentId": "ORDER-100106",
"status": "provider_pending",
"lifecycleStatus": "pending",
"legacyStatus": "provider_pending",
"amount": 49.99,
"currencyCode": "GBP",
"requiresAction": true,
"actionUrl": "https://example-acs.test/challenge"
}
- When
requiresAction = true, redirect the customer toactionUrlto complete the 3DS challenge. After the challenge,lifecycleStatusmoves frompendingtocaptured. - When
requiresAction = false, the handle is ready for capture.
Then POST /finalize-payment (reuse the same Idempotency-Key or generate a fresh UUID — both work):
{ "paymentId": "ORDER-100106" }
amount is optional. Omitting it captures the full authorized amount; passing a value must be less than or equal to the originally authorized amount (partial capture).
Response 200 OK:
{
"paymentId": "ORDER-100106",
"status": "provider_completed",
"lifecycleStatus": "captured",
"legacyStatus": "provider_completed",
"providerPaymentId": "pay_01J2EXAMPLE",
"providerCode": "pp_01",
"capturedAmount": 49.99,
"currencyCode": "GBP",
"failureCode": 0,
"failureDescription": ""
}
googlePayData field reference
tokenizationData — Encrypted (model B)
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | Must be "PAYMENT_GATEWAY" for the encrypted flow. |
token |
string | yes | Raw signed token from paymentData.paymentMethodData.tokenizationData.token. JSON-encoded string. |
tokenizationData — Decrypted (model C)
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | Must be "DIRECT" for the decrypted flow. |
decryptedToken |
object | yes | Decrypted Google Pay payload — see below. |
Send exactly one of token or decryptedToken per request. type is required and must match the payload kind — mismatches return 400.
decryptedToken
| Field | Type | Required | Description |
|---|---|---|---|
gatewayMerchantId |
string | no | Echoed verbatim to the upstream processor. |
messageId |
string | yes (processor contract) | messageId from the decrypted payload. Forwarded, but not enforced by the Fynex validator. |
messageExpiration |
string | yes (processor contract) | Epoch milliseconds as a string. Not checked by the request validator, but it must parse as an integer later — see caveat 2. |
paymentMethod |
string | yes (processor contract) | Always "CARD". Forwarded, but not enforced by the Fynex validator. |
paymentMethodDetails |
object | yes | See below. Missing returns 400. |
paymentMethodDetails
| Field | Type | Required | Description |
|---|---|---|---|
authMethod |
string | yes (processor contract) | "PAN_ONLY" or "CRYPTOGRAM_3DS". Matched case-insensitively and forwarded upstream in the spec spelling; a value that is neither is passed through unchanged, and the Fynex validator does NOT enforce the enum today. |
pan |
string | yes | Decrypted card PAN. Missing returns 400. |
expirationMonth |
int | yes (> 0) | 1–12. Zero or missing returns 400. |
expirationYear |
int | yes (> 0) | Full year (e.g. 2028). Zero or missing returns 400. |
cryptogram |
string | yes when authMethod is CRYPTOGRAM_3DS |
The network cryptogram. Omitting it on a CRYPTOGRAM_3DS token returns 400. PAN_ONLY tokens carry no cryptogram and are accepted without one. |
eciIndicator |
string | no | Recommended for CRYPTOGRAM_3DS (usually "05"/"06"). Not enforced by Fynex; gated to CRYPTOGRAM_3DS at the outbound builder so PAN_ONLY tokens don't get rejected with "Invalid field when authMethod is not CRYPTOGRAM_3DS". |
billingDetails (top level of InitiatePaymentRequest)
Required for AVS. Minimum fields: countryCode (alias country) and postalCode (alias zip).
Parent-object presence checks
Before the field-level checks above, the validator walks the request shape and rejects any missing parent. Each nil returns 400 with a clear path-rooted message:
googlePayDatagooglePayData.paymentTokengooglePayData.paymentToken.paymentMethodDatagooglePayData.paymentToken.paymentMethodData.tokenizationDatatokenizationData.tokenxortokenizationData.decryptedToken— send exactly one; neither or both returns400tokenizationData.type == "PAYMENT_GATEWAY"whentokenis settokenizationData.type == "DIRECT"whendecryptedTokenis set
Header + common body (shared with every paymentMethod)
| Field | Format |
|---|---|
Idempotency-Key (HTTP header) |
UUID, non-zero |
externalOrderRef |
non-empty string |
amount |
number > 0 |
paymentType |
"card" |
paymentMethod |
"google_pay" |
currencyCode |
3-letter ISO, must pass IsValidCurrencyCode |
countryCode |
2-letter ISO |
paymentType and paymentMethod are additionally checked against each other — an incompatible pair returns 400.
Upstream outbound mapping (for debugging processor rejections)
Unlike Apple Pay, the Google Pay DTO mirrors the upstream processor's shape closely — the only nesting difference is at the root: Fynex's googlePayData becomes the processor's googlePay.googlePayPaymentToken.
| Fynex DTO path | Upstream outbound path |
|---|---|
googlePayData.paymentToken.apiVersion |
googlePay.googlePayPaymentToken.apiVersion |
googlePayData.paymentToken.apiVersionMinor |
googlePay.googlePayPaymentToken.apiVersionMinor |
googlePayData.paymentToken.paymentMethodData.description |
googlePay.googlePayPaymentToken.paymentMethodData.description |
…tokenizationData.type |
…tokenizationData.type |
…tokenizationData.decryptedToken.paymentMethodDetails.pan |
…tokenizationData.decryptedToken.paymentMethodDetails.pan |
…paymentMethodDetails.expirationMonth |
…paymentMethodDetails.expirationMonth |
…paymentMethodDetails.expirationYear |
…paymentMethodDetails.expirationYear |
…paymentMethodDetails.cryptogram |
…paymentMethodDetails.cryptogram (sent only when authMethod is CRYPTOGRAM_3DS, matched case-insensitively) |
…paymentMethodDetails.eciIndicator |
…paymentMethodDetails.eciIndicator (sent only when authMethod is CRYPTOGRAM_3DS, matched case-insensitively) |
Caveats
authMethodenum is not enforced. The validator does not check thatauthMethodis one ofPAN_ONLY/CRYPTOGRAM_3DS. An empty or arbitrary string passes through to the processor, which will reject it with its own (less helpful) error.paymentMethod,messageId,messageExpirationare required by the processor's sample but the Fynex validator does not enforce them, and each behaves differently when omitted.messageIdround-trips exactly as sent, including empty.paymentMethoddefaults to"CARD".messageExpirationmust parse as an epoch-milliseconds integer — a missing or malformed value fails the payment during processing, after the202, with a Fynex error rather than an upstream one. Send all three.cryptogramgating onauthMethod. Fynex requirescryptogramonly whenauthMethodisCRYPTOGRAM_3DS, so aPAN_ONLYtoken validates without one — do not send a placeholder value. The outboundcryptogram+eciIndicatorare additionally gated onauthMethodbeingCRYPTOGRAM_3DS(matched case-insensitively) before the request leaves Fynex, which avoids the processor's "Invalid field when authMethod is not CRYPTOGRAM_3DS" error. A value that is neither enum neither requires acryptogramnor forwards one, so nothing is demanded of you that Fynex would discard — but the payment will still fail upstream on the unrecognised value. Send one of the two documented values.gatewayMerchantIdis forwarded, not ignored. It is not part of the processor's decrypted-token request sample, but Fynex passes it through verbatim when set. If you send it, send the same value your Google Pay client used — the processor matches this field against the merchant behind the tokenization key, and an unrelated value can be rejected upstream.
3DS behavior
Google Pay tokens carry one of two auth methods:
| Auth method | Source | What you'll see |
|---|---|---|
CRYPTOGRAM_3DS |
Tokenized device-bound card (Android phone, Wear OS) | The upstream processor may waive the customer-facing 3DS challenge since the GP cryptogram already proves cardholder presence. Fynex always sends the threeDs block, so you should still handle requiresAction: true in your client code in case it doesn't waive. |
PAN_ONLY |
Server-tokenized card stored in the Google account (no device cryptogram) | May trigger a 3DS step-up. Direct API: redirect the customer to actionUrl and resume on return. Hosted Checkout handles the redirect inside the page. |
Caution
Do not set
skip3DSfor Google Pay in live traffic. SCA exemptions are tied to MIT/MOTO flows, which Google Pay does not match.
Payment lifecycle
A Google Pay payment has the same lifecycle as a card payment: lifecycleStatus goes from pending to captured, or to failed / cancelled. See Payment Lifecycle.
Verifying the outcome
Same as any payment — three options, in decreasing order of recommendation:
- Webhook (
POST /payments-api/v1/webhooksto register a URL once; Fynex postsPaymentCompleted— andPaymentRefundedon refunds — when state changes). See Webhooks. - REST poll —
GET /payments-api/v1/payments/{payment_id}with your bearer token. Thepayment_idpath parameter is yourexternalOrderRef. Stop oncelifecycleStatusisrefunded,failedorcancelled(orcapturedif you don't need settlement progress). See Polling & SSE. - GraphQL —
genericPayment(id: Int!)on/dashboard/graphql(cookie-auth only — for dashboard integrations, not server-to-server).
The paymentMethod field on the response echoes what you sent on /initialize-payment — google_pay for a wallet payment, card for a typed card — even though the underlying card is charged through the same upstream processor. Use it to distinguish the two at the data level.
Testing
- Sandbox: set
environment: 'TEST'on the Google Pay JS side. On Fynex's side the seller account must be inDemooperational mode (capital D — that's the canonical value, though the routing layer is case-insensitive). - 3DS: the upstream sandbox lets you replay both success and failure challenges.
Test cards
Google Pay's TEST environment returns a sandbox PAN that is then authorized on the upstream card processor's sandbox. Google Pay tokens are network-tokenized, so the upstream handle is created with 3DS skipped — Google Pay payments authorize without a separate 3DS redirect.
Tokenize any PAN the upstream sandbox recognises; for example the universal Visa test PAN
4111 1111 1111 1111, or any of the PANs listed in Test cards & sandbox.
Expiry: any future month/year (4-digit year recommended, e.g. 2028). CVV: any 3-digit value
(4-digit for AmEx).
Caution
The card number does not select the authorization outcome on this sandbox. To simulate a decline, use the processor's sandbox simulation rules — ask your Fynex contact for the current values. Do not assume a particular PAN maps to "frictionless" vs "challenge" vs "decline".
Troubleshooting
The Google Pay button doesn't appear on the hosted checkout page
In order of frequency:
- Google Pay isn't enabled on this deploy yet. The platform's upstream-processor credentials for Google Pay haven't been provisioned, or the per-deploy GP merchant ID is missing. Sellers can't fix this — contact Fynex support.
- The customer's browser or device doesn't support Google Pay.
isReadyToPay()returnsfalseand the page (correctly) leaves the button hidden. Nothing to fix — the customer can use the card form. - The customer has no saved cards in their Google account.
isReadyToPay()may still returntrue, but the GP sheet shows an empty list when the customer opens it. Not a configuration bug. - Environment mismatch. The deploy points at Google Pay's
PRODUCTIONenvironment but the merchant ID is only enrolled inTEST, or vice versa. Google's SDK silently treats this as "GP unavailable." Contact Fynex support to confirm the configured environment.
/initialize-payment returns 400 "...either token or decryptedToken, not both"
You supplied both tokenizationData.token and tokenizationData.decryptedToken. Pick one path and remove the other.
/initialize-payment returns 400 "...token or .decryptedToken is required"
You supplied a tokenizationData block but neither token nor decryptedToken. Add the matching field for the model you're using.
/initialize-payment returns 400 "cryptogram is required"
You're on the Decrypted Direct API flow, authMethod is CRYPTOGRAM_3DS, and the token carries no cryptogram. Send the cryptogram, or — if the token really is a server-tokenized card — set authMethod to PAN_ONLY (matched case-insensitively), which is accepted without one.
/initialize-payment returns 502 "upstream card processor returned 400"
Most often: missing countryCode or postalCode in the top-level billingDetails. AVS scoring is performed against billingDetails, not the address embedded inside googlePayData.
/initialize-payment returns 409
Idempotency-Key reused with different business intent (amount, currencyCode, externalOrderRef, …). Use a fresh UUID v4.
The customer authorized Google Pay, but the order page says "failed"
Look at the payment's failureStage:
failureStage: "authorization"— the encrypted token reached the upstream processor but the issuer declined the card. Normal card decline; the customer should try a different card or method.failureStage: "settlement"— the handle was authorized but the settlement call failed. Usually a transient upstream issue; retry the order with a freshexternalOrderRef.- Empty
failureStagewithstatus: "cancelled"— the customer dismissed the GP sheet. The page treats this as a soft exit and does not create a payment.
Production checklist
For the Encrypted Direct API (recommended for non-hosted UIs):
- Google Pay merchant ID registered in the Google Pay Business Console.
- Domains that render the Google Pay button whitelisted in the Google Pay Console.
- Production seller API token issued (
Authorization: Bearer ...). google_payenabled for the seller account in the Fynex dashboard.returnLinksconfigured (success / failure URLs).- Webhook registered for final status delivery, or polling implemented via
GET /payments/{payment_id}.
For the Decrypted Direct API, additionally:
- Server-side DIRECT-mode decryption library in place that can handle ECv2 payloads, and your own DIRECT-mode key pair registered with Google.
See also
- Hosted Checkout — The seller-facing API that creates the checkout session Google Pay runs inside.
- Apple Pay — Apple Pay is decrypted-flow today — Fynex receives the decrypted token. See the Apple Pay guide for the payload shape.
- 3DS Authentication — How 3DS step-up works inside Hosted Checkout and Direct API flows.
- Polling & SSE — Verify the payment outcome from your backend.
- Errors — HTTP status codes and the per-seller rate-limit headers.