Tutorial: Google Pay

This tutorial will guide you through the Snipcart's custom payment gateway feature. We'll see how to add a payment method that is not supported by default to your Snipcart store.
We'll be using Google Pay for this tutorial, which isn't supported out of the box by Snipcart.
Please note that this is only for demo purposes. You should update the code to fit your needs

Google Pay API PaymentRequest reference
Payment Request API reference

Before you get started, make sure you configured your merchant dashboard and have a basic understanding of how the Custom payment gateway works.

1. Creating the checkout page

The first step to integrate a custom payment gateway is to create the checkout page. This is the page the user will be redirected to when using the custom gateway. When redirecting to this page, add the publicToken query param.

ex: YOUR_CHECKOUT_URL.com?publicToken=<THE_TOKEN>

This is how our demo checkout page looks. It uses the Payment Request API.

see Github repo for more details

1.1 Fetch order information

Within the checkout page, you'll want to retrieve information about the order. To do so, you need to call our payment-session API endpoint. This will allow you to display order information on the checkout page.
Learn how to retrieve information about the payment session.

  // Get the payment session from Snipcart
  const fetchPaymentSession = async () => {
    const publicToken = new URLSearchParams(window.location.search).get('publicToken')
    try {
      const response = await axios
        .get(`https://payment.snipcart.com/api/public/custom-payment-gateway/payment-session?publicToken=${publicToken}`)
      paymentSession = response.data
      currency = paymentSession.invoice.currency
    } catch (e) {
      console.error(e)
    }
  }

1.2 Payment request API

​The payment request API is used to process the payments.

Create Google Pay Request Data
First, you need to create the Request data for Google Pay.

  // Creates the Google Pay Request Data
  // This is needed for the Payment Request Api
  const createGooglePayRequestData = () => ({
    environment: 'TEST',
    apiVersion: 2,
    apiVersionMinor: 0,
    merchantInfo: {
      // A merchant ID is available after approval by Google.
      // 'merchantId':'12345678901234567890',
      merchantName: 'Example Merchant'
    },
    allowedPaymentMethods: [{
      type: 'CARD',
      parameters: {
        allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"],
        allowedCardNetworks: ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"]
      },
      tokenizationSpecification: {
        type: 'PAYMENT_GATEWAY',
        // Check with your payment gateway on the parameters to pass.
        // @see {@link https://developers.google.com/pay/api/web/reference/request-objects#gateway}
        parameters: {
          'gateway': 'example',
          'gatewayMerchantId': 'exampleGatewayMerchantId'
        }
      }
    }]
  })

Create payment details
The payment details will contain information about the order. This includes the total, the items, and the currency. Use the previously fetched payment sessions to get this information.

  // Create the payment details
  const createPaymentDetails = () => ({
    total: {
      label: 'TOTAL',
      amount: {
        currency: currency,
        value: paymentSession.invoice.amount
      }
    },
    displayItems: paymentSession.invoice.items.map(i => {
      const isItem = i.type !== 'Discount' && i.type !== 'Tax' && i.type !== 'Shipping' // Filter items from discount, tax, and shipping
      const label = isItem ? `${i.name} x ${i.quantity}` : i.name // Add the quantity if needed
      return {
        label,
        amount: {
          value: i.amount,
          currency
        }
      }
    })
  })

Create the payment request
The following will instantiate a new Payment Request.

  // Create the payment request (Using the Payment Request API)
  const createPaymentRequest = () => {
    const supportedMethods = [
      { supportedMethods: 'https://google.com/pay', data: createGooglePayRequestData() },
    ]

    paymentDetails = createPaymentDetails()

    const options = {
      requestPayerEmail: true,
      requestPayerName: true
    }
    paymentRequest = new PaymentRequest(
      supportedMethods,
      paymentDetails,
      options
    )
  }

Bind the buy button to the Payment Request

  // Add the buy button Event Listener
  const bindBuyButton = () => {
    paymentRequest.canMakePayment() // Validate the payment request
      .then(function (result) {
        if (result) {
          document.getElementById('pay').addEventListener('click', onBuyClicked);
        }
      })
      .catch(function (err) {
        console.log(err)
      })
  }

Handle payment

// Add the event listener on the Buy button
  const onBuyClicked = async () => {
    try {
      const paymentRes = await paymentRequest.show()
      await handlePayment(paymentRes)
    } catch (e) {
      console.log(e)
    }
  }
  // Payment completed callback
  const handlePayment = async (paymentRes) => {
    try {
      const res = await axios.post(`/api/confirm-payment?sessionId=${paymentSession.id}`, paymentRes)
      paymentRes.complete('success')
      window.location.href = res.data.returnUrl
    } catch (e) {
      console.error(e)
    }
  }

You can see the full code on the Github repo

2. Payment methods endpoint

The next step to integrate a custom payment gateway is to create the payment methods endpoint. The publicToken is provided in the request body.

Make sure you validate the request was made by our API.

Payment methods webhook reference

async (req, res) => {
  if (req.body && req.body.publicToken) {
    try {
      // Validate the request was made by Snipcart
      await axios.get(`https://payment.snipcart.com/api/public/custom-payment-gateway/validate?publicToken=${req.body.publicToken}`)
      // Return the payment methods
      return res.json([{
        id: 'paymentrequest-custom-gateway',
        name: 'Google pay',
        checkoutUrl: 'https://paymentrequest-custom-gateway.snipcart.vercel.app',
        iconUrl: `https://paymentrequest-custom-gateway.snipcart.vercel.app/google_pay.png`
      }])
    }catch(e){
      // Couldn't validate the request
      console.error(e)
      return res.status(401).send()
    }
  }
  // No publicToken provided. This means the request was NOT made by Snipcart
  return res.status(401).send()
}

3. Confirm payment

This endpoint is used to validate the payment with Snipcart when your payment gateway approves it. It should be called with the payment information. This has to be done server-side since we don't want to leak our secret API Key.

Create payment reference

async (req, res) => {

  // TODO: Validate the request was approved by your payment gateway (in this case Google Pay)

  // Parse the gateway payment info to match Snipcart's schema
  // This will change depending on the payment gateway you choose
  const paymentSessionId = req.query.sessionId
  const data = {
    paymentSessionId,
    state: 'processed',
    transactionId: req.body.requestId,
    instructions: 'Your payment will appear on your statement in the coming days',
    links: {
      refunds: `https://paymentrequest-custom-gateway.snipcart.vercel.app/api/refund?transactionId=${req.body.requestId}`
    },
  }

  // Add authentification
  // This is the secret API key created in Snipcart's merchant dashboar
  const options = {
    headers: {
      Authorization: 'Bearer <YOUR_SECRET_API_KEY>'
    }
  }

  try{
    // Confirm payment with Snipcart
    const resp = await axios.post(`${process.env.PAYMENT_URL}/api/private/custom-payment-gateway/payment`,data, options)

    // ReturnUrl will redirect the user to the Order confirmation page of Snipcart
    return res.json({
      returnUrl: resp.data.returnUrl
    })
  }catch(e){
    console.error(e)
  }

  return res.status(500).send()
}

4. Refund (optional)

The following is called when refunding an order via the merchant dashboard.

Make sure you validate the request was made by our API.

Refund webhook reference

async (req, res) => {
  const { transactionId } = req.query
  try {
    // Validate the request was made by Snipcart
    await axios.get(`https://payment.snipcart.com/api/public/custom-payment-gateway/validate?publicToken=${req.body.publicToken}`)

    // TODO: Refund the order via the gateway

    return res.json({
      refundId: transactionId
    })
  } catch (e) {
    // Couldn't validate the request
    console.error(e)
    return res.status(401)
  }
}