Accept & send payments with Google Pay™

Moov allows you to accept card payments and send payouts with Google Pay. This step by step guide covers how you can start using Google Pay on the web using the open standard Google Pay API.

Google Pay is a fast way to move money with strong privacy and security. Use this guide to learn about Google's guidelines and follow a step by step process of adding a functional Google Pay button to accepting a payment or sending a payout.

Starting with Moov API version v2026.04.00, linking a Google Pay token returns every supported Google Pay payment method in a single response so you can choose the right flow for your use case:

Payment method type Directionality Use case
google-pay Payment Accept a card payment from a payer using Google Pay
push-to-google-pay Payout (OCT) Send a near real-time disbursement to a recipient's card via Google Pay
pull-from-google-pay Funding (AFT) Pull funds from a debit or prepaid card on file in Google Pay for approved use cases
The link Google Pay token endpoint was introduced in API version v2026.04.00 and always returns an array of payment methods. Filter the array by paymentMethodType to pick the paymentMethodID that matches your flow (google-pay, push-to-google-pay, or pull-from-google-pay).

Prerequisites

Before getting started, make sure your website follows Google's guidelines and that your server is set up accordingly.

Add Google Pay JavaScript library

Include the Google Pay JavaScript library:

<!-- Load the Google Pay JavaScript library -->
<script
  async src="https://pay.google.com/gp/p/js/pay.js"
  onload="onGooglePayLoaded()">
</script>

Add Google Pay button

Add a div element to your webpage and use the Google Pay documentation to customize the button type and to see best practices on using CSS. For disbursements, we recommend using the checkout button type from Google.

Here's sample code for adding a simple black Buy with Google Pay button, using the default size and corner radius:

<div id="gpay-button-container"></div>

Set up Google Pay PaymentsClient

After adding the button, you can set up the PaymentsClient. Start with a base configuration and modify it to your needs.

In the configuration, indicate Google Pay's gateway integration with Moov by setting:

  • tokenizationSpecification.parameters.gateway to "moov"
  • tokenizationSpecification.parameters.gatewayMerchantId to the merchant's Moov account ID (a UUID)
  • merchantInfo.merchantId to your 20-character Google Pay merchant ID
  • merchantInfo.merchantName to your merchant display name from Google

Moov supports the PAN_ONLY and CRYPTOGRAM_3DS auth methods. Moov does not support 3DS from a third-party provider.

To accept card payments, list all card networks you intend to accept. Moov supports AMEX, DISCOVER, MASTERCARD, and VISA for Google Pay pay-ins.

// Environment: TEST or PRODUCTION
// Configure for production once your implementation testing is complete: environment: 'PRODUCTION'
const googlePayEnv = 'TEST';

// Base configuration for all Google Pay payment data requests
const googlePayBaseConfiguration = {
  apiVersion: 2,
  apiVersionMinor: 0,
  allowedPaymentMethods: [
    {
      type: 'CARD',
      parameters: {
        allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
        allowedCardNetworks: ['AMEX', 'DISCOVER', 'MASTERCARD', 'VISA'],
        billingAddressRequired: true,
        billingAddressParameters: {
          format: 'FULL',
        },
      },
      tokenizationSpecification: {
        type: 'PAYMENT_GATEWAY',
        parameters: {
          gateway: 'moov',
          gatewayMerchantId: 'merchants-moov-account-id'
        }
      },
    },
  ],
  merchantInfo: {
    merchantId: '01234567890123456789',
    merchantName: 'Example Merchant',
  },
  emailRequired: true,
};
Including billingAddressRequired: true with format: 'FULL' may reduce declines and avoid interchange downgrades.

For disbursements (OCT), restrict allowedCardNetworks to networks that support push-to-card (MASTERCARD, VISA). Set existingPaymentMethodRequired: true when creating the button so that Google Pay only surfaces the button when the recipient has an eligible card on file.

// Environment: TEST or PRODUCTION
const googlePayEnv = 'TEST';

// Base configuration for Google Pay disbursement requests
const googlePayBaseConfiguration = {
  apiVersion: 2,
  apiVersionMinor: 0,
  allowedPaymentMethods: [
    {
      type: 'CARD',
      parameters: {
        allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
        allowedCardNetworks: ['MASTERCARD', 'VISA'],
        billingAddressRequired: true,
        billingAddressParameters: {
          format: 'FULL',
        },
      },
      tokenizationSpecification: {
        type: 'PAYMENT_GATEWAY',
        parameters: {
          gateway: 'moov',
          gatewayMerchantId: 'merchants-moov-account-id'
        }
      },
    },
  ],
  merchantInfo: {
    merchantId: '01234567890123456789',
    merchantName: 'Example Merchant',
  },
  emailRequired: true,
};
AMEX and DISCOVER cards are not eligible for push-to-card transfers and must be excluded from allowedCardNetworks on disbursement flows.

Now create the PaymentsClient:

let googlePayClient = null;

// Create a new PaymentsClient instance
function getGooglePaymentsClient() {
  if (googlePayClient === null) {
    googlePayClient = new google.payments.api.PaymentsClient({
      environment: googlePayEnv,
    });
  }

  return googlePayClient;
}

Set up event handlers for page load and button click

Use isReadyToPay() to determine whether the customer is ready to pay, then render the Google Pay button:

let paymentRequest = null;

function onGooglePayLoaded() {
  paymentRequest = Object.assign({}, googlePayBaseConfiguration);

  getGooglePaymentsClient()
    .isReadyToPay(paymentRequest)
    .then((res) => {
      if (res.result) {
        const button = getGooglePaymentsClient().createButton({
          onClick: onGooglePayButtonClick
        });
        document.getElementById('gpay-button-container').appendChild(button);
      } else {
        console.log("Google Pay is not ready for this user.");
      }
    })
    .catch(console.error);
}

// Set the transactionInfo when the button is clicked and open the Google Pay sheet
function onGooglePayButtonClick() {
  paymentRequest.transactionInfo = {
    totalPriceStatus: 'FINAL',
    totalPrice: '12.34',
    currencyCode: 'USD',
    countryCode: 'US',
  };
  googlePayClient.loadPaymentData(paymentRequest)
    .then(onGooglePayPaymentLoaded)
    .catch(console.error);
}

For disbursements, pass existingPaymentMethodRequired: true to isReadyToPay() so Google Pay only shows the button when the recipient has a saved card eligible for disbursements.

let paymentRequest = null;

function onGooglePayLoaded() {
  paymentRequest = Object.assign({}, googlePayBaseConfiguration, {
    existingPaymentMethodRequired: true
  });

  getGooglePaymentsClient()
    .isReadyToPay(paymentRequest)
    .then((res) => {
      if (res.result) {
        const button = getGooglePaymentsClient().createButton({
          buttonType: 'checkout',
          onClick: onGooglePayButtonClick
        });
        document.getElementById('gpay-button-container').appendChild(button);
      } else {
        console.log("Google Pay is not ready for this recipient.");
      }
    })
    .catch(console.error);
}

// Set the transactionInfo for the disbursement and open the Google Pay sheet
function onGooglePayButtonClick() {
  paymentRequest.transactionInfo = {
    totalPriceStatus: 'FINAL',
    totalPriceLabel: 'Payout',
    totalPrice: '12.34',
    currencyCode: 'USD',
    countryCode: 'US',
  };
  googlePayClient.loadPaymentData(paymentRequest)
    .then(onGooglePayPaymentLoaded)
    .catch(console.error);
}

Moov only supports USD for the United States (US).

Once the customer selects their payment method, Google Pay returns a PaymentData response. Send the paymentMethodData property, unmodified, to Moov. Moov links the Google Pay token to the payer or recipient's Moov account using the Google Pay token POST endpoint.

The request requires two body fields:

  • merchantAccountID: The Moov account ID of the merchant accepting the payment or sending the payout. This must match the gatewayMerchantId value passed to Google Pay.
  • paymentMethodData: The paymentMethodData property from Google Pay's PaymentData response, passed through unmodified.

The response is an array of payment methods — one per supported Google Pay flow that could be created for the linked token. Filter the array by paymentMethodType to pick the paymentMethodID you need for your transfer.

To accept a payment, use the google-pay payment method as the transfer source. Set the Moov payer account as the path parameter (accountID) and the merchant Moov account ID in the request body.

async function linkGooglePayPayInToken(paymentResponse) {
  const url = `https://api.moov.io/accounts/${payerAccountID}/google-pay/tokens`;

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${bearerToken}`,
      'X-Moov-Version': 'v2026.04.00'
    },
    body: JSON.stringify({
      merchantAccountID: 'merchants-moov-account-id',
      paymentMethodData: paymentResponse.paymentMethodData
    })
  });

  if (!response.ok) throw new Error('Failed to link Google Pay token');
  const paymentMethods = await response.json();
  return paymentMethods.find(
    (pm) => pm.paymentMethodType === 'google-pay'
  )?.paymentMethodID;
}

For disbursements, use the push-to-google-pay payment method (OCT) as the transfer destination. Set the Moov recipient account as the path parameter (accountID) and the merchant Moov account ID in the request body. For approved AFT use cases, pull-from-google-pay will also be present in the response.

async function linkGooglePayPayoutToken(paymentResponse) {
  const url = `https://api.moov.io/accounts/${recipientAccountID}/google-pay/tokens`;

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${bearerToken}`,
      'X-Moov-Version': 'v2026.04.00'
    },
    body: JSON.stringify({
      merchantAccountID: 'merchants-moov-account-id',
      paymentMethodData: paymentResponse.paymentMethodData
    })
  });

  if (!response.ok) throw new Error('Failed to link Google Pay token');
  const paymentMethods = await response.json();
  return paymentMethods.find(
    (pm) => pm.paymentMethodType === 'push-to-google-pay'
  )?.paymentMethodID;
}
The link Google Pay token endpoint requires the /accounts/{accountID}/cards.write scope on the access token, where accountID is the Moov account ID of the payer (for pay-ins) or the recipient (for payouts). The payer or recipient account must be created before the token is requested.

Create a transfer & dismiss payment sheet

Use the paymentMethodID filtered from the link-token response as the transfer source (for pay-ins) or destination (for payouts) when calling Moov's create transfer POST endpoint.

Use the X-Wait-For header on the transfer request to receive a synchronous response so you can close out the payment sheet.

Google Pay pay-ins are created the same as other card payments — the google-pay payment method is the transfer source. To learn more about card payments with Moov, read our accept card payments use case guide.

async function onGooglePayPaymentLoaded(paymentResponse) {
  try {
    const paymentMethodID = await linkGooglePayPayInToken(paymentResponse);
    if (!paymentMethodID) throw new Error('Unable to link Google Pay token');

    // Pseudo-code: instruct your server to make the transfer
    const moovTransferResponse = await fetch('/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ paymentMethodID, amount: 12.34 })
    }).then((r) => r.json());

    if (moovTransferResponse.source.cardDetails.status === 'confirmed') {
      // Success
    } else {
      // Decline / failure
    }
  } catch (error) {
    console.error(error);
  }
}

Google Pay payouts are created the same as other push to card transfers — the push-to-google-pay payment method is the transfer destination, and the source is a Moov wallet with sufficient funds. The source account must have the send-funds.push-to-card capability enabled for funds disbursement use cases.

async function onGooglePayPaymentLoaded(paymentResponse) {
  try {
    const paymentMethodID = await linkGooglePayPayoutToken(paymentResponse);
    if (!paymentMethodID) throw new Error('Unable to link Google Pay token');

    // Pseudo-code: instruct your server to make the payout
    const moovTransferResponse = await fetch('/payout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ paymentMethodID, amount: 12.34 })
    }).then((r) => r.json());

    if (moovTransferResponse.destination.cardDetails.status === 'completed') {
      // Success
    } else {
      // Decline / failure
    }
  } catch (error) {
    console.error(error);
  }
}
Moov has a transfer limit of $50,000 per push-to-google-pay transaction. Network velocity limits also apply — see the push to card guide for details.

Seeing it all together

Here's a sample that synthesizes the steps above all in one place. Switch tabs to see the full flow for either accepting a payment or sending a payout.

<div id="gpay-button-container"></div>
<script type="text/javascript">
  //=============================================================================
  // Configuration
  //=============================================================================

  // Environment: TEST or PRODUCTION
  const googlePayEnv = 'TEST';

  // Moov account ID of the payer (cardholder)
  const payerAccountID = 'payer-uuid';

  // Moov account ID of the merchant accepting the payment
  const merchantAccountID = 'merchants-moov-account-id';

  // Access token with the /accounts/{payerAccountID}/cards.write scope
  const bearerToken = 'my token';

  const googlePayBaseConfiguration = {
    apiVersion: 2,
    apiVersionMinor: 0,
    allowedPaymentMethods: [
      {
        type: 'CARD',
        parameters: {
          allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
          allowedCardNetworks: ['AMEX', 'DISCOVER', 'MASTERCARD', 'VISA'],
          billingAddressRequired: true,
          billingAddressParameters: { format: 'FULL' },
        },
        tokenizationSpecification: {
          type: 'PAYMENT_GATEWAY',
          parameters: {
            gateway: 'moov',
            gatewayMerchantId: merchantAccountID
          }
        },
      },
    ],
    merchantInfo: {
      merchantId: '01234567890123456789',
      merchantName: 'Example Merchant',
    },
    emailRequired: true,
  };

  Object.freeze(googlePayBaseConfiguration);

  //========================
  // Google Payments Client
  //========================
  let googlePayClient = null;

  function getGooglePaymentsClient() {
    if (googlePayClient === null) {
      googlePayClient = new google.payments.api.PaymentsClient({
        environment: googlePayEnv,
      });
    }
    return googlePayClient;
  }

  //================
  // Event Handlers
  //================
  let paymentRequest = null;

  function onGooglePayLoaded() {
    paymentRequest = Object.assign({}, googlePayBaseConfiguration);

    getGooglePaymentsClient()
      .isReadyToPay(paymentRequest)
      .then((res) => {
        if (res.result) {
          const button = getGooglePaymentsClient().createButton({
            onClick: onGooglePayButtonClick
          });
          document.getElementById('gpay-button-container').appendChild(button);
        } else {
          console.log("Google Pay is not ready for this user.");
        }
      })
      .catch(console.error);
  }

  function onGooglePayButtonClick() {
    paymentRequest.transactionInfo = {
      totalPriceStatus: 'FINAL',
      totalPrice: '12.34',
      currencyCode: 'USD',
      countryCode: 'US',
    };
    googlePayClient.loadPaymentData(paymentRequest)
      .then(onGooglePayPaymentLoaded)
      .catch(console.error);
  }

  async function onGooglePayPaymentLoaded(paymentResponse) {
    try {
      // Link the Google Pay token to the payer's Moov account.
      // The response is an array of payment methods; filter by `google-pay`.
      const linkResponse = await fetch(
        `https://api.moov.io/accounts/${payerAccountID}/google-pay/tokens`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${bearerToken}`,
            'X-Moov-Version': 'v2026.04.00'
          },
          body: JSON.stringify({
            merchantAccountID,
            paymentMethodData: paymentResponse.paymentMethodData
          })
        }
      );
      if (!linkResponse.ok) throw new Error('Failed to link Google Pay token');
      const paymentMethods = await linkResponse.json();
      const paymentMethodID = paymentMethods.find(
        (pm) => pm.paymentMethodType === 'google-pay'
      )?.paymentMethodID;

      // Pseudo-code: instruct your server to make the transfer.
      // X-Wait-For: rail-response is needed to receive a synchronous result.
      const moovTransferResponse = await fetch('/pay', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ paymentMethodID, amount: 12.34 })
      }).then((r) => r.json());

      if (moovTransferResponse.source.cardDetails.status === 'confirmed') {
        // Success
      } else {
        // Decline / failure
      }
    } catch (error) {
      console.error(error);
    }
  }
</script>

<!-- Load the Google Pay JavaScript library -->
<script
  async src="https://pay.google.com/gp/p/js/pay.js"
  onload="onGooglePayLoaded()">
</script>
<div id="gpay-button-container"></div>
<script type="text/javascript">
  //=============================================================================
  // Configuration
  //=============================================================================

  const googlePayEnv = 'TEST';

  // Moov account ID of the recipient (cardholder receiving the funds)
  const recipientAccountID = 'recipient-uuid';

  // Moov account ID of the merchant sending the payout
  const merchantAccountID = 'merchants-moov-account-id';

  // Access token with the /accounts/{recipientAccountID}/cards.write scope
  const bearerToken = 'my token';

  const googlePayBaseConfiguration = {
    apiVersion: 2,
    apiVersionMinor: 0,
    allowedPaymentMethods: [
      {
        type: 'CARD',
        parameters: {
          allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
          allowedCardNetworks: ['MASTERCARD', 'VISA'],
          billingAddressRequired: true,
          billingAddressParameters: { format: 'FULL' },
        },
        tokenizationSpecification: {
          type: 'PAYMENT_GATEWAY',
          parameters: {
            gateway: 'moov',
            gatewayMerchantId: merchantAccountID
          }
        },
      },
    ],
    merchantInfo: {
      merchantId: '01234567890123456789',
      merchantName: 'Example Merchant',
    },
    emailRequired: true,
    existingPaymentMethodRequired: true
  };

  Object.freeze(googlePayBaseConfiguration);

  let googlePayClient = null;

  function getGooglePaymentsClient() {
    if (googlePayClient === null) {
      googlePayClient = new google.payments.api.PaymentsClient({
        environment: googlePayEnv,
      });
    }
    return googlePayClient;
  }

  let paymentRequest = null;

  function onGooglePayLoaded() {
    paymentRequest = Object.assign({}, googlePayBaseConfiguration);

    getGooglePaymentsClient()
      .isReadyToPay(paymentRequest)
      .then((res) => {
        if (res.result) {
          const button = getGooglePaymentsClient().createButton({
            buttonType: 'checkout',
            onClick: onGooglePayButtonClick
          });
          document.getElementById('gpay-button-container').appendChild(button);
        } else {
          console.log("Google Pay is not ready for this recipient.");
        }
      })
      .catch(console.error);
  }

  function onGooglePayButtonClick() {
    paymentRequest.transactionInfo = {
      totalPriceStatus: 'FINAL',
      totalPriceLabel: 'Payout',
      totalPrice: '12.34',
      currencyCode: 'USD',
      countryCode: 'US',
    };
    googlePayClient.loadPaymentData(paymentRequest)
      .then(onGooglePayPaymentLoaded)
      .catch(console.error);
  }

  async function onGooglePayPaymentLoaded(paymentResponse) {
    try {
      // Link the Google Pay token to the recipient's Moov account.
      // The response is an array of payment methods; filter by `push-to-google-pay`.
      const linkResponse = await fetch(
        `https://api.moov.io/accounts/${recipientAccountID}/google-pay/tokens`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${bearerToken}`,
            'X-Moov-Version': 'v2026.04.00'
          },
          body: JSON.stringify({
            merchantAccountID,
            paymentMethodData: paymentResponse.paymentMethodData
          })
        }
      );
      if (!linkResponse.ok) throw new Error('Failed to link Google Pay token');
      const paymentMethods = await linkResponse.json();
      const paymentMethodID = paymentMethods.find(
        (pm) => pm.paymentMethodType === 'push-to-google-pay'
      )?.paymentMethodID;

      // Pseudo-code: instruct your server to make the payout.
      // X-Wait-For: rail-response is needed to receive a synchronous result.
      const moovTransferResponse = await fetch('/payout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ paymentMethodID, amount: 12.34 })
      }).then((r) => r.json());

      if (moovTransferResponse.destination.cardDetails.status === 'completed') {
        // Success
      } else {
        // Decline / failure
      }
    } catch (error) {
      console.error(error);
    }
  }
</script>

<!-- Load the Google Pay JavaScript library -->
<script
  async src="https://pay.google.com/gp/p/js/pay.js"
  onload="onGooglePayLoaded()">
</script>

For more information, refer to Google's documentation and the Google Pay PaymentData response reference.

Summary Beta