To handle Paystack webhooks in Laravel, add the webhook route to the CSRF exceptions, create a controller that reads the raw body with request()->getContent(), computes an HMAC SHA-512 hash, compares it to the X-Paystack-Signature header, and dispatches a Job for processing. Return response 200.
// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
'webhooks/paystack',
];
Without this, Laravel returns 419 for all Paystack webhook requests because they lack a CSRF token.
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handlePaystack(Request $request)
{
$signature = $request->header('X-Paystack-Signature');
if (!$signature) {
return response('Missing signature', 400);
}
$body = $request->getContent();
$secret = config('services.paystack.secret_key');
$computedHash = hash_hmac('sha512', $body, $secret);
if (!hash_equals($computedHash, $signature)) {
return response('Invalid signature', 400);
}
$event = json_decode($body, true);
// Process asynchronously
dispatch(new \App\Jobs\ProcessPaystackWebhook($event));
return response('OK', 200);
}
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// app/Jobs/ProcessPaystackWebhook.php
namespace App\Jobs;
use App\Services\PaystackService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
class ProcessPaystackWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
private array $event;
public function __construct(array $event)
{
$this->event = $event;
}
public function handle(PaystackService $paystack): void
{
$eventType = $this->event['event'] ?? '';
$data = $this->event['data'] ?? [];
switch ($eventType) {
case 'charge.success':
$reference = $data['reference'] ?? null;
if ($reference) {
$paystack->verifyAndFulfill($reference);
}
break;
case 'subscription.create':
// Handle new subscription
break;
case 'invoice.payment_failed':
// Handle failed renewal
break;
}
}
}
// routes/web.php
Route::post('/webhooks/paystack', [WebhookController::class, 'handlePaystack']);
Set the webhook URL in your Paystack dashboard: https://yourdomain.com/webhooks/paystack
// Create a migration for webhook events
Schema::create('webhook_events', function (Blueprint $table) {
$table->id();
$table->string('event_type');
$table->string('reference')->nullable();
$table->json('payload');
$table->boolean('processed')->default(false);
$table->timestamps();
});
// In the controller, log before dispatching:
\App\Models\WebhookEvent::create([
'event_type' => $event['event'] ?? 'unknown',
'reference' => $event['data']['reference'] ?? null,
'payload' => $event,
]);
// tests/Feature/WebhookTest.php
public function test_valid_webhook()
{
$payload = json_encode([
'event' => 'charge.success',
'data' => ['reference' => 'test_123', 'amount' => 500000],
]);
$signature = hash_hmac('sha512', $payload, config('services.paystack.secret_key'));
$response = $this->postJson('/webhooks/paystack', json_decode($payload, true), [
'X-Paystack-Signature' => $signature,
]);
$response->assertStatus(200);
}
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course