What an hCaptcha token is, and why you cannot reuse one.
Pass an hCaptcha challenge and the widget hands the page a token: a long
string starting with P1_ that lands in a hidden form field,
expires in about two minutes, and verifies exactly once. On its own it
proves nothing. The site's server has to send it to siteverify
before it counts. Here is the full lifecycle, from mint to verification.
The token is the whole point of the challenge
Everything hCaptcha does, the checkbox, the image grid, the invisible
background check, exists to produce one artifact: a response
token. hCaptcha's servers issue it when a challenge passes, the
widget receives it through its iframe, and from there the page just moves
the string around. The hidden field, the form POST, the
siteverify call: each step carries the same token.
If you are automating a site behind hCaptcha, this reframes the problem: the widget itself is incidental. The job is getting a valid token into the right field before the form goes out.
Token format: the P1_ string
Every modern hCaptcha token starts with P1_, followed by
base64url segments shaped like a JWT:
P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...UV8w
(truncated: a real token runs well past 1,000 characters)
The eyJ right after the prefix is base64 for {",
the start of an encoded JSON header. The payload behind it is opaque, and
the signature is validated on hCaptcha's side, never yours. Decoding the
token locally gets you nothing you can act on, and there is no documented
way to construct one offline. A valid token always traces back to a passed
challenge.
Where it lands: the h-captcha-response field
The widget injects a hidden <textarea name="h-captcha-response">
inside its container and writes the token there on a pass. It also fills a
second hidden textarea named g-recaptcha-response with the
same value, so backends originally written for reCAPTCHA keep working
without a code change. When the user submits, the token travels as an
ordinary POST parameter:
<form action="/login" method="POST">
<!-- ...your inputs... -->
<div class="h-captcha" data-sitekey="f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8">
<!-- injected by the widget; both get the same token on a pass -->
<textarea name="h-captcha-response" style="display:none">P1_eyJ0eXAi...UV8w</textarea>
<textarea name="g-recaptcha-response" style="display:none">P1_eyJ0eXAi...UV8w</textarea>
</div>
<button type="submit">Log in</button>
</form>
Server-side, the site reads h-captcha-response out of the form
body, the same way it would read any other input. Nothing about the field
is special; it is just where the string waits between the pass and the
POST.
How the page reads the token
Not every site waits for a form submit. The hCaptcha JS API exposes the token three ways, and which one a site uses matters when you are injecting a token from outside:
- The hidden field. The default. The site reads the POST parameter and never touches JavaScript.
-
data-callback. The widget calls the named function with the token as its only argument the moment the challenge passes. Single-page apps usually fire their request from here, so a token sitting quietly in the textarea does nothing until the callback runs. -
hcaptcha.getResponse(). Returns the current token for a widget on demand. Invisible widgets pair it withhcaptcha.execute(), which triggers the challenge and, in async mode, resolves a promise with the token.
The injection snippet at the end of this page covers the first two cases: fill the fields, then invoke the site's callback if it has one.
Lifetime: about 120 seconds, exactly one use
Two hard rules govern every token. First, it expires roughly
120 seconds after issue (hCaptcha's documented default).
Past that, siteverify answers expired-input-response
and the widget fires its data-expired-callback so the page can
demand a fresh solve. Second, it is single-use. The first
successful verification burns it; a second attempt with the same token
returns already-seen-response. Older docs and a lot of
libraries report the same condition under the combined code
invalid-or-already-seen-response. Our
error codes guide covers the
full table.
The practical consequence for automation: mint the token immediately before you need it, and never cache one for later. A stockpile of pre-solved tokens is a stockpile of expired strings.
A token means nothing until siteverify sees it
The green check in the browser is feedback for the human. The decision that
matters happens server-side: the site POSTs the token together with its
secret key to https://api.hcaptcha.com/siteverify, and
hCaptcha answers with {"success": true} or an error code.
A passing response also echoes challenge_ts (when the
challenge was solved) and hostname (where), so a careful
backend can reject tokens minted on the wrong domain. Until that call, the
token is an unverified claim. The full request and response shapes are in
our siteverify guide.
This server-side check is why copied tokens fail. A token scraped from someone else's session is already burned or seconds from expiry, and it only verifies against the sitekey it was minted for. Fake-token generators fare worse: a fabricated
P1_string may sit in the form field looking plausible, butsiteverifyrejects it asinvalid-input-responseon first contact.
Why a token gets rejected
When a token that looked fine in the browser fails on the server, the cause is almost always one of four things:
- It expired. More than ~120 seconds passed between mint
and verification.
expired-input-response. - It was already verified. The same token hit
siteverifytwice.already-seen-response, or the combinedinvalid-or-already-seen-response. - Wrong key pair. The token was minted under a different
sitekey than the secret used to verify it, or the string is malformed.
invalid-input-response. - Missing
rqdatabinding. Enterprise only: the token was minted without the session'srqdatablob, so verification fails even though the token itself is genuine.
Each code, plus the client-side errors the widget can throw, is broken down in the error codes guide.
Enterprise tokens are bound to rqdata
Enterprise sitekeys add one more binding. The page passes a per-session
rqdata blob into the widget, and the token that comes out is
tied to it. A token minted without the matching rqdata looks
identical, lands in the same field, and still fails verification. So for
enterprise targets you capture the live blob alongside the sitekey and send
both with type: "hcaptcha_enterprise". The capture process is
covered in our rqdata guide.
Getting a token over an API
To get a token without touching the widget, send the page's
sitekey and URL to NoneCap's
solve endpoint and block on the result with
?wait:
curl "https://api.nonecap.com/v1/solves?wait=90" \
-H "Authorization: Bearer $NONECAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "hcaptcha",
"sitekey": "f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8",
"url": "https://target.example/login"
}' {
"id": "solve_01HQF7K3JKWZX",
"object": "solve",
"type": "hcaptcha",
"status": "solved",
"token": "P1_eyJ0eXAi...UV8w",
"credits_charged": 1
}
The token field is a real P1_ token that passes
siteverify, with the same 120-second clock already running.
Drop it into both hidden fields and submit:
// Inside page.evaluate() in Puppeteer or Playwright:
const token = "P1_eyJ0eXAi...UV8w"; // from the solve response
for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
document.querySelectorAll(`textarea[name="${name}"]`)
.forEach((el) => { el.value = token; });
}
// If the form waits on the widget's data-callback, invoke it with
// the token too, then submit within the ~120 s window. Billing starts at one credit per hCaptcha challenge round, charged on success only, at $0.25 to $0.50 per 1,000 credits, and new accounts start with 100 free credits (pricing). The same flow works from the official SDKs on npm and PyPI, and the Puppeteer and Playwright guides show the end-to-end run: read the sitekey, mint the token, inject, submit.
Last updated June 2026.
Frequently asked
What does an hCaptcha token look like?
P1_ followed by base64url text, for example P1_eyJ0eXAi...UV8w (truncated). The eyJ right after the prefix is an encoded JSON header, like a JWT. The payload is opaque and the signature is checked by hCaptcha’s servers, so decoding it locally tells you nothing useful. Real tokens run well past 1,000 characters.How long is an hCaptcha token valid?
siteverify rejects it with expired-input-response and the widget fires its data-expired-callback. Mint the token right before you submit the form rather than ahead of time.Can I reuse an hCaptcha token?
siteverify call for the same token returns already-seen-response (older docs and many libraries use the combined code invalid-or-already-seen-response). The full list is in our hCaptcha error codes guide.Why does an hCaptcha page have a g-recaptcha-response field?
h-captcha-response and a g-recaptcha-response mirror, so a backend originally written for reCAPTCHA can keep reading the field name it already knows. The values are identical.How do I get an hCaptcha token programmatically?
P1_ token that passes siteverify. Billing starts at one credit per hCaptcha challenge round, charged on success only, at $0.25 to $0.50 per 1,000 credits. New accounts get 100 free credits. See pricing.