# How to add hCaptcha to a form, with the server check

Add the hCaptcha widget to an HTML form and verify the token on your server with siteverify. Tested with hCaptcha's own test keys on 3 November 2026.

Author: Formgong
Published: 2026-11-03
Language: en
Canonical: https://formgong.com/en/blog/add-hcaptcha-to-form/

Add hCaptcha's script and a div with class h-captcha and your site key to the form. The widget puts a token in the h-captcha-response field; your server posts it with your secret to hCaptcha's siteverify and accepts the form only on success: true. With the test keys, 5 of 8 calls we made came back as success, including the same token twice.

## Why is the hCaptcha widget not enough on its own?

The checkbox on the page stops nobody. A bot does not need your page at all: it can post straight to the address in your form's `action`. The widget only hands the visitor a token, and the protection is the server code that asks hCaptcha whether that token is real. If that call is missing, the form works perfectly for people and bots alike, and nothing tells you.

So this guide spends most of its time on the server side. On 3 November 2026 we ran it end to end: hCaptcha's real script in Chromium, hCaptcha's real siteverify, and a local copy of Formgong's form backend behind a small Node server. If you are still choosing between captchas, our [Turnstile, reCAPTCHA and hCaptcha comparison](/en/blog/turnstile-vs-recaptcha-vs-hcaptcha/) answers that first.

## How do you add hCaptcha to the page?

Load `https://js.hcaptcha.com/1/api.js` and put a `div` with class `h-captcha` and your site key where the checkbox should appear. When the visitor passes, hCaptcha adds a hidden field named `h-captcha-response` to the form. It also fills `g-recaptcha-response` with the same token, so code written for reCAPTCHA keeps working; we saw both fields hold the identical value.

The script below refuses to send before the box is ticked, and calls `hcaptcha.reset()` after a failed send. hCaptcha's docs say a token "can only be used once", so a second try with the old one would fail on the server.

```html
<form id="contact">
  <label for="name">Name</label> <input id="name" name="name" required>
  <label for="email">Email</label> <input id="email" name="email" type="email" required>
  <label for="message">Message</label> <textarea id="message" name="message" required></textarea>
  <div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>
  <button type="submit">Send</button>
  <p id="form-status" role="status"></p>
</form>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<script>
  var BACKEND = "/contact"; // your own server route (below)
  var form = document.getElementById("contact");
  var statusLine = document.getElementById("form-status");
  form.addEventListener("submit", function (event) {
    event.preventDefault();
    if (!form.reportValidity()) return;
    var token = form.elements["h-captcha-response"] ? form.elements["h-captcha-response"].value : "";
    if (!token) { statusLine.textContent = "Please tick the box first."; return; }
    statusLine.textContent = "Sending…";
    fetch(BACKEND, { method: "POST", body: new URLSearchParams(new FormData(form)) })
      .then(function (response) { return response.json(); })
      .then(function (data) {
        statusLine.textContent = data.success ? "Thanks, your message was sent." : (data.message || "Not sent.");
        if (!data.success) hcaptcha.reset(); // a token works once: get a fresh one for the next try
      })
      .catch(function () { statusLine.textContent = "Not sent. Please try again."; });
  });
</script>
```

## How do you verify the hCaptcha token on the server?

Post the token and your secret key to `https://api.hcaptcha.com/siteverify` as form data, not JSON, and read `success`. Two optional fields are worth sending. `sitekey` makes hCaptcha refuse a token that was solved for a different widget; its docs recommend it so that tokens "issued on one sitekey" cannot be "redeemed elsewhere". `remoteip` is the visitor's address, which the docs mark as recommended.

Do not lean on the `hostname` field in the answer. With reCAPTCHA and Turnstile it is part of a good check, but hCaptcha's docs say it comes from the browser, "should not be used for authentication of any kind", and can read `not-provided` under heavy traffic. The `sitekey` parameter does that job instead.

```javascript
export const SITEVERIFY = "https://api.hcaptcha.com/siteverify";

// Send your site key too: hCaptcha then refuses a token solved for another of your widgets.
export async function siteverify(secret, token, { sitekey, ip } = {}) {
  const body = new URLSearchParams({ secret, response: token ?? "" });
  if (sitekey) body.set("sitekey", sitekey);
  if (ip) body.set("remoteip", ip);
  const response = await fetch(SITEVERIFY, { method: "POST", body }); // form-encoded, never JSON
  return response.json();
}
```

**The small Node backend that uses it**

```javascript
// Between the page and the form backend: verify the token, then forward the fields without it.
import http from "node:http";
import { siteverify } from "./verify.mjs";
const SECRET = process.env.HCAPTCHA_SECRET;
const SITEKEY = process.env.HCAPTCHA_SITEKEY;
const FORM_ENDPOINT = "https://formgong.com/submit";
const ACCESS_KEY = "fk_your_access_key";

http.createServer(async (req, res) => {
  const headers = { "access-control-allow-origin": "https://your-site.example", "content-type": "application/json" };
  let raw = ""; for await (const chunk of req) raw += chunk;
  const fields = new URLSearchParams(raw);
  const token = fields.get("h-captcha-response");
  fields.delete("h-captcha-response");
  fields.delete("g-recaptcha-response"); // hCaptcha fills this name too, with the same token
  const result = await siteverify(SECRET, token, { sitekey: SITEKEY, ip: req.socket.remoteAddress });
  if (result.success !== true) {
    res.writeHead(403, headers);
    return res.end(JSON.stringify({ success: false, message: "The check did not pass. Please tick the box again." }));
  }
  fields.set("access_key", ACCESS_KEY);
  const forwarded = await fetch(FORM_ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body: fields });
  res.writeHead(forwarded.status, headers);
  res.end(await forwarded.text());
}).listen(8792, "127.0.0.1");
```

## What can you test with hCaptcha's test keys?

hCaptcha publishes a test pair: site key `10000000-ffff-ffff-ffff-000000000001` and secret `0x0000000000000000000000000000000000000000`. Its docs say this pair "will never challenge" and always produces the same token. In our run a click on the checkbox produced a token in about 1.2 to 1.5 seconds, and the token was always `10000000-aaaa-bbbb-cccc-000000000001`. Then we asked siteverify about it:

hCaptcha's siteverify answers with the published test secret, 3 November 2026. Raw JSON in our notes.
What we sentWhat hCaptcha answered
Test secret + the page's token`success: true`, hostname `dummy-key-pass`
Test secret + the same token again`success: true` again
Test secret + the token + the test site key as `sitekey``success: true`
Test secret + the token + another test site key`success: true`
Test secret + the token + a made-up site key`success: true`
Test secret + the made-up token `abc``success: false`, `invalid-input-response`
Test secret + no token`success: false`, `missing-input-response`
A wrong secret + the page's token`success: false`, `not-using-dummy-secret`
So the test pair proves that your page and your server are wired together, and that a missing or garbage token is refused. It cannot show the single-use rule, because the same token passed twice, and it cannot show the `sitekey` check, because every site key passed. One error code also differs from the docs: a wrong secret came back as `not-using-dummy-secret`, while the docs list `not-using-dummy-passcode`. If your code matches error codes by name, log the raw answer before you rely on it.

## Does hCaptcha work on localhost?

With the test keys, yes. The widget rendered and passed on `http://127.0.0.1` and on a local page we served as `example.com`, 2 of 2 hosts. hCaptcha's docs warn that its API prohibits `localhost` and `127.0.0.1` as hostnames and suggest a hosts-file entry such as `127.0.0.1 test.mydomain.com`, and Formspree's hCaptcha guide says you cannot run hCaptcha on 127.0.0.1 at all. We did not test a real key, so if a real key fails locally, the hosts entry is the fix both of them give.

## What happened when we sent the form?

With the box ticked, the page posted to our Node server, siteverify said yes, and the local Formgong backend stored the message without the token. Without the tick, the page stopped at "Please tick the box first." and sent nothing. That second case is the browser's check only. A bot posting straight to your server is stopped by the `success` test, which the `abc` and empty-token rows above cover.

Captchas are not the only option. A hidden [honeypot field](/en/blog/honeypot-form-field/) catches simple bots without any third-party script, and our guide to [stopping spam without a CAPTCHA](/en/blog/contact-form-spam-without-captcha/) lists what to add next. The weight each widget adds to your page is in [the captcha speed test](/en/blog/turnstile-vs-hcaptcha-vs-recaptcha-speed/).

## Does Formgong verify hCaptcha?

No. If you post an hCaptcha token straight to `https://formgong.com/submit`, Formgong stores it as an ordinary field. To use hCaptcha with Formgong, put a small server like the one above in front of it. Other form backends do more here: Formspree's hCaptcha guide says it supports hCaptcha out of the box with your own secret key, and the Static Forms hCaptcha article (13 March 2026) puts it on the Starter plan and up. Both pages were read on 3 November 2026.

What Formgong verifies itself is Cloudflare Turnstile, on the Free plan too, with your own keys. It also checks that the token was solved on your own site, one of your allowed domains or the page that sent the form. The setup and its error codes are in our guide to [Turnstile siteverify errors](/en/blog/turnstile-timeout-or-duplicate/), and our [reCAPTCHA v3 guide](/en/blog/recaptcha-v3-setup/) shows the same proxy pattern for Google's widget.

## Did hCaptcha answer differently for you?

If a real key gives answers that differ from these, or you have seen the `sitekey` check refuse a token, write to [support@formgong.com](mailto:support@formgong.com). We will add what you found, with your permission.

## Frequently asked questions

### What are the hCaptcha test keys?

Site key 10000000-ffff-ffff-ffff-000000000001 with secret 0x0000000000000000000000000000000000000000. They never show a challenge and always return the same token.

### Which field holds the hCaptcha token?

h-captcha-response. hCaptcha also fills g-recaptcha-response with the same token, so code written for reCAPTCHA still finds it.

### Should I check the hostname in the hCaptcha answer?

No. hCaptcha says the hostname comes from the browser and should not be used for authentication. Send the sitekey parameter instead.

### Does Formgong verify hCaptcha tokens?

No. Formgong verifies Cloudflare Turnstile and uses honeypot fields. An hCaptcha token sent to it is stored as an ordinary field.

## Sources and documentation

- [Formgong: Turnstile vs reCAPTCHA vs hCaptcha](https://formgong.com/en/blog/turnstile-vs-recaptcha-vs-hcaptcha/)
- [Formgong: reCAPTCHA v3 setup with the server score check](https://formgong.com/en/blog/recaptcha-v3-setup/)
- [HTML forms](https://formgong.com/en/docs/html/)

[Get a form key](https://formgong.com/en/#top)
