Integrating Square Webhooks with Laravel

Integrating Square Webhooks with Laravel

Deniz Birlik
Deniz Birlik
·8 min read

Welcome to this comprehensive guide on integrating Square webhooks with Laravel applications. As a developer who has worked extensively with payment processing systems, I'll walk you through the process of setting up and handling Square webhooks in your Laravel project. This integration is crucial for real-time payment processing and can significantly enhance your application's capabilities.

Why Square Webhooks?

Before we dive into the implementation, let's discuss why Square webhooks are valuable.

Imagine you're running an online store. A customer makes a purchase, and you want to update your inventory, send a confirmation email, and trigger a shipping label creation. While you could do this immediately after processing the payment, what if the payment fails after your app thinks it succeeded? That's where webhooks prove invaluable.

Webhooks allow Square to send real-time updates to your application about various events - payments, refunds, disputes, and more. This way, your app can react to these events as they happen, ensuring your data stays in sync with Square.

Setting Up Your Laravel Project

First, let's set up our Laravel project. If you haven't already, create a new Laravel project:

composer create-project --prefer-dist laravel/laravel square-webhook-demo
cd square-webhook-demo

Configuring Square in Laravel

Let's set up our Square configuration. Add this to your config/services.php file:

<?php

    return [
        // Other services...
        'square' => [
            'webhook_signature_key' => env('SQUARE_WEBHOOK_SIGNATURE_KEY'),
        ],
    ];

Don't forget to add this to your .env file:

SQUARE_WEBHOOK_SIGNATURE_KEY=your_webhook_signature_key_here

Creating the Webhook Controller

Now, let's create a controller to handle our webhook requests:

php artisan make:controller SquareWebhookController

Open the newly created app/Http/Controllers/SquareWebhookController.php and let's add some code:

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;
use IlluminateSupportFacadesLog;

class SquareWebhookController extends Controller
{
    public function handleWebhook(Request $request)
    {
        $payload = $request->all();

        // Verify the webhook signature
        if (!$this->verifyWebhookSignature($request)) {
            return response('Invalid signature', 400);
        }

        // Process the webhook
        $eventType = $payload['type'];

        switch ($eventType) {
            case 'payment.created':
                $this->handlePaymentCreated($payload['data']['object']['payment']);
                break;
            case 'refund.created':
                $this->handleRefundCreated($payload['data']['object']['refund']);
                break;
            // Add more cases as needed
            default:
                Log::info('Unhandled webhook event: ' . $eventType);
                break;
        }

        return response('Webhook processed', 200);
    }

    protected function verifyWebhookSignature(Request $request)
    {
        $signatureKey = config('services.square.webhook_signature_key');
        $signatureHeader = $request->header('X-Square-Signature');

        if (!$signatureHeader || !$signatureKey) {
            return false;
        }

        $payload = $request->getContent();

        $calculatedSignature = hash_hmac('sha256', $payload, $signatureKey);

        return hash_equals($calculatedSignature, $signatureHeader);
    }

    protected function handlePaymentCreated($payment)
    {
        // Logic to handle a new payment
        Log::info('New payment received: ' . $payment['id']);
        // Update your database, send notifications, etc.
    }

    protected function handleRefundCreated($refund)
    {
        // Logic to handle a new refund
        Log::info('New refund created: ' . $refund['id']);
        // Update your database, send notifications, etc.
    }
}

This controller verifies the webhook signature and processes different types of events.

Setting Up the Route and Disabling CSRF Protection

Now, let's set up a route for our webhook. In routes/api.php, add:

Route::post('/square/webhook', [SquareWebhookController::class, 'handleWebhook']);

Remember to add the controller to your imports at the top of the file.

However, this route will encounter CSRF protection issues. To exempt this URL from CSRF verification in Laravel 11, you need to modify the bootstrap/app.php file. Add the webhook route to the except array in the validateCsrfTokens method:

->withMiddleware(function (Middleware $middleware) {
    // ... other middleware configurations ...

    $middleware->validateCsrfTokens(except: [
        'square/webhook',
        // ... other exempted routes ...
    ]);

    // ... rest of the middleware configuration ...
})

This configuration tells Laravel to skip CSRF token validation for the /square/webhook route, allowing external services to post to this endpoint without a valid CSRF token.

Note: Be cautious when exempting routes from CSRF protection and ensure that you implement appropriate security measures for these endpoints.

Handling Specific Events

With the foundational setup complete, let's now focus on handling specific webhook events. We'll expand our handlePaymentCreated method to demonstrate how to process payment notifications effectively:

protected function handlePaymentCreated($payment)
{
    Log::info('New payment received: ' . $payment['id']);

    // Update order status
    $order = Order::where('square_order_id', $payment['order_id'])->first();
    if ($order) {
        $order->status = 'paid';
        $order->save();

        // Send confirmation email
        Mail::to($order->customer->email)->send(new OrderConfirmation($order));

        // Update inventory
        foreach ($order->items as $item) {
            $product = Product::find($item->product_id);
            $product->stock -= $item->quantity;
            $product->save();
        }

        // Trigger shipping label creation
        dispatch(new CreateShippingLabel($order));
    }
}

This method updates the order status, sends a confirmation email, updates the inventory, and triggers a job to create a shipping label.

Testing Your Webhook

Webhook testing can be a challenge, particularly in a local development environment. You need a method to receive webhooks on your localhost and simulate various webhook events. This is where Webhook Simulator becomes an invaluable tool in your development arsenal.

Webhook Simulator is a robust platform specifically designed for testing webhook integrations. It enables you to simulate webhook events from a variety of services, including Square, without the need to set up actual payment flows or expose your local environment to the internet.

Here's a step-by-step guide on how to leverage Webhook Simulator for testing your Square webhook integration:

  1. Start by signing up for a lifetime membership to Webhook Simulator. Our incredibly affordable one-time payment gives you unlimited access to all current and future features. This makes it an excellent long-term investment for developers working on webhook integrations, ensuring you always have a powerful testing tool at your disposal.

  2. Once you've secured your lifetime membership, visit the "Platforms" page and add Square to your library. Then, install the Webhook Simulator CLI tool on your local machine. You can find detailed installation instructions in our documentation.

  3. After installing the CLI, use the following command to start listening for webhook events and forward them to your local Laravel application:

ws-cli listen --forward-to http://localhost:8000/api/square/webhook

Remember to replace http://localhost:8000/api/square/webhook with the actual URL where your Laravel application is listening for webhooks.

  1. Now you have two options for simulating Square webhook events:

a. Utilize the Webhook Simulator web application to create and send custom payloads.

b. Use the CLI to trigger predefined events. Open a new terminal window and run:

ws-cli trigger square payment.created

You can replace payment.created with any other Square event you wish to test.

  1. The simulated webhook will be sent to your local Laravel application via the forwarding set up in step 3.

  2. Examine your Laravel logs to see how your application processed the simulated webhook event.

Here's an example of what a Square payment.created event payload might look like (note that this is a simplified version):

{
  "data": {
    "object": {
      "payment": {
        "amount_money": {
          "amount": 1000,
          "currency": "USD"
        },
        "id": "abcdef123456",
        "order_id": "order_123",
        "status": "COMPLETED"
      }
    }
  },
  "type": "payment.created"
}

By utilizing Webhook Simulator, you can easily test various scenarios, edge cases, and error conditions without processing real payments or waiting for specific events to occur in Square.

While Webhook Simulator is an excellent tool for development and testing, remember to conduct tests with actual Square webhooks in a staging environment before moving to production. Square provides a sandbox environment for this purpose.

Error Handling and Logging

When working with webhooks, robust error handling and logging are crucial. Let's update our controller to include better error handling:

public function handleWebhook(Request $request)
{
    try {
        $payload = $request->all();

        if (!$this->verifyWebhookSignature($request)) {
            Log::warning('Invalid webhook signature received');
            return response('Invalid signature', 400);
        }

        $eventType = $payload['type'];

        switch ($eventType) {
            case 'payment.created':
                $this->handlePaymentCreated($payload['data']['object']['payment']);
                break;
            case 'refund.created':
                $this->handleRefundCreated($payload['data']['object']['refund']);
                break;
            default:
                Log::info('Unhandled webhook event: ' . $eventType);
                break;
        }

        return response('Webhook processed', 200);
    } catch (Exception $e) {
        Log::error('Error processing webhook: ' . $e->getMessage());
        return response('Error processing webhook', 500);
    }
}

This ensures that any exceptions are caught and logged, preventing Square from receiving an error response (which could cause them to retry the webhook unnecessarily).

Queuing Webhook Processing

If your webhook processing involves time-consuming tasks, it's a good idea to queue the processing to prevent timeouts. You can do this by creating a job:

php artisan make:job ProcessSquareWebhook

Then, update the job file (app/Jobs/ProcessSquareWebhook.php):

<?php

namespace AppJobs;

use IlluminateBusQueueable;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateQueueInteractsWithQueue;
use IlluminateQueueSerializesModels;

class ProcessSquareWebhook implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $payload;

    public function __construct($payload)
    {
        $this->payload = $payload;
    }

    public function handle()
    {
        $eventType = $this->payload['type'];

        switch ($eventType) {
            case 'payment.created':
                // Handle payment created
                break;
            case 'refund.created':
                // Handle refund created
                break;
            // Add more cases as needed
        }
    }
}

Now, update your controller to dispatch this job instead of processing the webhook directly:

public function handleWebhook(Request $request)
{
    if (!$this->verifyWebhookSignature($request)) {
        return response('Invalid signature', 400);
    }

    ProcessSquareWebhook::dispatch($request->all());

    return response('Webhook queued for processing', 202);
}

This approach allows your application to quickly acknowledge receipt of the webhook while processing it in the background.

Wrapping Up

Integrating Square webhooks with your Laravel application opens up a world of possibilities for real-time payment processing and order management. We've covered the basics of setting up webhook handling, verifying signatures, processing events, and even touched on more advanced topics like queuing.

Remember, when working with payment systems, always prioritize security and thorough testing. Square provides excellent documentation and tools to help you along the way.

Happy coding, and may your payments always process smoothly!