CarCaptcha documentation
A dependency-free, MIT-licensed captcha with five car-themed challenges. Everything is served from carcaptcha.advitechandtravel.com — one script tag, no build step, no tracking cookies. Every driving puzzle is playable with the arrow keys or WASD as well as mouse and touch.
1. Quick start
Register your domain in the dashboard, verify it, copy your site key, then drop this into any page. The token produced by the widget must be checked on your server.
<!-- 1. a place for the widget + a hidden field for the token -->
<form id="signup" method="post" action="/signup">
<div id="captcha"></div>
<input type="hidden" name="cc-token" id="cc-token" />
<button type="submit">Create account</button>
</form>
<!-- 2. the widget -->
<script type="module">
import { CarCaptcha } from "https://carcaptcha.advitechandtravel.com/carcaptcha.js";
CarCaptcha.render("#captcha", {
sitekey: "cc_site_xxx", // from your dashboard
puzzles: ["parking", "select", "circle", "rotate", "slide"],
attempts: 3,
onVerified: (token, puzzle) => {
document.querySelector("#cc-token").value = token;
},
onFailed: (attemptsLeft, puzzle) => {
console.log(puzzle, "failed —", attemptsLeft, "tries left");
},
});
</script>2. Installation
Script tag — the snippet above; nothing to install. The module is cached at the edge and is under 40 kB.
Bundler — install the package if you prefer to self-host the code:
npm i carcaptcha # or: bun add carcaptcha / pnpm add carcaptcha
import { CarCaptcha } from "carcaptcha";
const widget = CarCaptcha.render("#captcha", {
sitekey: "cc_site_xxx",
onVerified: (token) => setToken(token),
});
widget.reset(); // start a new challenge
widget.destroy(); // remove the widget and its listeners
widget.verified; // boolean
widget.token; // string | nullReact — mount it in an effect and destroy it on unmount:
import { useEffect, useRef } from "react";
export function Captcha({ onToken }: { onToken: (t: string) => void }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
let widget: { destroy: () => void } | null = null;
import("carcaptcha").then(({ CarCaptcha }) => {
if (!ref.current) return;
widget = CarCaptcha.render(ref.current, {
sitekey: "cc_site_xxx",
onVerified: (t: string) => onToken(t),
});
});
return () => widget?.destroy();
}, [onToken]);
return <div ref={ref} />;
}3. Verifying your domain
Site keys only work on domains you have proved you own, so nobody can borrow your key or your quota. Two methods are available.
A. DNS TXT record (recommended)
Type: TXT
Name: _carcaptcha
Value: carcaptcha-site-verification=xxxxxxxxxxxxxxxx
# then press "Check DNS" in the dashboard.
# Propagation is usually minutes, occasionally up to 24h.B. Page code (no DNS access)
Some builders (Google Sites, free hosting) give you no DNS access.
Choose "No DNS access" when adding the domain and instead:
1. paste the verification code anywhere on your published page
(footer text is fine)
2. paste the full public page address, e.g.
https://sites.google.com/view/your-site
3. press "Check page"
We fetch that exact page and look for the code — no DNS needed.Until a domain is verified the widget refuses to run there and the request is logged in your analytics as domain-not-verified.
4. Website builders & iframes
WordPress, Wix and Joomla accept the script tag as-is inside a custom HTML block. Use the “Add to my website” button above for per-builder steps.
Builders that block custom code (some Google Sites and Workspace accounts) can embed the hosted widget page by URL instead:
https://carcaptcha.advitechandtravel.com/embed?sitekey=cc_site_xxx&puzzles=parking,circle
Use with Insert → Embed → By URL in builders that block custom HTML.
The frame posts the token to the parent page:
window.addEventListener("message", (e) => {
if (e.data?.type === "carcaptcha:token") {
document.querySelector("#cc-token").value = e.data.token;
}
});The widget auto-detects sandboxed frames, reports its own height to the parent so it is never clipped, and falls back to the framing page’s address when the browser hides the origin.
5. Widget options
| Option | Type | Default | Notes |
|---|---|---|---|
sitekey | string | — | Public key from the dashboard; enables domain locking, tokens and analytics. |
puzzles | string[] | all five | Any subset of parking, select, circle, rotate, slide. |
attempts | number | 3 | Failed tries before the widget locks and must be reset. |
onVerified | (token, puzzle) => void | — | Fires once the challenge is solved and a token is issued. |
onFailed | (attemptsLeft, puzzle) => void | — | Fires on every failed attempt. |
apiBase | string | script origin | Override only when self-hosting the script and the API separately. |
6. Widget instance API
CarCaptcha.render(target, options)— mounts into a CSS selector or element and returns the instance.reset()— clears the token and starts a fresh random challenge.destroy()— removes the DOM, canvases and all listeners.verified—trueonce solved.token— the single-use token, ornull.
Only one widget runs per page: rendering again replaces the previous instance, so a hot reload or a re-render never stacks challenges.
7. The five puzzles
| id | Challenge | Controls |
|---|---|---|
parking | Drive the old car into the free bay while NPC traffic circulates | Arrows / WASD, drag |
rotate | Spin the car until it faces the exit arrow | Arrows / A-D, drag |
slide | Drive up and stop exactly on the stop line | Arrows / W-S, drag |
select | Tap every tile that contains a car | Click / tap, Tab + Enter |
circle | Draw a circle at 85% roundness or better | Pointer / touch |
All vehicles share one physical footprint and collide with oriented bounding boxes, so bumping a parked car or an NPC blocks the move instead of overlapping it.
8. Server-side token verification
Never trust the browser. A token is single-use, bound to your domain and must be exchanged for a verdict using your secret key.
// Node / Bun / Deno / Cloudflare Workers — server side only
const res = await fetch("https://carcaptcha.advitechandtravel.com/api/public/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
secret: process.env.CARCAPTCHA_SECRET, // secret key — never ship to the browser
token, // token from onVerified
puzzle: "parking", // optional
}),
});
const result = await res.json();
if (!result.success) {
return new Response("Captcha failed: " + result.error, { status: 400 });
}import os, requests
r = requests.post(
"https://carcaptcha.advitechandtravel.com/api/public/verify",
json={"secret": os.environ["CARCAPTCHA_SECRET"], "token": token},
timeout=10,
).json()
if not r.get("success"):
raise ValueError(r.get("error", "captcha failed"))<?php
$res = json_decode(file_get_contents(
'https://carcaptcha.advitechandtravel.com/api/public/verify', false,
stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode([
'secret' => getenv('CARCAPTCHA_SECRET'),
'token' => $_POST['cc-token'],
]),
]])), true);
if (empty($res['success'])) { http_response_code(400); exit('Captcha failed'); }9. REST API reference
POST /api/public/verify
Body: secret (required), token (required), puzzle (optional).
// 200 OK
{ "success": true, "domain": "example.com", "verified_at": "2026-08-03T18:00:00.000Z" }
// 4xx
{ "success": false, "error": "invalid-secret" }| error | Meaning |
|---|---|
invalid-request | Malformed or missing JSON body |
invalid-secret | Unknown or revoked secret key |
domain-not-verified | The domain has not passed DNS or page verification |
unknown-sitekey | Site key does not exist (widget-side check) |
origin-mismatch | The page origin does not match the registered domain |
GET /api/public/sitecheck?sitekey=…&origin=…
Called by the widget at load time; returns { allowed, domain, reason }. You normally never call this yourself.
POST /api/public/event
Anonymous solve telemetry sent by the widget (site key, puzzle id, pass/fail, solve timestamp, timezone, page address). No cookies, no IP profiling, no personal data.
10. Analytics & telemetry
Every widget load and every solve attempt on a verified domain is recorded and shown on your analytics page: solve rate, pass/fail per puzzle, daily trends, per-site error history and the exact time each visitor passed. Numbers are drawn straight from your own traffic — nothing is simulated, so a brand-new site key starts at zero until the first real visitor loads the widget.
Telemetry only runs when a sitekey is passed. A widget rendered without one (a local demo, for example) is fully functional but reports nothing.
11. Security & CSP
- The secret key belongs on your server only — never in client JavaScript or a public repo.
- Tokens are single-use; verify them immediately and reject a request if verification fails.
- Site keys are locked to verified domains, so a stolen key is useless elsewhere.
- Always pair the captcha with rate limiting; a captcha slows bots, it does not replace server-side limits.
If you run a strict Content-Security-Policy, allow the host:
Content-Security-Policy:
script-src 'self' https://carcaptcha.advitechandtravel.com;
img-src 'self' https://carcaptcha.advitechandtravel.com data:;
connect-src 'self' https://carcaptcha.advitechandtravel.com;
frame-src https://carcaptcha.advitechandtravel.com; # only needed for the /embed URL method12. Accessibility
Puzzles expose ARIA labels and live regions, are fully keyboard operable, honour dark mode and reduced motion. Visual challenges exclude some users, so always offer a second path — email confirmation or manual review — for anyone who cannot complete one.
13. Troubleshooting
| Symptom | Fix |
|---|---|
| Widget never appears | The script must be a module: use type="module" and load it from the host above. |
| “Domain not verified” | DNS TXT record missing or still propagating — re-run Check DNS, or use the page-code method. |
| “Origin not allowed” | The page is served from a domain you have not registered; add that exact domain. |
| Blocked inside a builder | Use the /embed?sitekey=… URL method and listen for the carcaptcha:token message. |
| Arrow keys do nothing | Click the challenge once — it takes focus before capturing keys so the page never scrolls unexpectedly. |
| Analytics stay at zero | Telemetry needs a sitekey on a verified domain; confirm the key on the live page. |
CarCaptcha