πŸͺWebhook Situations

Webhook types

There are 2 types of webhooks that will need to be handled depending on the payment flow your platform has opted for. For this guide, we will refer to these as Direct Webhooks and Indirect Webhooks.

Indirect Webhooks

Indirect webhooks are webhooks that relate to your own stripe account, in the case of connected account charges, these are referred to in connect as "Destination Charges".

All destination charges and Indirect webhooks should use the standard Cashier event handling methods. (See Cashier Guide).

They should use the WebhookReceived handler.

<?php
 
namespace App\Providers;
 
use App\Listeners\StripeEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Cashier\Events\WebhookReceived;
 
class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
       WebhookReceived::class => [
          StripeEventListener::class,
       ],
    ];
}

Direct Webhooks

Direct webhooks refer to events that have occured within a connetced account. So for example a subscription that was created by the connected account or what is known in stripe as a Direct Charge where the funds are taken directly by the connected account.

All of these events will be sent to the connected endpoint that you registered via:

php artisan connect:webhook

And you should use the ConnectWebhookReceived that has been created by this plugin.

<?php
 
namespace App\Providers;
 
use App\Listeners\ConnectStripeEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Cashier\Events\WebhookReceived;
 
class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
       // HANDLE DIRECT WEBHOOK
       ConnectWebhookReceived::class => [
          ConnectStripeEventListener::class,
       ],
    ];
}

Then using the payload data you receive from the webhook, you can identify the connected account that it relates to, and perform a reverse model lookup of that account using the connected account ID.

$account = ConnectMapping::where([
    "stripe_account_Id" => $stripe_account_id
])->first();

$customer = ConnectCustomer::where([
    "stripe_account_Id" => $stripe_account_id,
    "stripe_customer_Id" => $customer_id
])->first();

Once you retrieve these records you can then reverse lookup the owning model. (E.G. User / Store / Customer)

$store = $account->model::find($account->model_id);

// OR FOR UUIDS

$store = $account->model::find($account->model_uuid);

Once you have the model with the relevant trait, you can update as you wish.

Handling Multi Tenant Applications

One of the major challenges faced with webhooks on more advanced stripe connect applications is handling ConnectBillable and ConnectCustomer models that exist and interact between a tenant and central application. Some would argue there are more elegant ways to handle this, such as building a centralised table that maps your tenants to your connected accounts, however by doing this, we feel that erodes the intended segregation that mutli tenant was meant to provide in the first place, and with that you lose various dats security benefits associated with keeping data separate and unlinked.

Our approach, all be it potentially less elegamt, is one that is the most secure and requires the least setup. For this approach we will utilize the meta data fields available on stripe.

This example is based on your application running on Tenancy for Laravel V3 (Stancl)

Step 1: Marking the entity with the tenant ID

The first step is adding the tenant ID to the entity that future webhooks will be referencing. Some examples are below:

// CREATING A CONNECTED ACCOUNT WITH TENANT ID

$store->createAsStripeAccount('express', [
            'country' => 'GB',
            'business_profile' => [
                "mcc" => "8398",
                "name" => $store->public_name,
                "url" => $store->website_url,
                "product_description" => "Website for " . $store->public_name
            ],
            'business_type' => 'business',
            'capabilities' => [
                'card_payments' => ['requested' => true],
                'transfers' => ['requested' => true],
            ],
            'metadata' => [
                "tenant" => tenant()->id // SET THE TENANT ID HERE
            ]
        ]);

// CREATING A DIRECT CHARGE WITH THE TENANT ID ATTACHED

$options = [
    'metadata' => [
        "tenant" => tenant()->id // SET THE TENANT ID HERE
    ]
];

$brand->createDirectCharge($amount, $brand->currency, $options);

// CREATING A SUBSCRIPTION WITH THE TENANT ID ATTACHED 
// BY FEEDING IT IN VIA THE OPTIONS VARIABLE

$subscription = $brand->createDirectSubscription($donor, $brand->subscription_price_id, $amount / 100, $options);

Step 2: Initialize Tenancy using Tenant ID on webhook

On the return of a webhook you should be able to now check for the tenant ID of the related entity.

If the tenant field exists, you know this is a model that exists within a tenant context. Once you have this tenant ID, you can initalize the tenancy without needing to look up the tenant.

if(isset($entity['metadata']['tenant'])){
    // RELATES TO THE TENANT CONTEXT
    tenancy()->initialize($entity['metadata']['tenant']);
}

Once the tenancy has initialized you will be within the correct context to lookup the model as above.

$account = ConnectMapping::where([
    "stripe_account_Id" => $stripe_account_id
])->first();

$customer = ConnectCustomer::where([
    "stripe_account_Id" => $stripe_account_id,
    "stripe_customer_Id" => $customer_id
])->first();

Last updated