Integrations
Ready-made packages for Laravel, Symfony, WordPress and WoltLab Suite — plus the guide for one of your own.
Laravel integration
1. Add the repository
Add the CaptchaCore repository to your composer.json:
{
"repositories": [
{
"type": "composer",
"url": "https://captchacore.eu/packages"
}
]
}
2. Install the package
composer require captchacore/laravel # optional php artisan vendor:publish --tag=captchacore-config php artisan vendor:publish --tag=captchacore-lang
Laravel registers the ServiceProvider, middleware alias, Blade component and facade via auto-discovery. You only need to publish the configuration if you want to change values outside .env.
3. Configure .env
CAPTCHACORE_URL=https://api.captchacore.eu CAPTCHACORE_SITE_KEY=cc_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxx CAPTCHACORE_SECRET_KEY=cc_sec_xxxxxxxxxxxxxxxxxxxxxxxxxxxx # optional CAPTCHACORE_TIMEOUT=3 # Sekunden CAPTCHACORE_FAIL_OPEN=true # bei Störung des Dienstes durchlassen CAPTCHACORE_WIDGET_MODE= # interactive | visible | invisible CAPTCHACORE_WIDGET_THEME= # auto | light | dark CAPTCHACORE_WIDGET_COLOR= # z. B. #34d399 CAPTCHACORE_WIDGET_LABEL= CAPTCHACORE_WIDGET_BRAND= CAPTCHACORE_WIDGET_SIZE= # normal | compact CAPTCHACORE_WIDGET_LANG= # de | en | fr | es … CAPTCHACORE_WIDGET_ENDPOINT=eu # eu | global CAPTCHACORE_WIDGET_CDN_URL= # eigener Widget-Host (optional)
Widget options left empty are not output by the package: the widget then uses the settings from the customer area and the language of the page.
Blade component
<form method="post" action="/contact"> @csrf <!-- … --> <x-captchacore::widget form-type="contact" /> @error('captchacore') <p class="error">{{ $message }}</p> @enderror <button type="submit">Absenden</button> </form>
The form type of the widget and of the check must be identical: form-type="contact" belongs to captchacore:contact or new CaptchaCoreToken('contact'). Each type has its own difficulty; a different type is rejected.
Middleware
use App\Http\Controllers\ContactController; use Illuminate\Support\Facades\Route; Route::post('/contact', [ContactController::class, 'send']) ->middleware('captchacore:contact');
POST, PUT, PATCH and DELETE are checked. On rejection the middleware redirects back with the input and the message under the key captchacore. Requests via fetch() or XHR receive JSON with status 422 and errors.captchacore — send Accept: application/json for this (example under “Submitting forms via fetch()”).
Validation rule
use CaptchaCore\Laravel\Rules\CaptchaCoreToken; 'captchacore_token' => [new CaptchaCoreToken('contact')],
The rule also applies without a token — required is not needed and would produce a second message. The message is stored under the field name: @error('captchacore_token').
Facade
use CaptchaCore\Laravel\Facades\CaptchaCore; $result = CaptchaCore::verify($request->input('captchacore_token'), 'comment'); if (! $result->passed()) { return back()->withInput()->withErrors(['captchacore' => __('captchacore::messages.failed')]); }
Messages, logs and proxies
- The messages ship in German, English, Spanish and French and follow the app language. Customise them after
vendor:publish --tag=captchacore-langinlang/vendor/captchacore. - Rejections are logged as info with reasons and risk score, outages with fail-open as warning — the token itself is never logged.
- Behind a load balancer, CDN or reverse proxy, configure TrustProxies (
$middleware->trustProxies(at: …)), otherwise CaptchaCore sees the proxy’s IP for every visitor. - Strict CSP: the component takes the Vite nonce (
Vite::useCspNonce()) ornonce="…"and puts it on the script tag.
Tests
use CaptchaCore\Laravel\Facades\CaptchaCore; CaptchaCore::fake(); // jede Prüfung besteht; wird bei jedem App-Start zurückgesetzt $this->post('/contact', ['captchacore_token' => 'fake'])->assertRedirect();
Symfony Bundle
For Symfony 6.4 LTS, 7.x and 8.x. Four integration paths: FormType, validator constraint, controller attributes and a security badge for the login.
1. Install
Add the CaptchaCore repository to your composer.json:
{
"repositories": [
{ "type": "composer", "url": "https://captchacore.eu/packages" }
]
}
composer require captchacore/captchacore-bundle
Register the bundle in config/bundles.php and create the configuration as config/packages/captchacore.yaml: Both steps are manual — there is no Flex recipe for the bundle.
// config/bundles.php CaptchaCore\SymfonyBundle\CaptchaCoreBundle::class => ['all' => true], # config/packages/captchacore.yaml captchacore: service_url: '%env(CAPTCHACORE_URL)%' site_key: '%env(CAPTCHACORE_SITE_KEY)%' secret_key: '%env(CAPTCHACORE_SECRET_KEY)%'
2. Configure .env
CAPTCHACORE_URL=https://api.captchacore.eu CAPTCHACORE_SITE_KEY=cc_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxx CAPTCHACORE_SECRET_KEY=cc_sec_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
FormType (recommended)
use CaptchaCore\SymfonyBundle\Form\Type\CaptchaCoreType; $builder ->add('email', EmailType::class) ->add('message', TextareaType::class) ->add('captcha', CaptchaCoreType::class, [ 'form_type' => 'contact', ]);
The recommended way for HTML forms: a rejection appears as a normal form error. The widget always sends its token as captchacore_token; the FormType takes it over before validation.
Controller attributes
use CaptchaCore\SymfonyBundle\Security\Attribute\RequiresCaptcha; #[Route('/contact', methods: ['POST'])] #[RequiresCaptcha(formType: 'contact')] public function submit(Request $request): Response { /* ... */ }
A rejection ends with status 403 (logged as info, not error) — suitable for endpoints called via JavaScript. With throwOnFailure: false you get JSON with status 422 instead. For HTML forms use the FormType. The attribute does not protect the login, because the firewall authenticates before the controller runs.
Validator constraint (DTOs)
For #[MapRequestPayload] Symfony additionally needs symfony/serializer and symfony/property-access.
use CaptchaCore\SymfonyBundle\Validator\CaptchaCoreToken; final class ContactDto { public function __construct( #[Assert\NotBlank] public string $email, #[CaptchaCoreToken(formType: 'contact')] public string $captchaToken, ) {} }
Programmatic
use CaptchaCore\SymfonyBundle\Client\CaptchaCoreClient; $result = $this->captcha->verify($token, 'login'); if ($result->blocked()) { throw new AccessDeniedHttpException(); } // Properties: valid, riskScore, action, confidence, reasons, stepUp
Twig (standalone)
{{ captchacore_widget(form_type: 'contact') }}
@{# Strikte CSP: Nonce Ihrer Seite mitgeben #}
{{ captchacore_widget(form_type: 'contact', nonce: page_nonce) }}
{{ form_row(form.captcha, {nonce: page_nonce}) }}
Without form_type, default_form_type applies, as with the FormType. The script is included only once per page, even with several widgets or mixed with the FormType. The widget uses the nonce of its script tag for its style element; allow it in script-src and style-src-elem. Set your own widget host with widget.cdn_url.
Protecting the login
Symfony’s standard form_login is not protected: only logins carrying a CaptchaCoreBadge are checked. Replace form_login with your own authenticator that adds the badge — the token is then checked before the password.
// src/Security/LoginFormAuthenticator.php namespace App\Security; use CaptchaCore\SymfonyBundle\Security\CaptchaCoreAuthenticator; use CaptchaCore\SymfonyBundle\Security\CaptchaCoreBadge; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\SecurityRequestAttributes; use Symfony\Component\Security\Http\Util\TargetPathTrait; final class LoginFormAuthenticator extends AbstractLoginFormAuthenticator { use TargetPathTrait; public function __construct(private readonly UrlGeneratorInterface $urlGenerator) {} public function authenticate(Request $request): Passport { $payload = $request->getPayload(); $username = $payload->getString('_username'); $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $username); return new Passport( new UserBadge($username), new PasswordCredentials($payload->getString('_password')), [ new CaptchaCoreBadge(CaptchaCoreAuthenticator::extractToken($request) ?? '', 'login'), new CsrfTokenBadge('authenticate', $payload->getString('_csrf_token')), new RememberMeBadge(), ], ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { $target = $this->getTargetPath($request->getSession(), $firewallName); return new RedirectResponse($target ?? $this->urlGenerator->generate('app_home')); } protected function getLoginUrl(Request $request): string { return $this->urlGenerator->generate('app_login'); } }
# config/packages/security.yaml — statt form_login security: firewalls: main: custom_authenticator: App\Security\LoginFormAuthenticator logout: path: app_logout @{# templates/security/login.html.twig, im <form method="post"> #} <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"> {{ captchacore_widget(form_type: 'login') }}
app_login is the route of your login page (GET shows the form, POST is handled by the authenticator), app_home the page after login. The complete example with controller and template is in the bundle’s README.
Tests
CaptchaCoreClient::fake(VerificationResult::allow()); $this->client->request('POST', '/contact', [...]); self::assertResponseIsSuccessful();
WordPress Plugin
- Install the plugin — in WordPress, go to
Plugins → Add Newand search for “CaptchaCore” (entry in the WordPress directory). Updates then come from WordPress itself. Alternatively upload the ZIP from your account. - Activate it under Plugins
- Open Settings > CaptchaCore
- Enter the service URL, site key and secret key
- Choose widget mode, theme and color
- Enable the forms you want (login, registration, comments, etc.)
No code required. The plugin automatically embeds the widget in all enabled WordPress forms and verifies tokens server-side.
WoltLab Suite
The package is distributed through the official WoltLab plugin store. Installation and every future update run through it — there is no separate download in your account.
- Buy the package in the plugin store and install it in the ACP under
Packages → Install package - Enter your credentials under
Options → Security → Anti-Spam → CaptchaCore: service URL, site key and secret key - Choose the appearance: mode, theme, accent colour and language — “auto” follows the language of your forum.
- Select CaptchaCore as the captcha under
Options → Security → Anti-Spam → Captcha— it then applies to registration, the contact form and every other system form
| Setting | Meaning |
|---|---|
| Endpoint | EU standard or worldwide endpoint. EU keeps all verification data inside the EU; the global endpoint speeds things up for distant visitors. |
| Mode | interactive, visible or invisible — the same three modes as everywhere else, see widget modes. |
| Behaviour during an outage | If CaptchaCore cannot be reached, the plugin lets the form through by default and logs the error. Switch it off if you would rather block in case of doubt. |
One package for 6.1 and 6.2. The handler verifies every token server-side against the V2 API; the extra check (“step_up”) is handled by the widget itself, and the visitor gets a message of its own instead of an error.
Build your own integration
Do you want to bring CaptchaCore into a system with no package yet, such as Shopware, Joomla or your own framework? It takes a few hours. This order has proven itself.
Deliver the script
Include captchacore-v2.min.js from the EU endpoint and pass the site key and service URL as data attributes. In a CMS this belongs in the hook that registers frontend scripts. Only load the script on pages that actually contain a protected form.
Mark the forms
Put data-captchacore on the form and insert an empty container with data-captchacore-widget. data-form-type decides which policy applies. Several forms on one page are no problem, each gets its own widget.
Check the token server side
Read the captchacore_token field from the request and send it with your secret key to /api/v2/verify. Check decision, and do not treat step_up like block.
Handle outages
Set a timeout of about three seconds and decide deliberately whether an outage lets traffic through or blocks it. Make it configurable in the plugin, the answer differs from form to form.
Account for late loading
If your system swaps forms via AJAX, call CaptchaCoreV2.attach() afterwards.
Offer settings
Site key, secret key, choice of protected forms, appearance and the behaviour on outage belong in your plugin's configuration screen.
Templates to copy from
All supplied integrations follow the same pattern. For a Symfony-based system such as Shopware 6 the Symfony bundle is the closest template, for a classic CMS the WordPress plugin.
Check before release
- Form submitted without JavaScript — does your fail rule apply?
- Token used twice — the second check must fail, the nonce is single use.
- Token submitted after more than five minutes — the widget renews it by itself, verify that in a long-running test.
- Two protected forms on one page — both must work independently.
- Form loaded in via AJAX — does the widget appear?
- The secret key appears nowhere in the HTML or in JavaScript.
- A response with decision=step_up — the user must not end up in a dead end.
Building an integration and want us to list it here or help maintain it? Get in touch — we provide test accounts and a contact person.