Deploying Stripe payments with Google Apps Script

today October 26, 2022

blog post hero


This post details my experience in deploying Stripe payments using Google Apps Script (GAS) in three different projects. The Stripe platform is easy to use, and when it's combined with GAS, launching a payment system is straightforward.

Interested in purchasing this script? Contact me

Business requirements

When designing the solution, I've had the following business requirements:

  • Make it easy to sell more products in the future.
  • Use a webhook to collect payment information from Stripe
  • Record payments in a Google Sheet
  • Email the customer an order confirmation message
  • Make it easy for the customer to download the product
  • Ensure that the order confirmation page works only for valid customers

High-level design

I'm sure there are many ways to implement Stripe payments on a website today, but here's what I ended up doing: A "Buy now" button on the website is linked to a Stripe payment link. When clicked, the Stripe checkout window opens.

Stripe checkout page

The payment link is set to return to the product page in case of a problem, and to redirect to the order confirmation page after a successful transaction.

I've configured Stripe to post the payment information to a webhook on my end.

The webhook is a Google Apps script that is deployed as a web app. The app collects the post payload and saves it in a Google Sheet. The app then sends an email to the customer's email address with the order information.

When Stripe redirects to the order confirmation page, it adds a session ID query parameter to the URL. I have client-side code that grabs the ID and sends it to the GAS web app. The app checks in the Google Sheet if this is a valid purchase ID. If so, then it sends the order information back to the client, which displays it on the page.

Both the email message and the order confirmation page include a download link to the product that the customer purchased.

Implementation pointers

There are a few things that I've learned from my three implementations of Stripe and Google Apps Script:

Since the webhook handles both the Stripe posts and the order confirmation posts (I prefer not to use "doGet" here), then it's comfortable to split the traffic using a query parameter:

function doPost(e) {
  if (e.parameter && e.parameter.wh == 1) {
    return handleWebhook_(e);
  if (e.parameter && e.parameter.c == 1) {
    return handleClient_(e);

In order to display the product name on the page (and in the email), the web app needs to call Stripe and fetch the order details. It then concatenates that information with the order (summary) information it originally receives and saves it in the sheet.

Every order needs an order id. I opted to use Utilities.getUuid() for that.

To display the correct download URL for the purchased product, the Google Sheet has a second tab for product information. The app looks up the product ID and gets the corresponding URL. The URL is included in the order confirmation email and page.

I wanted to protect the order confirmation that would prevent non-buyers from accessing the download link. To do so, when the page sends the Stripe session ID to the web app, the app looks up the ID in the orders sheet. If it doesn't find it (or if no ID is provided) then the app responds with 'invalid data supplied' message.

If the ID does exist in the sheet then the app checks if this order has already been fulfilled (ie, this page was reloaded or revisited). If so, then the app responds with 'Order already fulfilled. Please check your inbox for order details and download link' message.

If the order hasn't been already fulfilled then the app marks it as fulfilled in the sheet, and sends the order information back to the client. In my case, the app sends the entire HTML snippet to render on the page, as the page itself contains almost nothing except the Javascript code.

Productivity pointers

To speed up development, I captured the entire payload that Stripe sends to the webhook and saved it as a dev object in my script. I then created a function that calls doPost() with the object (as well as the routing query parameter mentioned above). That way I didn't have to regenerate Stripe calls to my webhook.

Another time-saver was to implement a proxy webhook for the order confirmation page. As I was developing the web app code, I didn't want to waste time re-deploying the app after every code change, so that the order confirmation page will always use the latest version.

Instead, I have a general-purpose proxy server that I built using Google Apps Script. This web app gets a routing query parameter and the payload. It calls the right function based on the routing parameter, passing it the payload. The function then uses URLFetchApp() to fetch the '/dev' URL of the web app that always points to the head deployment. Once the web app is ready to go, I deploy it, and updates the Javascript on the order confirmation page to call the web app instead of the proxy server.

Interested in purchasing this script? Contact me