Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

UUID's avatar
Level 6

I have a problem I have one website but two webhooks endpoints

I have a problem I have one website but two webhooks endpoints when call one endpoint the other also call and this not right I don't why that happend

every webhooks has STRIPE_SECRET

ex: https://website.com/webhooks/stripe
https://website.com/extensions/stripe

there both use checkout.session.completed

but every endpoint have different route and different controller

route one 
Route::post('/rentals/purchase/{plan}', RentalCheckoutController::class)->name('rentals.checkout')->middleware('auth');

Route::get('/rentals/purchase/success', RentalCheckoutSuccessController::class)->name('rentals.success')->middleware('auth');

Route::post('/webhooks/stripe',StripeWebhookController::class);

----------------------------------------------
route two 
Route::post('/rentals/purchase/extension/{rental}',[RentalExtensionCheckoutController::class,'index'])->name('rentals.extension.checkout')->middleware('auth');

Route::get('/rentals/purchase/extension/success', RentalExtensionSuccessController::class)->name('rentals.extension.success')->middleware('auth');

Route::post('/extensions/stripe',StripeWebhookExtensionController::class);


this is the handleCheckoutSessionCompleted for one controller 
protected function handleCheckoutSessionCompleted(array $payload)
    {
        $paymentStatus = data_get($payload, 'data.object.payment_status', null);
        $priceInCents = $payload['data']['object']['amount_subtotal'];
        $priceInEur = $priceInCents / 100;

        // Log::info($payload);

        $session_id = $payload['data']['object']['id'];
        $metadata = $payload['data']['object']['metadata'];
        $rental_uuid = $metadata['rental_uuid'];
    
        $rental = Rental::where('uuid', $rental_uuid)->first();
    
        if ($rental) {
            // Update the end_time
            $newEndTime = Carbon::parse($metadata['end_time']);
            $rental->end_time = $newEndTime;
            $rental->save();
    
            // Create a new additional_rentals record
            AdditionalRental::create([
                'user_id' => $rental->user_id,
                'rental_uuid' => $rental->uuid,
                'end_time' => $newEndTime,
                'price' => $priceInEur, 
                'datetime' => now(),
            ]);
        }
    }
}


this is the middlerware for one of them
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Stripe\WebhookSignature;
use Symfony\Component\HttpFoundation\Response;

class VerifyStripeWebhookExtensionSecret
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {

        $endpoint_secret = env('STRIPE_WEBHOOK_SECRET2');


            try {
                WebhookSignature::verifyHeader(
                    $request->getContent(),
                    $request->header('Stripe-signature'),
                    $endpoint_secret



                );
             
            } catch (SignatureVerificationException $exception) {
                // Invalid payload
                throw new AccessDeniedHttpException($exception->getMessage(),$exception);
            }


        return $next($request);
    }

}




#stripeWebhookController
<?php

namespace App\Http\Controllers;

use Dompdf\Dompdf;
use App\Models\Box;
use Ramsey\Uuid\Uuid;
use App\Models\Rental;
use App\Models\System;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Mail\PaymentConfirmation;
use Illuminate\Support\Facades\Mail;
use App\Http\Middleware\VerifyStripeWebhookSecret;

class StripeWebhookController extends Controller
{
    //

    public function __construct()
    {
        $this->middleware(VerifyStripeWebhookSecret::class);
    }

    public function __invoke(Request $request)
    {
        $payload=json_decode($request->getContent(),true);

        // \Log::info($payload);

        $method='handle'. Str::studly(str_replace('.','_',$payload['type']));

        if(method_exists($this,$method)){
            $response=$this->{$method}($payload);

            return $response;
        }

        return new Response();
    }

    protected function handleCheckoutSessionCompleted(array $payload)
    {

            //  \Log::info($payload);
      
    
      
      
    
    
        $paymentStatus = data_get($payload, 'data.object.payment_status', null);
        $priceInCents = $payload['data']['object']['amount_subtotal'];
        $priceInEur = $priceInCents / 100;
    
        // Create a new rental record in the database for the found system
        $rental = Rental::create([
            // 'email' => $email,
            'tenant_id'=>$payload['data']['object']['metadata']['tenant_id'],
            'system_id' => $payload['data']['object']['metadata']['system_id'],
            "user_id" => $payload['data']['object']['metadata']['user_id'],
            'session_id' => $payload['data']['object']['id'],
            'price' => $priceInEur ,
            'status' => $paymentStatus === 'paid' ? 'paid' : null,
            'plan_id'=>$payload['data']['object']['metadata']['plan_id'],
            'box_id' => $payload['data']['object']['metadata']['box_id'],
            'uuid' => Uuid::uuid4()->toString(),
            'duration' => $payload['data']['object']['metadata']['duration'],
            'start_time' => $payload['data']['object']['metadata']['start_time'],
            'end_time' => $payload['data']['object']['metadata']['end_time'],
            'pincode' =>  mt_rand(100000000, 999999999),
        ]);

        $boxId = $payload['data']['object']['metadata']['box_id'];
        $box = Box::findOrFail($boxId);
        $box->rental_uuid = $rental->uuid;
        $box->occupied = true;
        $box->save();

        $dompdf = new Dompdf();
        $dompdf->loadHtml(view('emails.payment_confirmation', ['rental' => $rental])->render());
   
        $dompdf->render();
        $pdfContents = $dompdf->output();
   
        // Save the PDF file to a temporary location
        $pdfPath = storage_path('app/tmp/rechnung.pdf');
        file_put_contents($pdfPath, $pdfContents);
        // Send the pin code email
     
   
       Mail::to($rental->user->email)->send(new PaymentConfirmation($rental));
        // Cleanup the temporary PDF file
        unlink($pdfPath);
    
      
    }
    
    
}


-checkoutControler for one of them


        \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
        $customer = \Stripe\Customer::create([
            'email' => $user->email, 
            
         
        ]);

        
        \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));

          
        $lineItems[] = [
            'price_data' => [
                'currency' => 'eur',
                'product_data' => [
                    'name' =>'Mietgebühr:'. ' ' .$plan->name,
                    // 'description' => 'test',
                  
                ],
                'unit_amount' => $plan->price * 100,
            ],
            'quantity' => 1,
        ];
        
        $session = \Stripe\Checkout\Session::create([
            'line_items' => $lineItems,
            'mode' => 'payment',
            'success_url' => route('rentals.success', []) , 
            'cancel_url' => route('home', []),
            'customer' => $customer->id,
            ['metadata' =>
            [
               
                "user_id" => $user->id,
                "tenant_id" => $tenantId,
                'plan_id' => $plan->id,
                'system_id' => $systemId,
                'box_id' => $boxId,
                'rental_uuid' => $rental->uuid, 
                'end_time' => $end_time,
                 
                  
            ],
            'metadata'=>[
               
                
                'rental_uuid' => $rental->uuid, 
                 'end_time' => $end_time,
                 

            ]
            ],
          
           
          
        ]);
    
     
    
        return redirect($session->url);
    
    }
}

0 likes
11 replies
LaryAI's avatar
Level 58

The issue you're facing is that when one webhook endpoint is called, the other endpoint is also triggered. This is likely because both endpoints are using the same event, checkout.session.completed, and both endpoints are being called when this event occurs.

To resolve this issue, you can modify the webhook routes and controllers to ensure that each endpoint is only triggered for its respective event.

Here's how you can modify the routes and controllers:

  1. For the first endpoint (/webhooks/stripe), update the route to include a specific event parameter:
Route::post('/webhooks/stripe/{event}', [StripeWebhookController::class, 'handle'])->name('webhooks.stripe');
  1. Update the handleCheckoutSessionCompleted method in the StripeWebhookController to check for the specific event:
protected function handleCheckoutSessionCompleted(array $payload)
{
    // Check if the event is checkout.session.completed
    if ($payload['type'] === 'checkout.session.completed') {
        // Your existing code for handling the event
        // ...
    }
}
  1. Repeat the above steps for the second endpoint (/extensions/stripe):
Route::post('/extensions/stripe/{event}', [StripeWebhookExtensionController::class, 'handle'])->name('extensions.stripe');
  1. Update the handleCheckoutSessionCompleted method in the StripeWebhookExtensionController to check for the specific event:
protected function handleCheckoutSessionCompleted(array $payload)
{
    // Check if the event is checkout.session.completed
    if ($payload['type'] === 'checkout.session.completed') {
        // Your existing code for handling the event
        // ...
    }
}

By adding a specific event parameter to the webhook routes and checking for the event type in the controllers, you can ensure that each endpoint is only triggered for its respective event.

Snapey's avatar

Duplicate question that has already been answered, but the OP did not like the answers given.

UUID's avatar
Level 6

@Snapey You did not answer anything, and if you do not know the answer, do not answer, and when you talk to people, you speak with respect, as it is not your home.

Snapey's avatar

@Ahmadalnaib I did answer. You do not need and should not have, two webhook endpoints. delete one and its controller, and delete it from your stripe dashboard.

To better work with Stripe in development use their CLI tool as it is developed specifically for this purpose

If stripe gets a 500 error when they send webhooks then look in YOUR Laravel logfile to find out why.

I have integrated about a dozen different projects with Stripe. So, yes, I probably know the answer

1 like
UUID's avatar
Level 6

@Snapey Thanks I don't want delete because every endpoint have a task If you have multiple webhook endpoints on the same account that listen to the same event types, and an event of that type occurs on your Stripe account, all endpoints will be sent that event

Snapey's avatar

@Ahmadalnaib EXACTLY which is why you only need one endpoint. You cannot tell Stripe which webhook endpoint to call in which circumstance, so you have ONE endpoint and then in your application you inspect the webhook and decide what it relates to.

BUT you seem only interested in defending your design decisions and not listening to advice.

1 like
UUID's avatar
Level 6

@Snapey yes because how i will make it for both rental and extend rental

krisi_gjika's avatar

@Ahmadalnaib by relating the order/session id to your model when you create the rental/extension, and on the webhook using that same order/session id to find the rental/extension model related to that order.

krisi_gjika's avatar

your success_url for the stripe session is 'success_url' => route('rentals.success'), however your second route does not have a name, so how exactly are you creating the session for the extensions?

UUID's avatar
Level 6

@krisi_gjika

   \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
        $customer = \Stripe\Customer::create([
            'email' => $user->email, 
            
         
        ]);

        
        \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));

          
        $lineItems[] = [
            'price_data' => [
                'currency' => 'eur',
                'product_data' => [
                    'name' =>'Mietgebühr:'. ' ' .$plan->name,
                    // 'description' => 'test',
                  
                ],
                'unit_amount' => $plan->price * 100,
            ],
            'quantity' => 1,
        ];
        
        $session = \Stripe\Checkout\Session::create([
            'line_items' => $lineItems,
            'mode' => 'payment',
            'success_url' => route('rentals.extension.success', []) , 
            'cancel_url' => route('home', []),
            'customer' => $customer->id,
            ['metadata' =>
            [
               
                "user_id" => $user->id,
                "tenant_id" => $tenantId,
                'plan_id' => $plan->id,
                'system_id' => $systemId,
                'box_id' => $boxId,
                'rental_uuid' => $rental->uuid, 
                'end_time' => $end_time,
                 
                  
            ],
            'metadata'=>[
               
                
                'rental_uuid' => $rental->uuid, 
                 'end_time' => $end_time,
                 

            ]
            ],
          
           
          
        ]);
    
     
    
        return redirect($session->url);
    
    }





second one 

        \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
        $customer = \Stripe\Customer::create([
            'email' => $user->email, 
            
         
        ]);

        
        \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));

          
        $lineItems[] = [
            'price_data' => [
                'currency' => 'eur',
                'product_data' => [
                    'name' =>'Mietgebühr:'. ' ' .$plan->name,
                    // 'description' => 'test',
                  
                ],
                'unit_amount' => $plan->price * 100,
            ],
            'quantity' => 1,
        ];
    
    $session = \Stripe\Checkout\Session::create([
        'line_items' => $lineItems,
        'mode' => 'payment',
        'success_url' => route('rentals.success', []) , 
        'cancel_url' => route('home', []),
        'customer' => $customer->id,
        'payment_intent_data'=>
        ['metadata' =>
        [
           
             "user_id" => $user->id,
             'plan_id'=>$plan->id,
             'system_id' => $systemId, 
              'box_id' => $boxId,
             
              
          ]
        ],
        'metadata'=>[
          "tenant_id" => $tenantId,
           "user_id" => $user->id,
           'plan_id'=>$plan->id,
           'duration'=>$plan->name,
           'system_id' => $systemId, 
            'box_id' => $boxId,
            'user'=> $user->id,
            'start_time' => $start_time,
            'end_time' => $end_time,
        ],
       
      
    ]);

 

    return redirect($session->url);
    }
<?php

namespace App\Http\Controllers;

use Dompdf\Dompdf;
use App\Models\Box;
use Ramsey\Uuid\Uuid;
use App\Models\Rental;
use App\Models\System;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Mail\PaymentConfirmation;
use Illuminate\Support\Facades\Mail;
use App\Http\Middleware\VerifyStripeWebhookSecret;

class StripeWebhookController extends Controller
{
    //

    public function __construct()
    {
        $this->middleware(VerifyStripeWebhookSecret::class);
    }

    public function __invoke(Request $request)
    {
        $payload=json_decode($request->getContent(),true);

        // \Log::info($payload);

        $method='handle'. Str::studly(str_replace('.','_',$payload['type']));

        if(method_exists($this,$method)){
            $response=$this->{$method}($payload);

            return $response;
        }

        return new Response();
    }

    protected function handleCheckoutSessionCompleted(array $payload)
    {

            //  \Log::info($payload);
      
    
      
      
    
    
        $paymentStatus = data_get($payload, 'data.object.payment_status', null);
        $priceInCents = $payload['data']['object']['amount_subtotal'];
        $priceInEur = $priceInCents / 100;
    
        // Create a new rental record in the database for the found system
        $rental = Rental::create([
            // 'email' => $email,
            'tenant_id'=>$payload['data']['object']['metadata']['tenant_id'],
            'system_id' => $payload['data']['object']['metadata']['system_id'],
            "user_id" => $payload['data']['object']['metadata']['user_id'],
            'session_id' => $payload['data']['object']['id'],
            'price' => $priceInEur ,
            'status' => $paymentStatus === 'paid' ? 'paid' : null,
            'plan_id'=>$payload['data']['object']['metadata']['plan_id'],
            'box_id' => $payload['data']['object']['metadata']['box_id'],
            'uuid' => Uuid::uuid4()->toString(),
            'duration' => $payload['data']['object']['metadata']['duration'],
            'start_time' => $payload['data']['object']['metadata']['start_time'],
            'end_time' => $payload['data']['object']['metadata']['end_time'],
            'pincode' =>  mt_rand(100000000, 999999999),
        ]);

        $boxId = $payload['data']['object']['metadata']['box_id'];
        $box = Box::findOrFail($boxId);
        $box->rental_uuid = $rental->uuid;
        $box->occupied = true;
        $box->save();

        $dompdf = new Dompdf();
        $dompdf->loadHtml(view('emails.payment_confirmation', ['rental' => $rental])->render());
   
        $dompdf->render();
        $pdfContents = $dompdf->output();
   
        // Save the PDF file to a temporary location
        $pdfPath = storage_path('app/tmp/rechnung.pdf');
        file_put_contents($pdfPath, $pdfContents);
        // Send the pin code email
     
   
       Mail::to($rental->user->email)->send(new PaymentConfirmation($rental));
        // Cleanup the temporary PDF file
        unlink($pdfPath);
    
      
    }
    
    
}


krisi_gjika's avatar

@Ahmadalnaib your only option would be to pass some data like a unique order id to your success/error urls that you can than use to validate if the payment was indeed successful/canceled.

Otherwise you would have to create multiple stripe accounts, so that the same event would not be pushed to both webhooks.

1 like

Please or to participate in this conversation.