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();
    }
}

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.

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