hCaptcha error codes, explained.
hCaptcha errors come from two different places, and the fix depends on which
one you are looking at. Server-side, siteverify returns an
error-codes array like invalid-or-already-seen-response.
Client-side, the widget raises codes like rate-limited and
challenge-expired. This page lists every code from both sides,
what each one means, and what actually fixes it.
First: which side produced the error?
Before debugging, place the error. If the string came back in a JSON body from
https://api.hcaptcha.com/siteverify, it is a server-side
verification error: your backend sent something siteverify rejected. If it
surfaced in the browser, in an error-callback, a rejected
hcaptcha.execute() promise, or a message inside the widget, it is a
client-side widget error: the widget could not load, run, or finish a
challenge. The two sets do not overlap, so the code alone tells you where to look.
siteverify error codes (the error-codes array)
Your server validates a token by POSTing it to siteverify as a form-encoded body:
curl https://api.hcaptcha.com/siteverify \
-d "secret=$HCAPTCHA_SECRET" \
-d "response=P1_eyJ0eXAi...UV8w" \
-d "remoteip=203.0.113.7"
On failure, success is false and error-codes
says why:
{
"success": false,
"error-codes": ["invalid-or-already-seen-response"]
} Here is the full list, straight from hCaptcha’s docs, with the practical fix for each:
| Code | What it means | The fix |
|---|---|---|
missing-input-secret | No secret parameter reached siteverify | Send secret as a form field in the POST body, not as a header or JSON key |
invalid-input-secret | The secret is malformed or wrong | Copy the secret fresh from the hCaptcha dashboard; check for a stale env var or trailing whitespace |
missing-input-response | No response parameter (the token) was sent | Read h-captcha-response from the submitted form; it is empty when the user never solved the widget |
invalid-input-response | The token is invalid or malformed | Send the full P1_… string untouched. Truncation, URL-decoding twice, or sending the wrong field all land here |
expired-input-response | The token was older than the validity window (120 s default) | Verify immediately after the form arrives, and mint a fresh token for every attempt |
already-seen-response | The token was already verified once | Tokens are single-use. Never retry siteverify with the same token; get a new one instead |
invalid-or-already-seen-response | Combined form of the two rows above; what most integrations actually receive | Same fixes: one fresh token per siteverify call, used within ~120 s |
bad-request | The request itself is malformed | POST a form-encoded body (application/x-www-form-urlencoded); siteverify does not accept JSON |
missing-remoteip | The remoteip parameter is missing | Include the end user’s IP, or drop the parameter entirely; it is optional |
invalid-remoteip | remoteip is not a valid IP address | Pass one real client IP, not unknown or a comma-joined X-Forwarded-For chain |
not-using-dummy-passcode | A test sitekey was paired with a real secret | Test keys only work with their matching dummy secret; see hCaptcha test keys |
sitekey-secret-mismatch | The sitekey is not registered to the account that owns the secret | Use the sitekey and secret from the same hCaptcha site entry; this hits after dashboard reshuffles |
A quirk that trips people up: hCaptcha’s current docs list
expired-input-response and already-seen-response as separate
codes, but live siteverify responses very often return the combined
invalid-or-already-seen-response instead. Treat the three as one family.
Whichever spelling you get, the token you sent is dead and you need a fresh one.
Widget and client-side errors
The widget reports errors through the error-callback you register at
render time. Async hcaptcha.execute() calls can also reject with a few
extra codes (challenge-closed, challenge-expired,
missing-captcha, invalid-captcha-id) that never reach the
callback because they describe the call, not the widget.
<div class="h-captcha"
data-sitekey="f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8"
data-error-callback="onHcaptchaError"></div>
<script>
function onHcaptchaError(code) {
// "rate-limited", "network-error", "challenge-error", ...
console.warn("hCaptcha error:", code);
}
</script> | Code | When it fires | What to do |
|---|---|---|
rate-limited | Too many requests from this client; often paired with HTTP 429s to hCaptcha endpoints | Back off and slow down. If it persists on first load, the IP itself is the problem (see below) |
network-error | The widget cannot reach hCaptcha (offline, blocked, or filtered) | Check connectivity and anything intercepting requests to hcaptcha.com: ad blockers, proxies, CSP |
invalid-data | An hCaptcha endpoint rejected the data the widget sent | Usually bad render parameters; check the sitekey and any rqdata you pass in |
challenge-error | The challenge failed during setup | Transient on hCaptcha’s side more often than yours; reset the widget and retry once |
challenge-closed | The user dismissed the open challenge (async execute() rejection only) | Treat as a cancel, not a failure; re-arm the widget for the next attempt |
challenge-expired | The challenge sat open past its time limit (async execute() rejection only) | Common in automation that opens a challenge and stalls; answer or close promptly |
missing-captcha | You called execute() but no widget exists (async only) | Render before executing; watch for SPA re-renders that unmount the widget container |
invalid-captcha-id | The widget ID passed to the API call does not exist (async only) | Store the ID returned by hcaptcha.render() and reuse exactly that one |
internal-error | The hCaptcha client hit an internal error | Reset and retry; if it repeats across browsers, check the hCaptcha status page |
script-error | The hCaptcha JS SDK itself failed to load | The api.js request was blocked or failed; check CSP, ad blockers, and the script tag URL |
rate-limited and 429s: usually the IP, not the code
rate-limited is the widget error people search most, because the message it
produces (“Rate limited or network error, please retry”) shows up even when
your integration is correct. Under the hood the widget’s requests to hCaptcha are
coming back as HTTP 429.
If it appears after a burst of activity, it is a genuine rate limit: a retry loop
hammering execute(), a component re-rendering the widget on every state
change, or a script solving in a tight loop. Add backoff and the error goes away.
If a fresh session hits
rate-limitedon the first widget load, stop debugging your code. hCaptcha scores the client before any challenge renders, and addresses it distrusts (datacenter ranges, overused residential proxies, VPN exits) get throttled immediately. The fix is a cleaner IP. No client-side change makes a burned address trustworthy again.
The two errors that eat automation pipelines
If you solve hCaptcha programmatically, two failure modes account for most of the
siteverify rejections you will ever see. They come from how tokens
themselves behave, which is covered in depth in
how hCaptcha tokens work.
invalid-or-already-seen-response: tokens are single-use
Every P1_ token is consumed by its first siteverify call.
The second call with the same string fails, every time, by design. That single rule
explains a whole class of confusing bugs:
Retry loops that reuse the token. A submit fails for an unrelated reason (timeout, 500, validation), your code retries the POST, and the captcha field still holds the old token. First attempt consumed it; every retry now fails verification. The retry must include minting a new token, not just resending the form.
Double verification. Some stacks verify in middleware and then again
in the handler, or a frontend “pre-validates” the token before the real
submit. The first check passes and silently kills the token; the second gets
invalid-or-already-seen-response. Verify exactly once, server-side.
Sharing one token across workers. Caching a solved token and handing it to multiple jobs cannot work. One job wins the race; the rest fail. Each submit needs its own token.
expired-input-response: the ~120-second window
A token is valid for roughly 120 seconds from issue. The clock starts when the token is minted, not when you read it, so every second spent between solve and verification counts against the budget. Pipelines that solve early and submit late are the classic case: solve the captcha, scrape three more pages, fill the form, submit, and the token is dead on arrival.
The reliable pattern is to make token acquisition the last step before submit.
Prepare everything else first, mint the token, inject it into
h-captcha-response, and POST within seconds. If a flow legitimately takes
longer than the window, mint the token after the slow part, never before.
Getting a fresh token per submit
Both failure modes reduce to the same requirement: one fresh, unused token per submit
attempt, used immediately. That is exactly the contract of NoneCap’s
solve API. Each POST /v1/solves returns a
newly minted P1_ token for your sitekey and page, never a cached or reused
one, so already-seen rejections cannot happen on the first verification:
# One fresh token per submit attempt:
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
}
Call it at submit time, drop token into h-captcha-response,
and verify once. Solves that fail or expire are auto-refunded, so an error on
hCaptcha’s side never costs a credit. Regular, invisible, and enterprise
(rqdata) sitekeys are all supported; for enterprise specifics see
enterprise rqdata.
Debugging checklist
When an hCaptcha integration fails and the error is not obvious, this order finds it fastest:
1. Log the raw siteverify response. Many wrappers swallow
error-codes and report a bare “verification failed”. The array
names the exact problem.
2. Check the token’s age and history. Log when each token was
minted and how many times you verified it. Anything verified twice or after ~120
seconds explains invalid-or-already-seen-response with no further
digging.
3. Confirm key pairing. sitekey-secret-mismatch and
not-using-dummy-passcode both mean your keys do not belong together.
They typically appear after switching between test and production keys.
4. Watch the network panel for 429s. Client-side weirdness with no
callback error often turns out to be rate-limited in disguise.
Last updated June 2026.
Frequently asked
What does invalid-or-already-seen-response mean?
siteverify was either already verified once or is no longer valid. hCaptcha tokens are strictly single-use: the first siteverify call consumes the token, and every later call with the same string fails. Retry loops that resend the same token, double form submits, and webhooks that re-verify are the usual culprits. The fix is one fresh token per verification, used within about 120 seconds. See how hCaptcha tokens work.Why does the hCaptcha widget say "rate limited" (429)?
rate-limited errors, which come with HTTP 429s from hCaptcha endpoints. It means this client has sent too many requests: rapid solve attempts, a render loop, or simply an IP that hCaptcha already distrusts. Datacenter ranges and overused proxies can hit it on the very first load. Back off, fix any loop, and if a clean session still gets it immediately, change the IP, not the code.How long is an hCaptcha token valid?
siteverify call. Verify late and you get expired-input-response; verify twice and you get already-seen-response (or the combined invalid-or-already-seen-response). Budget your flow so the token is minted, injected, and verified inside that window.