← Back to Home
Stripe Webhook + GCP Functions Framework (Python) cover image

Stripe Webhook + GCP Functions Framework (Python)

This took a couple of days of messing around so decided to make a post out of it.

Here is a minimal enough example repo using Terraform and GCP Functions Framework to build a GCP Python function that will receive a Stripe webhook event, perform signature verification, and then just print the event. You can adapt and build whatever logic you want then on top of this.

TL DR; if you are looking at the stripe docs and cant figure out why you can run your python function locally but the signature verification fails once deployed to GCP functions (with this generic error message error.SignatureVerificationError( stripe.error.SignatureVerificationError: No signatures found matching the expected signature for payload) it could be that you need to replace payload = request.data with payload = request.data.decode('utf-8'). This i found out after a day or two thanks to this SO comment.

Most of what might be useful is in the repo readme, but below i’ll quickly walk through the structure and moving parts.

Repo structure

Here are the main folders and files involved. Greyed out files are those that might have sensitive info and so are part of the `.gitignore` and so not to be committed to source control. I’ll quickly walkthrough each folder and important files below.

Run function locally

To run the function locally we can use the functions framework cli like below:

# run function locally in debug mode on port 8081
functions-framework --source=./python-functions/stripe_webhook/main.py \
  --target=stripe_webhook \
  --debug \
  --port=8081

This should return something like below to show the function running locally on port 8081 (you can use whatever port you want).

(venv) PS > functions-framework --source=./python-functions/stripe_webhook/main.py \
  --target=stripe_webhook \
  --debug \
  --port=8081
 * Serving Flask app 'stripe_webhook'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8081
Press CTRL+C to quit
 * Restarting with watchdog (windowsapi)
 * Debugger is active!
 * Debugger PIN: XXX-XXX

Stripe CLI

Set up local forwarding

Once the function is running locally you can use the stripe cli to (1) forward events to a local endpoint and then (2) trigger some test events to see the response.

# once function is running locally you can forward events to local endpoint
stripe listen --forward-to localhost:8081

You should see something like this when local forwarding is set up:

PS C:\Users\andre> stripe listen --forward-to localhost:8081
> Ready! You are using Stripe API Version [2022-11-15]. Your webhook signing secret is xxx_xxxxxx (^C to quit)

Create some test events

# create a test event
stripe trigger payment_intent.succeeded

You should see something like this for a successful test event creation:

PS C:\Users\andre> stripe trigger payment_intent.succeeded
Setting up fixture for: payment_intent
Running fixture for: payment_intent
Trigger succeeded! Check dashboard for event details.

If the function as been invoked successfully then in the window from step 1 above you should see something like this:

PS C:\Users\andre> stripe listen --forward-to localhost:8081
> Ready! You are using Stripe API Version [2022-11-15]. Your webhook signing secret is xxx_xxxxxx (^C to quit)
2022-12-22 12:31:10   --> charge.succeeded [evt_3MHnvQFE3Qfj39xW1UE7UhT6]
2022-12-22 12:31:10   --> payment_intent.succeeded [evt_3MHnvQFE3Qfj39xW1vzt9gAn]
2022-12-22 12:31:10   --> payment_intent.created [evt_3MHnvQFE3Qfj39xW1KR01wQ8]
2022-12-22 12:31:10  <-- [200] POST http://localhost:8081 [evt_3MHnvQFE3Qfj39xW1UE7UhT6]
2022-12-22 12:31:11  <-- [200] POST http://localhost:8081 [evt_3MHnvQFE3Qfj39xW1vzt9gAn]
2022-12-22 12:31:11  <-- [200] POST http://localhost:8081 [evt_3MHnvQFE3Qfj39xW1KR01wQ8]

Finally in the window where you triggered the functions framework to run you should just see the a json string with all the event info itself.

(venv) PS C:\Users\andre\Documents\repos\stripe-webhook-gcp-function> functions-framework --source=./python-functions/stripe_webhook/main.py --target=stripe_webhook --debug --port=8081
 * Serving Flask app 'stripe_webhook'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8081

Press CTRL+C to quit
 * Restarting with watchdog (windowsapi)
 * Debugger is active!
 * Debugger PIN: 107-679-323
{"id": "evt_3MHnvQFE3Qfj39xW1UE7UhT6", "object": "event", "api_version": "2022-11-15", "created": 1671712265, "data": {"object": {"id": "ch_3MHnvQFE3Qfj39xW1ljeXQ0U", "object": "charge", "amount": 2000, "amount_captured": 2000, "amount_refunded": 0, 
...
"name": "Jenny Rosen", "phone": null, "tracking_number": null}, "source": null, "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "requires_payment_method", "transfer_data": null, "transfer_group": null}}, "livemode": false, "pending_webhooks": 4, "request": {"id": "req_BsagVXD6LQwqjH", "idempotency_key": "e36a0855-a9e6-441a-b9ca-181632fd43ad"}, "type": "payment_intent.created"}
127.0.0.1 - - [22/Dec/2022 12:31:11] "POST / HTTP/1.1" 200 -

Thats it

That’s it, you can then add whatever additional logic you want to handle specific stripe webhook events and control what events make it to this webhook from within the stripe ui itself as needed.

The developer tools and experience with Stripe is really good and being able to so easily run a cloud function locally with realistic test data from stripe using these two cli’s (stripe and functions-framework) is really nice, even if it did take me a few days to realize I needed to use that .decode('utf-8') once the function was deployed to GCP cloud - there’s always going to be something that trips you up a little :)

← Back to Home