On this page
Overview
Wiz Captcha is a small, self-hosted CAPTCHA package for Laravel. It draws an image challenge on your own server with PHP’s GD extension, stores only a hash of the answer in your cache, and validates what the visitor types with a standard Laravel validation rule. There is no third-party service, script or tracking involved.
It supports classic form posts, where the image is served from a route and tied to the visitor’s session, and API clients, where you request a challenge as JSON and send the key back with the answer. The package is compact: one facade, three helpers, two validation rules and three challenge presets. It is built by the DevWizard core team and is MIT licensed.
Features
- Three challenge types: random text, numbers only, and simple math (
+and-). - Answers are hashed before storage and never sent to the browser.
- Challenges expire after a configurable time, and a challenge is deleted after too many wrong attempts.
- Image rendering with GD, with configurable size, noise, lines and background colour.
- Validation through
CaptchaRule, thewiz_captchastring rule, orCaptchaApiRulefor API flows. - A JSON endpoint that returns the image as a base64 data URI.
- Built-in route throttling, tunable through one environment variable.
- Cache-backed storage, which uses Redis automatically when your cache does.
- Swappable internals through the
CaptchaStore,ChallengeGeneratorandImageRenderercontracts.
Requirements
- PHP 8.2 or newer with the
gdextension. - Laravel 11, 12 or 13.
- A cache store (the default store works).
Installation
composer require devwizardhq/wiz-captchaPublish the configuration file if you want to change presets or limits:
php artisan vendor:publish --tag="wiz-captcha-config"Configuration
The published file is config/wiz-captcha.php.
| Key | Environment variable | Default | Purpose |
|---|---|---|---|
enabled |
WIZ_CAPTCHA_ENABLED |
true |
When false, verification always passes |
default |
none | default |
Preset used when none is named |
cache_store |
WIZ_CAPTCHA_CACHE_STORE |
app default | Cache store for challenges |
expire |
WIZ_CAPTCHA_EXPIRE |
300 |
Challenge lifetime in seconds |
max_attempts |
WIZ_CAPTCHA_MAX_ATTEMPTS |
5 |
Wrong answers before the challenge is discarded |
case_sensitive |
WIZ_CAPTCHA_CASE_SENSITIVE |
false |
Whether answers are case sensitive |
routes.enabled |
none | true |
Register the image and API routes |
routes.prefix |
none | captcha |
URL prefix |
routes.throttle |
WIZ_CAPTCHA_ROUTE_THROTTLE |
60,1 |
Requests per minute per client |
Three presets ship under presets: default (text, 6 characters), number (5 digits) and math (operands from 1 to 20 with + and -). Each sets width, height, lines, noise and background, and you can add your own. Set WIZ_CAPTCHA_ROUTE_THROTTLE to a bare 60,1 or a full middleware string such as throttle:120,1.
Usage
Show the image in a form and validate the answer in the controller:
<form method="POST" action="/contact">
@csrf
{!! wiz_captcha_img('default', ['id' => 'captcha-image']) !!}
<input type="text" name="captcha" required autocomplete="off">
@error('captcha')
<div>{{ $message }}</div>
@enderror
<button type="submit">Send</button>
</form>use DevWizardHQ\Captcha\Rules\CaptchaRule;
$request->validate([
'captcha' => ['required', new CaptchaRule],
]);The image route stores the challenge key in the visitor’s session, so the rule knows which challenge to check. To let visitors refresh the image, reload its source with a changing query string:
<script>
function refreshCaptcha() {
const img = document.getElementById('captcha-image');
img.src = "{{ route('wiz-captcha.image') }}?" + Date.now();
}
</script>API Reference
Helpers
| Helper | Returns |
|---|---|
wiz_captcha_src($preset = 'default') |
URL of the image route for a preset |
wiz_captcha_img($preset = 'default', $attributes = []) |
An <img> tag as HtmlString |
wiz_captcha_api($preset = 'default') |
Array with key, image (base64 data URI) and expires_in |
Validation rules
new CaptchaRule, or the string rulewiz_captcha: checks the answer against the latest challenge issued to the session.new CaptchaApiRule($key), or the string rulewiz_captcha_api: checks the answer against the challenge with that key.
Captcha class and facade
DevWizardHQ\Captcha\Captcha (also available as DevWizardHQ\Captcha\Facades\Captcha) has three public methods: create($preset) returns key, image, mime and expires_in; createApi($preset) returns the same with a base64 image; verify($key, $input) returns a boolean.
Routes
Under the configured prefix (default /captcha):
| Route | Name | Response |
|---|---|---|
GET /captcha/{preset?} |
wiz-captcha.image |
PNG image with no-cache headers |
GET /captcha/api/{preset?} |
wiz-captcha.api |
JSON challenge |
Contracts
CaptchaStore (put, get, forget, incrementAttempts), ChallengeGenerator (generate) and ImageRenderer (render). The package binds CacheStore and GdRenderer by default.
Examples
Use the math preset for a friendlier challenge:
{!! wiz_captcha_img('math', ['id' => 'captcha-image']) !!}Serve a challenge to a single-page app or mobile client:
Route::get('/auth/captcha', fn () => response()->json(wiz_captcha_api('math')));Validate the answer in the matching API endpoint:
use DevWizardHQ\Captcha\Rules\CaptchaApiRule;
$request->validate([
'captcha_key' => ['required', 'string'],
'captcha' => ['required', new CaptchaApiRule($request->captcha_key)],
]);Switch off the CAPTCHA in a local environment:
WIZ_CAPTCHA_ENABLED=falseFAQ & Troubleshooting
The image is broken or blank. The gd PHP extension must be installed and enabled. Check with php -m | grep gd.
Validation always fails. Form flows rely on the session, so the image route and the form must share the same session and cookies. For stateless clients use wiz_captcha_api() and CaptchaApiRule instead.
Users get “429 Too Many Requests” when refreshing. The routes are throttled. Raise the limit with WIZ_CAPTCHA_ROUTE_THROTTLE.
A correct answer was rejected. Challenges expire after WIZ_CAPTCHA_EXPIRE seconds and are discarded after WIZ_CAPTCHA_MAX_ATTEMPTS wrong tries. Matching is case insensitive unless WIZ_CAPTCHA_CASE_SENSITIVE is true.
Can I use my own storage or renderer? Yes. Bind your own implementation of CaptchaStore, ChallengeGenerator or ImageRenderer in a service provider.
Changelog & Support
Release notes are in the CHANGELOG and on the GitHub releases page. Report bugs in GitHub issues and security concerns through the repository’s security policy. Source: DevWizardHQ/wiz-captcha. For custom authentication or anti-abuse work, the DevWizard team takes project contracts and monthly retainers through the contact page.