Skip to article
Integrations

How to Integrate hCaptcha with Symfony

Add hCaptcha to Symfony forms with the maintained hCaptcha bundle, strict server-side validation, and reviewed HTTP client settings.

How do you integrate hCaptcha with Symfony?#

Install the maintained community hCaptcha bundle for Symfony, configure the sitekey and secret, add its HCaptchaType to a Symfony form, and run the protected action only after the submitted form is valid. The form type renders the widget and verifies h-captcha-response on the server.

Use Composer to confirm that the current bundle release supports the application's Symfony and PHP versions before installation.

These instructions were last validated on September 22, 2026 with meteo-concept/hcaptcha-bundle 4.5.0.

Make protected Symfony forms less disruptive#

  • Keep visitors focused on their task. hCaptcha Pro's 99.9% Passive mode challenges fewer than 0.1% of legitimate users, reducing interruptions on Symfony forms containing HCaptchaType.
  • Apply verification in proportion to risk. Pro increases challenge difficulty for suspicious interactions, helping ordinary visitors complete their forms with less friction while retaining stronger checks against abuse.

New Pro sitekeys use 99.9% Passive by default. For an existing sitekey upgraded to Pro, select that mode under Behavior in the hCaptcha dashboard.

Before you start#

You need:

  • A Symfony form and controller action to protect.
  • PHP, Symfony, and Composer versions compatible with bundle 4.5.0.
  • An hCaptcha account with a sitekey and matching secret.
  • A PSR-18 HTTP client and PSR-17 request and stream factories.

Review the hCaptcha Symfony catalog entry, the bundle's source repository and Packagist page, and its official Symfony Flex recipe.

Create your hCaptcha credentials#

  1. Start with hCaptcha Pro for fewer challenges and adaptive protection on protected Symfony forms, or use existing compatible hCaptcha credentials.
  2. Create a sitekey and configure every production or test hostname that will use it.
  3. Store the secret in the deployment's protected environment or secret manager.
  4. Expose the public sitekey through server configuration.

Never put the secret in Twig, JavaScript, a public .env file, logs, or error responses.

Install the bundle and HTTP implementations#

If the application does not already provide compatible PSR-18 and PSR-17 implementations, install the bundle with Symfony's HTTP client and Nyholm's PSR-7 factories:

composer require meteo-concept/hcaptcha-bundle:^4.5 symfony/http-client nyholm/psr7

Symfony Flex enables the bundle and applies the contrib recipe. Review every recipe change before committing it. The recipe starts with hCaptcha's documented test sitekey and secret; replace those values for deployed environments.

Configure credentials and strict validation#

Configure both credentials through environment variables and keep validation in strict mode:

parameters:
    hcaptcha_site_key: '%env(resolve:HCAPTCHA_SITE_KEY)%'
    hcaptcha_secret: '%env(resolve:HCAPTCHA_SECRET)%'

meteo_concept_h_captcha:
    hcaptcha:
        site_key: '%hcaptcha_site_key%'
        secret: '%hcaptcha_secret%'
    validation: strict

Add placeholder variable names to the committed environment template and supply real values through the deployment environment:

HCAPTCHA_SITE_KEY=
HCAPTCHA_SECRET=

The bundle also offers lax validation, which can accept a submission when verification times out or returns an unexpected response. Do not use that fail-open setting for protected actions. Strict mode rejects the bundle’s recognized verification errors, but the verifier’s boolean return type coerces scalar values. It does not by itself enforce a JSON boolean true; apply the correction described below.

Enable the Twig form theme#

Add the bundle's form theme if Flex did not configure it:

twig:
    form_themes:
        - '@MeteoConceptHCaptcha/hcaptcha_form.html.twig'

The supplied theme renders the widget and loads the hCaptcha client script. Check the final page so another template or asset pipeline does not load the script twice.

Add hCaptcha to the Symfony form#

Add HCaptchaType to the form that owns the protected submission:

use MeteoConcept\HCaptchaBundle\Form\HCaptchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

final class SignupType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // Add the application's other fields first.
            ->add('captcha', HCaptchaType::class, [
                'label' => 'Anti-bot verification',
            ]);
    }
}

The field is unmapped by default, so the token is not persisted to the application's entity. The form type applies NotBlank and IsValidCaptcha, reads the standard h-captcha-response field, and sends the token, sitekey, secret, and Symfony-derived client IP address to the verification service.

Gate the protected action on form validity#

Handle the form normally and keep all side effects after both submission and validation succeed:

$form = $this->createForm(SignupType::class, $signup);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    // Create the account or perform the protected action here.
}

Symfony's client IP depends on the trusted-proxy configuration. Review that configuration before relying on remoteip, especially behind a CDN, load balancer, or reverse proxy.

Review endpoint and timeout behavior#

Bundle 4.5.0 remains useful for its Symfony form integration, expected-sitekey binding, HTTP-status checks, JSON validation, response-size limit, and strict mode. Its source hardcodes https://hcaptcha.com/siteverify and its Twig theme loads https://hcaptcha.com/1/api.js; our current public documentation specifies https://api.hcaptcha.com/siteverify and https://js.hcaptcha.com/1/api.js for new integrations. Retain the form integration with a reviewed endpoint update. Also require $json['success'] === true before the form becomes valid because the verifier's non-strict PHP boolean return type can coerce a string such as "false" to true.

The bundle does not expose endpoint or timeout settings in its configuration. Its PSR-18 client controls connection and response timeouts. The verifier does not catch Psr\Http\Client\ClientExceptionInterface, while the form constraint catches only the bundle's BadAnswerFromHCaptchaException. A PSR-18 transport exception therefore stops normal form validation but can escape as an application error.

Production approach: Decorate or replace the bundle verifier service while retaining its form type, Twig theme, and constraint. The adapter should use the current endpoints, configure finite timeouts on the selected PSR-18 client, inspect raw decoded JSON before PHP return-type coercion, accept only $json['success'] === true, and translate every ClientExceptionInterface into the bundle's controlled invalid-form exception. Keep validation: strict; the adapter strengthens strict mode rather than replacing it.

Test the Symfony integration#

  1. Confirm a valid token permits the protected action exactly once.
  2. Reject missing, invalid, expired, reused, and wrong-sitekey tokens.
  3. Confirm strict mode rejects non-200 responses, malformed JSON, oversized responses, and success: false.
  4. Simulate DNS, connection, TLS, and timeout exceptions and confirm the form fails closed with controlled user-facing behavior.
  5. Verify the Flex recipe's test credentials are absent from production configuration.
  6. Confirm trusted proxies produce the intended client IP and do not accept forged forwarding headers.
  7. Test Twig rendering, duplicate scripts, Content Security Policy, every configured hostname, and the exact PSR HTTP implementation.

Troubleshoot common Symfony problems#

Composer cannot find a PSR HTTP implementation

Install compatible PSR-18 and PSR-17 implementations. The documented Symfony path uses symfony/http-client and nyholm/psr7 with the bundle.

The widget does not appear

Confirm the bundle is enabled, its Twig form theme is listed, the sitekey is present, and Content Security Policy allows the hCaptcha client resources.

The form accepts requests during a verification outage

Set validation: strict. Lax mode intentionally accepts some unexpected or unavailable verification responses.

The reported client IP is incorrect

Review Symfony's trusted proxies and headers. The bundle uses the main request's getClientIp() value.

A network failure produces an application error

Strict mode handles non-200 and malformed responses returned by the verifier, but it does not catch exceptions thrown directly by the PSR-18 client. Configure finite client timeouts and add a reviewed adapter that converts ClientExceptionInterface failures into the bundle's controlled verification error. Confirm that the protected action remains behind isSubmitted() and isValid().

Frequently asked questions#

How do I confirm bundle compatibility?

The current bundle supports maintained Symfony releases. Confirm the exact Symfony and PHP constraints through Composer before upgrading.

Does the bundle verify hCaptcha on the server?

Yes. Its form constraint sends the submitted token and server-held secret to hCaptcha before the Symfony form becomes valid.

Should I use strict or lax validation?

Use strict validation for protected actions. Lax mode can accept a submission when verification cannot return a valid response.

Can the hCaptcha secret be rendered in Twig?

No. Twig needs the public sitekey only. Keep the secret in protected server configuration.

Does the Flex recipe finish production configuration?

No. It enables the bundle and supplies starter configuration with hCaptcha test credentials. Review the recipe and supply environment-specific production credentials before deployment.

Sources and references

  1. hCaptcha Pro product overview hCaptcha
  2. hCaptcha integrations — Symfony hCaptcha
  3. Verify the hCaptcha response server-side hCaptcha
  4. MeteoConcept hCaptcha bundle source MeteoConcept
  5. MeteoConcept hCaptcha bundle package Packagist
  6. Symfony Flex recipe for the hCaptcha bundle Symfony
  7. Symfony HTTP client Symfony
  8. Symfony validation Symfony
  9. hCaptcha Pro hCaptcha
  10. hCaptcha integrations list source hCaptcha