Verify Paystack Payments in Symfony
To verify a Paystack payment in Symfony, use your PaystackService to GET the verify endpoint. Check status, amount, and currency against your Doctrine Order entity. Use DQL UPDATE with a condition on paid=false for idempotent fulfillment.
Verification Service Method
// In PaystackService
public function verifyAndFulfill(string $reference, EntityManagerInterface $em): array
{
$order = $em->getRepository(Order::class)->findOneBy(['reference' => $reference]);
if (!$order) {
return ['success' => false, 'error' => 'Order not found'];
}
if ($order->isPaid()) {
return ['success' => true, 'already_fulfilled' => true];
}
$result = $this->verifyTransaction($reference);
if (!($result['status'] ?? false) || $result['data']['status'] !== 'success') {
return ['success' => false, 'error' => 'Payment not successful'];
}
if ($result['data']['amount'] !== $order->getAmount()) {
return ['success' => false, 'error' => 'Amount mismatch'];
}
// Atomic update
$updated = $em->createQueryBuilder()
->update(Order::class, 'o')
->set('o.paid', true)
->set('o.paidAt', ':now')
->where('o.reference = :ref')
->andWhere('o.paid = false')
->setParameter('ref', $reference)
->setParameter('now', new \DateTime())
->getQuery()
->execute();
return [
'success' => true,
'already_fulfilled' => $updated === 0,
];
}
Controller Usage
#[Route('/payment/callback')]
public function callback(Request $request, PaystackService $paystack, EntityManagerInterface $em): Response
{
$reference = $request->query->get('reference') ?? $request->query->get('trxref');
if (!$reference) {
return new Response('Missing reference', 400);
}
$result = $paystack->verifyAndFulfill($reference, $em);
if ($result['success']) {
return $this->render('payment/success.html.twig', ['reference' => $reference]);
}
return $this->render('payment/failed.html.twig', ['error' => $result['error']]);
}
Common Mistakes
- Using $em->flush() without atomic conditions. Use DQL UPDATE with a WHERE clause instead.
- Not checking currency. Always verify the currency matches your entity.
- Trusting request parameters. Verify against your database, not URL params.
Key Takeaways
- ✓Extend your PaystackService with a verifyAndFulfill method that checks amount, currency, and status.
- ✓Use Doctrine DQL or QueryBuilder for atomic updates that prevent double-granting.
- ✓Both callback and webhook handlers should call the same verification service method.
- ✓Always check amount and currency against your Doctrine entity, not request parameters.
- ✓If the verify call fails, retry with exponential backoff.
- ✓The Paystack verify endpoint is idempotent. Calling it multiple times returns the same result.
Frequently Asked Questions
- Can I use the Symfony Messenger component for async verification?
- Yes. Dispatch a message from your webhook handler and process it asynchronously. The idempotent verification logic ensures safety.
- Should I verify in the webhook handler?
- Yes. Both callback and webhook should call verifyAndFulfill. The atomic update ensures only one fulfills the order.
- How do I handle verification timeouts?
- Set a timeout on the HttpClient request. If it times out, retry. The transaction state on Paystack does not change.
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