How to Implement Stripe Webhooks for Multi-Tenanted System?

I need to implement Stripe webhooks for my app... but my clients all have their own Stripe account... so when the webhook arrives, my app will have no idea which client it came from and my app will have a range of webhook keys to match up with the call.

I'd love some pointers about how to handle this in my PHP based app, either the Wappler way or otherwise!

I sense the webhook key will be in the header of the incoming API call... is there a way I can access that using Wappler to verify it?

I have written a very simple API using a Server Action... when I call it from the browser it runs and just returns the 'Hello World!' I had expected, but when Stripe calls it then the call fails... so I guess there is some authorisation thing going in the header which I need to connect to!

Many thanks,
Antony.

Stripe Connected Accounts might be the way to go:

I already have lots of customers working with the same integration method as Wappler has, so I definitely need to implement the webhooks!

Let me see if I can capture everything I did to make this happen, for standard stripe accounts and connected, but this is of course using Node.

What is described below has 2 main goals, 1 support a second standard account (we were transitioning from one to another) and for both of those, support connected accounts.

In the app/webhooks folder there is by default stripe, but you can add additional empty folders and these will each serve as a webhook endpoint.

For me, it is these four (stripe being the parent with all the actual workflows):

Screenshot 2024-11-14 at 4.42.37 PM

Next in app/lib/webhooks there are js files, one for each of the named folders above:

Screenshot 2024-11-14 at 4.44.14 PM

The contents of the js files is modified to change the secretKey and endpointSecret, along with the endpoint folder created above

Here is the default stripe.js:

const webhook = require('../core/webhook');
const config = require('../setup/config');
const fs = require('fs-extra')

if (fs.existsSync('app/webhooks/stripe')) {
    const stripe = require('stripe')(config.stripe.secretKey);
    const endpointSecret = config.stripe.endpointSecret;

    exports.handler = webhook.createHandler('stripe', (req, res, next) => {
        const sig = req.headers['stripe-signature'];

        try {
            stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
        } catch (err) {
            res.status(400).send(`Webhook Error: ${err.message}`);
            return false;
        }

        // return the action name to execute
        return req.body.type;
    });
}

And here is a modified version using environment variables for the secrets:

const webhook = require('../core/webhook');
const config = require('../setup/config');
const fs = require('fs-extra')

if (fs.existsSync('app/webhooks/stripe_connect_ui')) {
    const stripe = require('stripe')(process.env.STRIPE_SECRET_UI);
    const endpointSecret = process.env.STRIPE_WEBHOOK_CONNECT_SECRET_UI;

    exports.handler = webhook.createHandler('stripe', (req, res, next) => {
        const sig = req.headers['stripe-signature'];

        try {
            stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
        } catch (err) {
            res.status(400).send(`Webhook Error: ${err.message}`);
            return false;
        }

        // return the action name to execute
        return req.body.type;
    });
}

So then we get to the actual webhook workflows. There will only be a single stripe folder so the logic is done all within a single webhook for any you need to support.

Screenshot 2024-11-14 at 4.59.02 PM

If the code will be the same for everybody, then you just need something to determine which tenant, be it the endpoint or something in the Stripe POST values.

For example a connected account will always have a $_POST.account value, which you can use to lookup in your db and process accordingly. I just use a conditional statement to run different code, based on the event being a standard vs connected account.

You then create at Stripe the webhook endpoints you need for both the standard and connected accounts.

You can see 2 of them here (2 are on another Stripe account):

Screenshot 2024-11-14 at 5.00.36 PM

This goes outside the standard Wappler support, so heavy testing using the Test settings of Stripe is important.

While I'm here, don't forget that webhooks are not guaranteed to come in any particular order, nor are they guaranteed to only be sent once, so use the event id's to make sure you are processing properly. And if you have a long running logic, don't do it in the webhook, log the event in your db and use a queue to process...webhooks should report back a 200 response quickly.

3 Likes

@mebeingken … you superstar! :star2:

I so appreciate you taking the time to explain that… it all makes sense now…

I’ll have a fiddle around with it!

1 Like

I like that A LOT!

1 Like