Skip to content
In this section

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

  • 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 to POST /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(...) with tokenizationSpecification.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

  1. 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.

  2. 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.

  3. Customer clicks Google Pay. The native Google Pay sheet opens; the customer picks a card and authorizes.

  4. The page POSTs the encrypted GP token to /api/v1/checkout/google-pay-submit along with the sessionId. The handler:

    • Resolves the authoritative amount, currencyCode, and upstream-provider accountId server-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 paymentHandleToken and merchantRefNum.
  5. The page completes the payment through the shared checkout contract — the same initialize/finalize path a card payment takes, using the paymentHandleToken and merchantRefNum from step 4. There is no separate complete-google-pay-payment call: that legacy route bypassed the platform's antifraud and compliance gates and has been removed. The outcome is mirrored onto the payment (lifecycleStatus captured on success, with providerPaymentId set).

  6. Customer is redirected to your returnUrls.success or .failure URL, 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 the returnUrls redirect.

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

  1. Your seller account has google_pay in allowedPaymentMethods (check via GET /payments-api/v1/payment-methods).
  2. A Google Pay merchant ID in the Google Pay Business Console, with domains whitelisted where you render the button.
  3. 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) or tokenizationData.decryptedToken (decrypted, see below) must be present. Providing both returns 400.
  • tokenizationData.type is required and must match the payload kind: "PAYMENT_GATEWAY" when token is set, "DIRECT" when decryptedToken is set. Empty or mismatched values return 400.
  • Top-level billingDetails is required for AVS scoring. Minimum: countryCode and postalCode. You may use the aliases country / zip. Omitting them returns a 502 with 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:

  • pan is required.
  • cryptogram is required when authMethod is CRYPTOGRAM_3DS. PAN_ONLY tokens carry no network cryptogram and are accepted on this flow without one.
  • expirationMonth and expirationYear are required.
  • eciIndicator is recommended for CRYPTOGRAM_3DS but 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 to actionUrl to complete the 3DS challenge. After the challenge, lifecycleStatus moves from pending to captured.
  • 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:

  • googlePayData
  • googlePayData.paymentToken
  • googlePayData.paymentToken.paymentMethodData
  • googlePayData.paymentToken.paymentMethodData.tokenizationData
  • tokenizationData.token xor tokenizationData.decryptedToken — send exactly one; neither or both returns 400
  • tokenizationData.type == "PAYMENT_GATEWAY" when token is set
  • tokenizationData.type == "DIRECT" when decryptedToken is 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

  1. authMethod enum is not enforced. The validator does not check that authMethod is one of PAN_ONLY / CRYPTOGRAM_3DS. An empty or arbitrary string passes through to the processor, which will reject it with its own (less helpful) error.
  2. paymentMethod, messageId, messageExpiration are required by the processor's sample but the Fynex validator does not enforce them, and each behaves differently when omitted. messageId round-trips exactly as sent, including empty. paymentMethod defaults to "CARD". messageExpiration must parse as an epoch-milliseconds integer — a missing or malformed value fails the payment during processing, after the 202, with a Fynex error rather than an upstream one. Send all three.
  3. cryptogram gating on authMethod. Fynex requires cryptogram only when authMethod is CRYPTOGRAM_3DS, so a PAN_ONLY token validates without one — do not send a placeholder value. The outbound cryptogram + eciIndicator are additionally gated on authMethod being CRYPTOGRAM_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 a cryptogram nor 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.
  4. gatewayMerchantId is 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 skip3DS for 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:

  1. Webhook (POST /payments-api/v1/webhooks to register a URL once; Fynex posts PaymentCompleted — and PaymentRefunded on refunds — when state changes). See Webhooks.
  2. REST poll — GET /payments-api/v1/payments/{payment_id} with your bearer token. The payment_id path parameter is your externalOrderRef. Stop once lifecycleStatus is refunded, failed or cancelled (or captured if you don't need settlement progress). See Polling & SSE.
  3. 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 in Demo operational 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:

  1. 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.
  2. The customer's browser or device doesn't support Google Pay. isReadyToPay() returns false and the page (correctly) leaves the button hidden. Nothing to fix — the customer can use the card form.
  3. The customer has no saved cards in their Google account. isReadyToPay() may still return true, but the GP sheet shows an empty list when the customer opens it. Not a configuration bug.
  4. Environment mismatch. The deploy points at Google Pay's PRODUCTION environment but the merchant ID is only enrolled in TEST, 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 fresh externalOrderRef.
  • Empty failureStage with status: "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):

  1. Google Pay merchant ID registered in the Google Pay Business Console.
  2. Domains that render the Google Pay button whitelisted in the Google Pay Console.
  3. Production seller API token issued (Authorization: Bearer ...).
  4. google_pay enabled for the seller account in the Fynex dashboard.
  5. returnLinks configured (success / failure URLs).
  6. Webhook registered for final status delivery, or polling implemented via GET /payments/{payment_id}.

For the Decrypted Direct API, additionally:

  1. 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.