Skip to content

Laravel Integration

This guide shows a full Laravel integration using push delivery with signature verification and queued job processing.

app/Http/Controllers/WebhookController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Jobs\ProcessWebhook;
use App\Models\ProcessedWebhook;
class WebhookController extends Controller
{
public function handle(Request $request)
{
if (!$this->verifySignature($request)) {
abort(401, 'Invalid signature');
}
$payload = $request->json()->all();
$eventId = $payload['event_id'];
// Idempotency check
if (ProcessedWebhook::where('event_id', $eventId)->exists()) {
return response('OK', 200);
}
// Dispatch for async processing
ProcessWebhook::dispatch($payload);
return response('OK', 200);
}
private function verifySignature(Request $request): bool
{
$timestamp = $request->header('X-Gateway-Timestamp');
$signature = $request->header('X-Gateway-Signature');
$body = $request->getContent();
$secret = config('services.transyt.delivery_secret');
$expected = hash_hmac('sha256', "{$timestamp}.{$body}", $secret);
return hash_equals($expected, $signature ?? '');
}
}
routes/api.php
use App\Http\Controllers\WebhookController;
Route::post('/webhooks/transyt', [WebhookController::class, 'handle'])
->withoutMiddleware(['throttle:api']);
app/Jobs/ProcessWebhook.php
<?php
namespace App\Jobs;
use App\Models\ProcessedWebhook;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public array $backoff = [60, 300, 900];
public function __construct(
private array $payload
) {}
public function handle(): void
{
$eventId = $this->payload['event_id'];
$provider = $this->payload['provider'];
$eventType = $this->payload['event_type'];
$data = $this->payload['payload'];
// Idempotency check (in case of job retry)
if (ProcessedWebhook::where('event_id', $eventId)->exists()) {
return;
}
ProcessedWebhook::create(['event_id' => $eventId]);
match ($provider) {
'stripe' => $this->handleStripe($eventType, $data),
'mailgun' => $this->handleMailgun($eventType, $data),
default => logger()->warning("Unhandled provider: {$provider}"),
};
}
private function handleStripe(string $eventType, array $data): void
{
match ($eventType) {
'charge.succeeded' => $this->processCharge($data),
'charge.failed' => $this->handleFailedCharge($data),
'customer.subscription.deleted' => $this->cancelSubscription($data),
default => null,
};
}
private function handleMailgun(string $eventType, array $data): void
{
match ($eventType) {
'delivered' => $this->markEmailDelivered($data),
'failed' => $this->handleEmailFailure($data),
default => null,
};
}
}
database/migrations/xxx_create_processed_webhooks_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('processed_webhooks', function (Blueprint $table) {
$table->id();
$table->string('event_id')->unique();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('processed_webhooks');
}
};
config/services.php
return [
// ...
'transyt' => [
'delivery_secret' => env('TRANSYT_DELIVERY_SECRET'),
],
];

Add to your .env:

TRANSYT_DELIVERY_SECRET=your-delivery-secret

Ensure the webhook route is excluded from CSRF verification. In Laravel 11+, API routes don’t have CSRF by default. For earlier versions:

app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
'webhooks/transyt',
];