Verify Paystack Payments in Laravel
To verify a Paystack payment in Laravel, use Http::withToken() to GET the verify endpoint. Check data.data.status is "success", the amount matches your Order model, and the currency is correct. Use Order::where("reference", $ref)->where("paid", false)->update() for idempotent fulfillment.
PaystackService Class
// app/Services/PaystackService.php
namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\Http;
class PaystackService
{
private string $secretKey;
public function __construct()
{
$this->secretKey = config('services.paystack.secret_key');
}
public function verify(string $reference): array
{
$response = Http::withToken($this->secretKey)
->get('https://api.paystack.co/transaction/verify/' . $reference);
return $response->json();
}
public function verifyAndFulfill(string $reference): array
{
$order = Order::where('reference', $reference)->first();
if (!$order) {
return ['success' => false, 'error' => 'Order not found'];
}
if ($order->paid) {
return ['success' => true, 'already_fulfilled' => true];
}
$result = $this->verify($reference);
if (!($result['status'] ?? false) || $result['data']['status'] !== 'success') {
return ['success' => false, 'error' => 'Payment not successful'];
}
if ($result['data']['amount'] !== $order->amount) {
return ['success' => false, 'error' => 'Amount mismatch'];
}
if ($result['data']['currency'] !== $order->currency) {
return ['success' => false, 'error' => 'Currency mismatch'];
}
$updated = Order::where('reference', $reference)
->where('paid', false)
->update(['paid' => true, 'paid_at' => now()]);
return [
'success' => true,
'already_fulfilled' => $updated === 0,
'amount' => $order->amount / 100,
'currency' => $order->currency,
];
}
}
Using the Service in Controllers
// In PaymentController
public function callback(Request $request, PaystackService $paystack)
{
$reference = $request->query('reference') ?? $request->query('trxref');
if (!$reference) {
return redirect('/')->with('error', 'Missing reference');
}
$result = $paystack->verifyAndFulfill($reference);
if ($result['success']) {
return view('payment.success', ['reference' => $reference]);
}
return view('payment.failed', ['error' => $result['error']]);
}
public function verify(Request $request, PaystackService $paystack)
{
$reference = $request->query('reference');
if (!$reference) {
return response()->json(['error' => 'Missing reference'], 400);
}
$result = $paystack->verifyAndFulfill($reference);
return response()->json($result, $result['success'] ? 200 : 400);
}
Laravel automatically injects the PaystackService through constructor or method injection.
Retry Logic
public function verifyWithRetry(string $reference, int $maxRetries = 3): ?array
{
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
try {
$response = Http::withToken($this->secretKey)
->timeout(10)
->get('https://api.paystack.co/transaction/verify/' . $reference);
if ($response->successful()) {
return $response->json();
}
} catch (\Exception $e) {
// Log and retry
}
if ($attempt < $maxRetries - 1) {
sleep(pow(2, $attempt));
}
}
return null;
}
Common Mistakes
- Using $order->save() instead of ::where()->update(). The save() method is not atomic. Two concurrent requests could both read paid=false and both save paid=true.
- Trusting request input for amounts. Always compare against the Eloquent model.
- Mixing test and live keys. Both must be in the same mode.
- Not checking currency. Always verify the currency matches.
Key Takeaways
- ✓Laravel HTTP client makes verification calls clean: Http::withToken($key)->get($url).
- ✓Always check status, amount, and currency against your Eloquent model.
- ✓Use where("paid", false)->update() for atomic idempotent fulfillment.
- ✓Create a PaystackService class for reusable verification logic across controllers.
- ✓Both callback and webhook handlers should call the same verification method.
- ✓If the verify call fails, retry. The Paystack transaction state does not change.
Frequently Asked Questions
- Can I verify a transaction multiple times?
- Yes. The Paystack verify endpoint is idempotent. It returns the same result every time for the same reference.
- Should I register PaystackService as a singleton?
- It depends on your needs. As a singleton, the same instance is reused. As a regular binding, a new instance is created per request. Both work fine for Paystack API calls.
- How do I handle verification in queued jobs?
- Your webhook handler can dispatch a job that calls verifyAndFulfill. The idempotent logic ensures safety even if the job runs multiple times.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon