Developer Tutorials

How to Send OTP with Symfony (2026)

Symfony OTP tutorial using StartMessaging. Uses HttpClient, ParameterBag for secrets, Form component for validation, and Session for storing the request ID.

10 May 20268 min read

StartMessaging Team

Engineering

Symfony’s service-container model and HttpClient make OTP integration straightforward. This tutorial uses StartMessaging.

Setup

symfony new otp-symfony --webapp
cd otp-symfony
composer require symfony/http-client

Service Configuration

# .env
SM_API_KEY=sm_live_xxxxxxxxxxxxxxxx
SM_BASE_URL=https://api.startmessaging.com
# config/services.yaml
services:
  App\Service\StartMessagingService:
    arguments:
      $apiKey: '%env(SM_API_KEY)%'
      $baseUrl: '%env(SM_BASE_URL)%'

StartMessagingService

<?php
// src/Service/StartMessagingService.php
namespace App\Service;

use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\Uid\Uuid;

class StartMessagingService
{
    public function __construct(
        private HttpClientInterface $client,
        private string $apiKey,
        private string $baseUrl,
    ) {}

    public function sendOtp(string $phoneNumber): array
    {
        $r = $this->client->request('POST', $this->baseUrl.'/otp/send', [
            'headers' => ['X-API-Key' => $this->apiKey, 'Content-Type' => 'application/json'],
            'json' => ['phoneNumber' => $phoneNumber, 'idempotencyKey' => Uuid::v4()->toRfc4122()],
            'timeout' => 10,
        ]);
        return $r->toArray()['data'];
    }

    public function verifyOtp(string $requestId, string $otpCode): bool
    {
        $r = $this->client->request('POST', $this->baseUrl.'/otp/verify', [
            'headers' => ['X-API-Key' => $this->apiKey, 'Content-Type' => 'application/json'],
            'json' => ['requestId' => $requestId, 'otpCode' => $otpCode],
        ]);
        return $r->getStatusCode() === 200;
    }
}

Auth Controller

<?php
// src/Controller/AuthController.php
namespace App\Controller;

use App\Service\StartMessagingService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class AuthController extends AbstractController
{
    #[Route('/auth/send-otp', methods: ['POST'])]
    public function send(Request $request, StartMessagingService $sm): Response
    {
        $phone = $request->getPayload()->getString('phoneNumber');
        $data = $sm->sendOtp($phone);
        $request->getSession()->set('otp_req', $data['requestId']);
        return $this->json(['expiresAt' => $data['expiresAt']]);
    }

    #[Route('/auth/verify-otp', methods: ['POST'])]
    public function verify(Request $request, StartMessagingService $sm): Response
    {
        $code = $request->getPayload()->getString('otpCode');
        $rid = $request->getSession()->get('otp_req');
        if (!$rid) return $this->json(['error' => 'no active otp'], 400);
        $ok = $sm->verifyOtp($rid, $code);
        if (!$ok) return $this->json(['error' => 'invalid'], 401);
        $request->getSession()->remove('otp_req');
        $request->getSession()->set('userPhone', 'verified');
        return $this->json(['verified' => true]);
    }
}

Form Types

Use Symfony\Component\Validator\Constraints to enforce E.164 phone shape and 4–8 digit OTP shape on incoming requests.

FAQ

Same flow in PHP/Laravel? See our Laravel guide.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.