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:

An hCaptcha token (truncated)
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:

The hidden token fields after a pass
<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 with hcaptcha.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, but siteverify rejects it as invalid-input-response on 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 siteverify twice. already-seen-response, or the combined invalid-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 rqdata binding. Enterprise only: the token was minted without the session's rqdata blob, 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:

Create an hCaptcha solve
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"
  }'
Response
{
  "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:

Inject the token · Puppeteer / Playwright
// 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?
It starts with 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?
About 120 seconds by default, per hCaptcha’s docs. After that, 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?
No. Each token verifies exactly once. The second 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?
For drop-in compatibility. The hCaptcha widget writes the same token into both 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?
Send the page’s sitekey and URL to POST /v1/solves and NoneCap returns a real 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.

Start solving hCaptcha in minutes.

100 free credits on signup. Pay per solve, credits never expire, failed solves auto-refunded.