Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in Symfony

To accept Paystack payments in Symfony, inject the HttpClientInterface, create a PaystackService that wraps API calls, and build controllers for initialization and callback handling. Use environment variables for your secret key and verify every transaction server-side.

Setup

Add your Paystack keys to .env:

PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx

Bind it as a parameter in config/services.yaml:

parameters:
    paystack_secret_key: '%env(PAYSTACK_SECRET_KEY)%'

PaystackService

// src/Service/PaystackService.php
namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;

class PaystackService
{
    private HttpClientInterface $client;
    private string $secretKey;

    public function __construct(HttpClientInterface $client, string $paystackSecretKey)
    {
        $this->client = $client;
        $this->secretKey = $paystackSecretKey;
    }

    public function initializeTransaction(string $email, int $amountKobo, string $reference, string $callbackUrl): array
    {
        $response = $this->client->request('POST', 'https://api.paystack.co/transaction/initialize', [
            'headers' => [
                'Authorization' => 'Bearer ' . $this->secretKey,
                'Content-Type' => 'application/json',
            ],
            'json' => [
                'email' => $email,
                'amount' => $amountKobo,
                'reference' => $reference,
                'callback_url' => $callbackUrl,
            ],
        ]);

        return $response->toArray();
    }

    public function verifyTransaction(string $reference): array
    {
        $response = $this->client->request('GET', 'https://api.paystack.co/transaction/verify/' . $reference, [
            'headers' => [
                'Authorization' => 'Bearer ' . $this->secretKey,
            ],
        ]);

        return $response->toArray();
    }
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Payment Controller

// src/Controller/PaymentController.php
namespace App\Controller;

use App\Service\PaystackService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Uid\Uuid;

class PaymentController extends AbstractController
{
    #[Route('/pay', methods: ['POST'])]
    public function initialize(Request $request, PaystackService $paystack): Response
    {
        $email = $request->request->get('email');
        $amount = (int) ($request->request->get('amount', 5000) * 100);
        $reference = 'order_' . Uuid::v4()->toRfc4122();

        // TODO: Save order to database

        $result = $paystack->initializeTransaction(
            $email, $amount, $reference,
            $this->generateUrl('payment_callback', [], 0)
        );

        if ($result['status'] ?? false) {
            return $this->redirect($result['data']['authorization_url']);
        }

        return new Response('Failed: ' . ($result['message'] ?? 'Unknown'), 400);
    }

    #[Route('/payment/callback', name: 'payment_callback')]
    public function callback(Request $request, PaystackService $paystack): Response
    {
        $reference = $request->query->get('reference') ?? $request->query->get('trxref');

        if (!$reference) {
            return new Response('Missing reference', 400);
        }

        $result = $paystack->verifyTransaction($reference);

        if (($result['status'] ?? false) && $result['data']['status'] === 'success') {
            // TODO: Verify amount, mark order as paid
            return $this->render('payment/success.html.twig', ['reference' => $reference]);
        }

        return $this->render('payment/failed.html.twig');
    }
}

Service Configuration

# config/services.yaml
services:
    App\Service\PaystackService:
        arguments:
            $paystackSecretKey: '%paystack_secret_key%'

Security Checklist

  1. Validate amounts server-side. Look up prices from your database.
  2. Set up webhooks. See Handle Paystack Webhooks in Symfony.
  3. Use HTTPS. Required for callback and webhook URLs.
  4. Verify every transaction. The redirect alone is not proof of payment.

Ship Payments Faster

Key Takeaways

  • Symfony HttpClient provides a clean API for calling the Paystack REST endpoints.
  • Store your Paystack keys in .env and bind them as service parameters.
  • Create a PaystackService that you inject into controllers via autowiring.
  • Amounts are in kobo (NGN), pesewas (GHS), or cents. Convert before sending.
  • Always verify the transaction server-side after the redirect callback.
  • No external packages needed. Symfony HttpClient handles all Paystack API calls.

Frequently Asked Questions

Does Symfony need CSRF exemption for webhooks?
Symfony does not have automatic CSRF protection on all POST routes like Laravel does. If you use the Symfony Form component, CSRF is form-specific. Your webhook route does not need special CSRF handling.
Can I use Guzzle instead of Symfony HttpClient?
Yes, but Symfony HttpClient is built into the framework and integrates better with dependency injection. Guzzle works fine too.
How do I test PaystackService?
Mock the HttpClientInterface in your unit tests. Symfony provides a MockHttpClient for this purpose.

Learn Paystack Integration

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

Not ready? Create Free Account