Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Hcaptcha Laravel Package

scyllaly/hcaptcha

Scyllaly/hCaptcha is a Laravel wrapper for hCaptcha, providing a Blade directive to render the widget plus a validation rule and server-side verification to protect forms from bots. Simple config for site and secret keys makes it a drop-in spam solution for Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes)

composer require scyllaly/hcaptcha
php artisan vendor:publish --provider="Scyllaly\HCaptcha\HCaptchaServiceProvider"

On Laravel 5.5+ the provider (Scyllaly\HCaptcha\HCaptchaServiceProvider) and the HCaptcha facade (Scyllaly\HCaptcha\Facades\HCaptcha) are auto-discovered. On older apps, add both to config/app.php.

Credentials. Grab a site key and secret from the hCaptcha dashboard and put them in .env, not in config/hcaptcha.php directly:

HCAPTCHA_SITEKEY=your-site-key
HCAPTCHA_SECRET=your-secret

Where to look first. After publishing, open config/hcaptcha.php — it's small and holds secret, sitekey, and the default error_message returned by the validator. Then open vendor/scyllaly/hcaptcha/src/HCaptcha.php to see the three methods you'll actually call: renderJs(), display(), and verifyResponse(). That's the whole API surface; everything else is Laravel plumbing.

First use case — a contact form. In your Blade form:

<form method="POST" action="/contact">
    @csrf
    <input type="text" name="email">
    <textarea name="message"></textarea>

    {!! HCaptcha::renderJs() !!}
    {!! HCaptcha::display() !!}

    <button type="submit">Send</button>
</form>

Then validate the token the widget injects (h-captcha-response):

$request->validate([
    'email'   => 'required|email',
    'message' => 'required',
    'h-captcha-response' => 'required|HCaptcha',
]);

That single rule hits hCaptcha's /siteverify endpoint server-side. If it fails, the rule fails with the message from config('hcaptcha.error_message').

Implementation Patterns

Form Request validation (recommended). Rather than repeating the rule in every controller, put it in a FormRequest and reuse it:

class ContactRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email' => 'required|email',
            'message' => 'required',
            'h-captcha-response' => 'required|HCaptcha',
        ];
    }
}

This is the pattern to standardize on — the rule name and token field are easy to get subtly wrong, and centralizing them means one place to fix.

Manual verification. When you can't use the rule (e.g. an API endpoint receiving JSON, or you want custom error handling), call the facade directly:

if (! HCaptcha::verifyResponse($request->input('h-captcha-response'))) {
    return back()->withErrors(['captcha' => 'Verification failed, please retry.']);
}

Keep this close to the controller, not buried in a service, so it's obvious where the external call happens.

SPA / Inertia / Vue / React. Don't use HCaptcha::display() here. Instead load the script once in your layout with {!! HCaptcha::renderJs() !!}, then render the widget client-side via the hCaptcha JS API in your component's mounted hook, and submit the token as a normal field. The server side stays identical — one rule, one token field.

Livewire. Re-render the widget after every Livewire navigation or you'll submit a token tied to a stale widget. A common pattern is a tiny Alpine/Livewire hook that calls hcaptcha.reset() and re-renders on livewire:navigated.

Testing. Never let tests hit the real hCaptcha API. Two options:

  1. Use hCaptcha's official test keys (10000000-ffff-ffff-ffff-000000000001 / 0x0000000000000000000000000000000000000000) in your .env.testing. Test keys always pass verification.
  2. Mock the facade in the test: HCaptcha::shouldReceive('verifyResponse')->andReturn(true); — faster and hermetic, but it means your rule wiring is untested, so keep at least one feature test using the test keys.

Blade component. Wrap the two facade calls in an anonymous Blade component (<x-hcaptcha />) so page templates stay clean and you have one place to change rendering attributes if hCaptcha updates its markup.

Gotchas and Tips

The token field name is h-captcha-response. Not g-recaptcha-response (though hCaptcha injects a compatibility alias, don't rely on it). If validation "always fails" with required, this is almost always the cause — dump $request->all() and look at what the widget actually posted.

The validation rule alias varies by release. Depending on version it's exposed as HCaptcha or aliased as hcaptcha. Confirm what your installed version registers by reading HCaptchaServiceProvider::boot(). If the rule silently does nothing (validation passes with a garbage token), the alias is wrong — Laravel ignores unknown rule names in older versions.

Tokens are single-use and short-lived (~2 minutes). Never validate twice (no sometimes + explicit verifyResponse), never cache the result, and don't render the widget inside a modal that's been open for 10 minutes — reset it on open.

Behind a load balancer, IP forwarding breaks. verifyResponse sends the client IP, and hCaptcha will reject tokens if the IP doesn't match. Ensure TrustProxies is configured correctly, otherwise every request from behind your ALB/Cloudflare appears to come from the proxy and verification fails intermittently.

config:cache will bite you. Config is cached in production; changing HCAPTCHA_SECRET in .env does nothing until you php artisan config:clear and redeploy. This is the single most common "it works locally" bug with this class of package.

Don't leak the secret. HCAPTCHA_SITEKEY is public (it's rendered into HTML); HCAPTCHA_SECRET must never reach a Blade template or a JS bundle. Only renderJs()/display() output should hit the page.

Localization. hCaptcha's widget language is controlled by the hl parameter on the script tag, not by Laravel's locale. If you support multiple locales, pass the language to renderJs() explicitly rather than expecting it to follow App::getLocale().

Network failures look like captcha failures. verifyResponse returns a bool; a timeout, DNS failure, or 500 from hCaptcha all collapse into false and the user sees "please prove you're human." If captcha failures are critical to your signup funnel, log the raw response around the call so you can distinguish "user is a bot" from "our egress is broken."

Multiple widgets on one page. hCaptcha assigns ids; display() needs an explicit id attribute per instance or the second widget won't bind. Reset by id, not globally.

Custom error messages. Override error_message in the published config for a global change, or add a per-rule message in the FormRequest's messages() array if a specific form needs different wording.

Extension points are thin. The package doesn't expose an HTTP client or an events hook — if you need retries, custom timeouts, or telemetry, wrap HCaptcha::verifyResponse() in your own small service class rather than patching the vendor file. Because the API surface is just three methods, wrapping is cheap and keeps you upgrade-safe.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings