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

Laravel Laravel Package

captchaapi/laravel

Official Laravel SDK for captchaapi.eu, an EU-hosted, GDPR-compliant proof-of-work CAPTCHA. Ships a drop-in Blade component, validation rule, and Livewire trait. No cookies, no tracking, no Google—server-side verification keeps your secret key safe.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin, opinionated SDK: one Blade component, one validation rule, one Livewire trait. You can be productive in about five minutes.

Install and wire up:

composer require captchaapi/laravel
php artisan vendor:publish --tag=captchaapi-config
CAPTCHAAPI_SITE_KEY=pk_live_...
CAPTCHAAPI_SECRET_KEY=sk_live_...

Two rules that matter immediately: the site key is public (goes in the browser), the secret key never leaves the server. Get both from the captchaapi.eu dashboard before you touch code.

Where to look first:

  1. config/captchaapi.php — published by the command above. Read it top to bottom; it encodes the whole behavioural contract (enabled, fail_open, mode, preload, timeout, locale).
  2. The README sections Usage → Plain HTML form and README → Fail policy. Those two determine whether your form silently accepts bots.
  3. The dashboard for key provisioning and secret rotation.

First use case — a contact form:

{{-- layout <head> --}}
<x-captchaapi::widget />
<form action="/contact" method="POST" data-captcha>
    @csrf
    <input type="email" name="email" required>
    <button type="submit">Send</button>
</form>
$request->validate([
    'email'               => ['required', 'email'],
    'captchaapi_response' => ['required', 'string', new ValidCaptcha],
]);

That is the whole loop. If it works, everything else in the package is refinement of the same three seams: the widget script, the captchaapi_response field, and the server-side verify call.

Implementation Patterns

Prefer the string alias for terse rulesets. 'captcha' is registered as an alias for ValidCaptcha, so it drops into inline validation and FormRequests without an import:

'captchaapi_response' => ['required', 'captcha'],

Livewire — trait plus wrapper, not hand-rolled wiring. The trait injects the rule and the wrapper handles the event round-trip:

use Captchaapi\Laravel\Concerns\WithCaptcha;

class RegisterForm extends Component
{
    use WithCaptcha;
    public string $email = '';

    public function register(): void
    {
        $this->validateWithCaptcha(['email' => 'required|email']);
        // captchaapi_response verified
    }
}
<x-captchaapi::livewire-form action="register" class="space-y-4">
    <input wire:model="email" type="email" required>
    <button type="submit">Register</button>
</x-captchaapi::livewire-form>

If you are inside a validateOnly() flow and cannot use the sugar, rulesForCaptcha() is public — array_merge($yourRules, $this->rulesForCaptcha()) is exactly what the sugar does, so compose it yourself rather than reaching for validate() in a piecemeal way.

Render errors with the shipped component so the field name stays consistent. captchaapi_response is the fixed field name; <x-captchaapi::error /> defaults to it. Use it for both redirect-with-errors (plain forms) and Livewire, and override for/as/class attributes when you need different markup.

Tier your fail_open setting by route risk. The default is true — a vendor outage will not block submissions, but neither will a 5xx. For login, registration, password reset, and payment it should be false. In practice this means the setting is nearly route-dependent, but the config is global, so the workable pattern is: keep the global default conservative (false) and opt out on low-risk public forms, or accept a per-application decision and document it. This is the single most important judgement call in the whole integration.

Use the status element as a deliberate UX surface. <div data-captcha-status></div> is opt-in and the widget never injects it. For a proof-of-work CAPTCHA the visitor sees nothing while solving, so an explicit waiting → solving → ready indicator is the difference between "did it submit?" and a confident click. Add it to any form where the solve time is noticeable (eager preload, slow devices).

Kill-switch for CI and staging. CAPTCHAAPI_ENABLED=false makes the rule pass silently and the widget render nothing, with zero code changes. This is the right lever for local, CI, and preview environments where no live site key exists — keep the trait, rule, and markup intact so environments do not drift.

Testing — fake mode, and prove the single-use contract. FakeCaptchaapi::enable() in beforeEach, disable() in afterEach. If you are integrating with Fortify (or anything that validates twice in one request), add FakeCaptchaapi::enforceSingleUse() — otherwise a missing per-request memo passes silently and you ship a real bypass.

Rotate secrets with zero-downtime deploy ordering. Dashboard → Rotate secret key → update .env → deploy → activate new → retire old. Both keys verify during the overlap, so the deploy timing is irrelevant. Only Revoke immediately (compromise path) causes in-flight solves to fail. Note this sequencing in your runbook; nobody remembers it at 2am.

Override base_url for staging or a mirror. Useful if you proxy the API or test against a non-production origin — the same key governs both the widget script origin and the server verify call.

Gotchas and Tips

  • fail_open=true is the default and it is a real bypass in the wrong place. Every 5xx or timeout lets the submission through. Audit which routes rely on the global default; anything authenticated or financial should assert false. There is no automatic retry either way — the response is single-use and re-verifying spends a token the visitor already solved.
  • The response token is single-use. Do not wrap verification in a retry loop, and do not call the rule twice for the same payload outside the memoized path. If you see intermittent failures on a login form, suspect double validation (Fortify) before you suspect the widget.
  • waiting and ready share the same shield icon. If you add data-captcha-no-color, those two states become visually identical. Supply per-state colours yourself or you have built a confusing indicator.
  • Default state colours are low-specificity by design. Override with two attribute selectors ([data-captcha-status][data-captcha-state="ready"]), not !important. The cascade already favours you.
  • Missing CAPTCHAAPI_SITE_KEY fails quietly on the front end. The widget renders nothing, but with enabled=true the server rule still runs — you get either a rejected form or a fail-open pass depending on config. Staging environments with half-configured keys are the classic source of "captcha works for me".
  • Put <x-captchaapi::widget /> in the layout, not inside the Livewire component. Livewire re-renders the component DOM; the script belongs in <head> of the page shell. Repeating it inside a component is a plausible-looking mistake that produces duplicate script injection.
  • mode is the Livewire/SPA switch. submit is native form POST, event dispatches a CustomEvent. The livewire-form wrapper already sets data-captcha-mode="event"; only set CAPTCHAAPI_MODE globally if you are driving a custom SPA integration, or you will break plain forms.
  • preload=lazy is the right default; eager trades first-paint work for a shorter perceived wait. Measure before changing it — eager on a heavy landing page is a self-inflicted performance regression.
  • CAPTCHAAPI_ENABLED=false is a permanent kill-switch, not a test bypass. For tests use FakeCaptchaapi::enable(). Conflating them means your test suite depends on an environment variable and your production bypass depends on someone remembering to flip it back.
  • FakeCaptchaapi throws outside the testing environment. That is a feature (it cannot leak onto an Octane worker), but it also means a fake left enabled in a non-test bootstrap will blow up loudly rather than silently — read the exception as a configuration bug, not a package bug.
  • Octane is safe by design, but only if you do not cache around it. Verification is stateless per call and the per-request memo lives on the request instance, so don't "optimise" by memoizing verification results on your own singleton — you will reintroduce cross-request leakage the package deliberately avoided.
  • Locale resolution is three-tier (CAPTCHAAPI_LOCALE → <html lang> → en). Set the config explicitly in multilingual apps; relying on <html lang> is fine until the one page that omits it.
  • Maturity caveat. One star, zero dependents, single-vendor API. Pin the version in composer.json, read the CHANGELOG before upgrades, and keep the integration surface small (rule + trait + two components) so swapping providers is a contained change rather than a refactor.
  • base_url is your escape hatch and your risk. Pointing it at a self-hosted or proxied origin is supported, but it also moves where the secret key is sent — treat that env var as production-sensitive, not a debugging convenience.
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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox