rvxlab/hcaptcha
Server-side hCaptcha verification for Laravel. Validate hCaptcha tokens in forms and APIs with a simple validation rule, configurable keys, and clear error messages. Lightweight, framework-native, and easy to drop into existing validation flows.
rvxlab/hcaptcha is a thin server-side wrapper around hCaptcha's siteverify endpoint for Laravel 8+. It gives you config, a validation rule, and (depending on version) a Blade helper for rendering the widget. Start by confirming it installs against your framework version — the package is archived and its constraints may not allow Laravel 11/12.
Minimal setup:
composer require rvxlab/hcaptcha
php artisan vendor:publish --tag=hcaptcha-config
Add credentials to .env (names may differ slightly by release; check config/hcaptcha.php after publishing):
HCAPTCHA_SITEKEY=your-site-key
HCAPTCHA_SECRET=your-secret-key
Render the widget in your form layout. hCaptcha's script is loaded from their CDN, not the package:
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<form method="POST" action="/contact">
@csrf
<div class="h-captcha" data-sitekey="{{ config('hcaptcha.sitekey') }}"></div>
<button type="submit">Send</button>
</form>
Validate on the server. The response field posted by the widget is h-captcha-response, and most hCaptcha packages accept the legacy g-recaptcha-response too:
$request->validate([
'h-captcha-response' => ['required', 'hcaptcha'],
]);
Where to look first: config/hcaptcha.php for key names and bypass switches, then the package's src/ for the rule class and any Blade directive/component. The first real use case is almost always a public contact, registration, or password-reset form.
Form Requests are the cleanest integration. Keep captcha validation next to the rest of the form rules:
class ContactRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:120'],
'email' => ['required', 'email'],
'message' => ['required', 'string', 'max:5000'],
'h-captcha-response' => ['required', 'hcaptcha'],
];
}
public function messages(): array
{
return [
'h-captcha-response.required' => 'Please complete the captcha.',
'h-captcha-response.hcaptcha' => 'Captcha verification failed. Please try again.',
];
}
}
Normalize the token field once. If you support both hCaptcha and reCAPTCHA markup, merge the token into a predictable key in prepareForValidation():
protected function prepareForValidation(): void
{
$this->merge([
'captcha' => $this->input('h-captcha-response')
?? $this->input('g-recaptcha-response'),
]);
}
Then validate 'captcha' => ['required', 'hcaptcha'].
Inertia / SPA / JSON APIs. The browser widget still populates a hidden input, but you can also read the token explicitly and send it in your payload:
const token = document.querySelector('[name="h-captcha-response"]')?.value;
await axios.post('/api/contact', { ...form, 'h-captcha-response': token });
On the server, validate it exactly as above. Tokens are single-use and short-lived, so do not cache or replay them. If your SPA renders multiple forms, reset the widget after a failed submit (hcaptcha.reset(widgetId)).
Centralize for many endpoints. Rather than copy the rule into every controller, wrap it in a dedicated rule object or a small middleware for abuse-prone public routes. A middleware is convenient for endpoints that are not traditional form posts, but remember to return a proper 422/400 rather than a redirect for JSON clients.
Combine with rate limiting. Captcha is not a substitute for throttling. Pair it with throttle: middleware on login and registration, and keep Laravel's built-in rate limiter as the first line of defense.
Configuration-driven bypass. For local development and CI, the package usually exposes a way to disable verification (commonly a config flag). Wire it to an env variable so tests never hit hCaptcha:
HCAPTCHA_ENABLED=true
// tests/TestCase.php or phpunit.xml
<env name="HCAPTCHA_ENABLED" value="false"/>
If no bypass flag exists, bind a fake rule or mock the HTTP client in tests instead of making real network calls.
Custom verification. When you need the response payload (hostname, challenge timestamp, error codes), skip the rule and call the package's verifier/service directly, or Http::asForm()->post('https://hcaptcha.com/siteverify', [...]) yourself. This is the cleanest path for logging suspicious attempts or returning granular errors.
HCAPTCHA_SITEKEY is public and belongs in the rendered HTML; HCAPTCHA_SECRET must never leave the server. A common failure is swapping them, which produces "invalid-input-secret" from the verify endpoint.h-captcha-response. If your validation rule looks for a different key, validation fails even though the user solved the challenge. Dump $request->all() when debugging..env or config/hcaptcha.php, run php artisan config:clear (or config:cache in deploy). Stale config is a frequent cause of "it works locally but not in production."Http::fake(), or a fake rule. A test suite that hits the live endpoint is flaky, slow, and rate-limited.render calls and unique IDs; the automatic h-captcha class scan only handles the simple case. Load api.js once per page.https://js.hcaptcha.com and the associated frame/style origins, or the widget silently fails to render.How can I help you explore Laravel packages today?