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 | null

React — 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

OptionTypeDefaultNotes
sitekeystringPublic key from the dashboard; enables domain locking, tokens and analytics.
puzzlesstring[]all fiveAny subset of parking, select, circle, rotate, slide.
attemptsnumber3Failed tries before the widget locks and must be reset.
onVerified(token, puzzle) => voidFires once the challenge is solved and a token is issued.
onFailed(attemptsLeft, puzzle) => voidFires on every failed attempt.
apiBasestringscript originOverride 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.
  • verifiedtrue once solved.
  • token — the single-use token, or null.

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

idChallengeControls
parkingDrive the old car into the free bay while NPC traffic circulatesArrows / WASD, drag
rotateSpin the car until it faces the exit arrowArrows / A-D, drag
slideDrive up and stop exactly on the stop lineArrows / W-S, drag
selectTap every tile that contains a carClick / tap, Tab + Enter
circleDraw a circle at 85% roundness or betterPointer / 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" }
errorMeaning
invalid-requestMalformed or missing JSON body
invalid-secretUnknown or revoked secret key
domain-not-verifiedThe domain has not passed DNS or page verification
unknown-sitekeySite key does not exist (widget-side check)
origin-mismatchThe 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 method

12. 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

SymptomFix
Widget never appearsThe 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 builderUse the /embed?sitekey=… URL method and listen for the carcaptcha:token message.
Arrow keys do nothingClick the challenge once — it takes focus before capturing keys so the page never scrolls unexpectedly.
Analytics stay at zeroTelemetry needs a sitekey on a verified domain; confirm the key on the live page.