HOW HCAPTCHA ACTUALLY WORKS (FROM NETWORK TRAFFIC)

HOW HCAPTCHA ACTUALLY WORKS (FROM NETWORK TRAFFIC)

@Meshprivacy telegram

I spent a while staring at Chrome DevTools trying to figure out what hCaptcha does under the hood. Turns out it's more than just "click the motorcycles" -- there's proof-of-work, encrypted payloads, mouse tracking, and a whole Enterprise tier most people don't know about.


This is everything I found, pulled straight from HAR captures.


This research is part of our work at meshprivacy.com -- we build privacy-focused tools and do deep dives into how web security systems actually work.



WHAT IS HCAPTCHA?

-----------------


hCaptcha is a bot-detection service. You've probably seen the checkbox or the image grids. But the visible part is only one layer. There are actually four things happening at once:


- Proof-of-work -- your browser has to compute a SHA-256 hash before it can even talk to the API

- Browser fingerprinting -- screen size, navigator properties, rendering capabilities

- Mouse tracking -- up to 256 pointer events with timestamps, analyzed for human-like movement

- Visual challenges -- the image grids and drag-drop tasks, only served when the other signals aren't enough


Most of the time, if you're on a normal browser with normal behavior, you'll never see a challenge. The system just passes you through.



HOW IT LOADS

------------


When a site adds hCaptcha, here's the actual load sequence:


1. Browser fetches api.js from js.hcaptcha.com (~300KB)

2. api.js finds the <div class="h-captcha"> on the page and injects two iframes -- one for the checkbox, one for challenges

3. A Web Worker loads hsw.js (~850KB) and starts computing proof-of-work in the background

4. The widget calls checksiteconfig to ask: should I auto-pass this user or show a challenge?


All of this happens before the user does anything.


Domains you'll see in DevTools:


 js.hcaptcha.com            Hosts api.js, the main SDK

 newassets.hcaptcha.com         Widget HTML, challenge JS, hsw.js

 imgs.hcaptcha.com / imgs3.hcaptcha.com Challenge images

 api.hcaptcha.com            Backend API

 {hex}.w.hcaptcha.com          Logo images (hex is per-account)



THE SOLVE FLOW

--------------


The whole thing boils down to three API calls:


 checksiteconfig --> getcaptcha --> checkcaptcha --> token


Let me walk through each one.



1. CHECKSITECONFIG


First thing the widget does. Asks the server whether this user needs a challenge.


 POST https://api.hcaptcha.com/checksiteconfig

    ?v={build_hash}&host={domain}&sitekey={key}&sc=1&swa=1&spst=0


The params: v is the hCaptcha build hash, host is the site domain, sitekey is the site's UUID. swa=1 means the browser supports Service Workers. spst is the Service Worker persistence state.


The response looks like this:


 {

  "pass": true,

  "c": {

   "type": "hsw",

   "req": "eyJ0eXAiOiJKV1Qi..."

  },

  "features": {

   "enc_get_req": true

  }

 }


Now here's where it gets confusing. pass: true doesn't always mean you got a token:


 pass=true + generated_pass_UUID present --> Done. Token's in the response. No challenge at all.

 pass=true + NO generated_pass_UUID    --> Widget auto-fires getcaptcha without waiting for a checkbox click

 pass=false + NO generated_pass_UUID    --> User has to click the checkbox first


I got tripped up by this for a while. pass: true without a token just means "skip the checkbox, go straight to getcaptcha."



2. GETCAPTCHA


This requests the actual challenge (or sometimes gets an auto-pass token back).


 POST https://api.hcaptcha.com/getcaptcha/{sitekey}


The body format depends on whether it's Free or Enterprise hCaptcha. For Enterprise, it's an encrypted binary blob. For Free, it's regular form params.


The response tells you what challenge type you're getting:


 {

  "success": true,

  "key": "E1_eyJ0eXAi...",

  "request_type": "image_drag_drop",

  "requester_question": {

   "en": "Please place the objects in the correct position"

  },

  "tasklist": [

   {

    "datapoint_uri": "https://imgs3.hcaptcha.com/...",

    "task_key": "c243f02c-8024-4a9d-bf64-d7793ed2ff1d"

   }

  ]

 }


That request_type field is everything. It tells you exactly what kind of challenge was served.



3. CHECKCAPTCHA


After the user solves the challenge, the widget submits the answers:


 POST https://api.hcaptcha.com/checkcaptcha/{sitekey}/{challenge_key}

 Content-Type: application/json


 {

  "v": "{build_hash}",

  "job_mode": "image_drag_drop",

  "answers": { "..." },

  "serverdomain": "example.com",

  "sitekey": "{sitekey}",

  "motionData": "{...}",

  "n": "{hsw_proof_token}",

  "c": "{challenge_config}"

 }


If you pass, you get back a generated_pass_UUID -- that's your hCaptcha token. The site's backend then verifies it server-side via /siteverify.



4. SERVER-SIDE VERIFICATION (you won't see this in DevTools)


 POST https://api.hcaptcha.com/siteverify

    secret={server_secret}&response={token}&sitekey={sitekey}



CHALLENGE TYPES

---------------


The request_type in the getcaptcha response determines what the user sees. There are five known types.



image_label_binary -- the classic 3x3 grid


You know this one. "Click all images containing a motorcycle." Nine tiles, you pick which ones match.


 +-------+-------+-------+

 | img | img | img |

 +-------+-------+-------+

 | img | img | img |

 +-------+-------+-------+

 | img | img | img |

 +-------+-------+-------+


Answer format -- each tile gets "true" or "false":


 { "answers": { "key_1": "true", "key_2": "false", "key_3": "true" } }



image_drag_drop -- drag objects onto a scene


This one showed up in my HAR capture. You get a background scene (JPEG, ~40KB) and 2+ small entity images (PNGs, ~2-3KB each). You drag the entities to where they belong on the scene.


Answer format -- pixel coordinates for each entity:


 {

  "job_mode": "image_drag_drop",

  "answers": {

   "task_uuid": [

    { "entity_name": "uuid_1", "entity_type": "default", "entity_coords": [170, 86] },

    { "entity_name": "uuid_2", "entity_type": "default", "entity_coords": [43, 146] }

   ]

  }

 }


A separate challenge.js (~35KB) gets loaded to handle the drag-drop UI.



image_label_area_select -- click a specific spot


Single image, and you click where the target object is. Answer is just [x, y] coordinates.


 { "answers": { "task_key": [x, y] } }



image_label_multiple_choice -- pick one option


Multiple choices, you select one. Not that common.


 { "answers": { "task_key": "selected_option_key" } }



text_free_entry -- type something


Rare. You type a text answer.


 { "answers": { "task_key": "typed answer" } }



Quick comparison:


 image_label_binary      Pick tiles in a 3x3 grid      Very common

 image_drag_drop       Drag objects to correct positions  Common

 image_label_area_select   Click a point on an image      Common

 image_label_multiple_choice Pick one option           Uncommon

 text_free_entry       Type an answer            Rare



FREE VS ENTERPRISE

------------------


This was the part that confused me most. hCaptcha has two editions, and the network traffic looks completely different depending on which one a site uses.


Here's how to tell them apart:


 features.enc_get_req     Free: Missing or false   Enterprise: true

 getcaptcha Content-Type   Free: form-urlencoded    Enterprise: application/octet-stream

 getcaptcha body       Free: Readable form params Enterprise: Encrypted binary blob

 Token/key prefix       Free: P0_ or P1_      Enterprise: E0_ or E1_

 rqdata            Free: Not supported     Enterprise: Supported


Free hCaptcha sends getcaptcha as a normal form POST. You can read every field:


 v={build_hash}&sitekey={key}&host={domain}&hl=en&motionData={json}&n={hsw_proof}&c={config}


Enterprise hCaptcha encrypts the whole getcaptcha body into a binary payload (application/octet-stream, typically 30-50KB). It's built client-side by api.js using msgpack + encryption. Same data inside -- hsw proof, fingerprint, motionData, rqdata -- but you can't read it from a HAR dump.


The E vs P prefix on tokens is the fastest way to check. If the challenge key starts with E1_, it's Enterprise.



RQDATA

------


This only exists on Enterprise. It's an opaque token that the site's backend generates and passes to the hCaptcha widget through the page config.


The flow:


 Site backend --> generates rqdata --> frontend widget --> embedded in getcaptcha --> hCaptcha reads it


What's inside: custom risk signals from the site's own systems. Login failure counts, purchase history, fraud scores, whatever the site wants to tell hCaptcha about this specific user.


The point is to let sites say "this user just failed 3 login attempts, maybe give them a harder challenge." hCaptcha uses it to adjust difficulty on the fly.



PROOF-OF-WORK (HSW)

--------------------


Every API call -- both getcaptcha and checkcaptcha -- needs a fresh proof-of-work token. You can't skip this or reuse old ones.


The proof is computed by hsw.js (~850KB), which runs in a Web Worker. The checksiteconfig response includes a JWT in c.req that tells the worker what to compute:


 {

  "f": 0,      // feature flags

  "s": 2,      // difficulty (leading zero bits)

  "t": "w",     // type: worker

  "d": "<base64>", // challenge data

  "l": "/c/{hash}", // script location

  "i": "sha256-{sri_hash}", // subresource integrity

  "e": 1773657298, // expiry (unix timestamp, ~1 hour)

  "n": "hsw",    // algorithm name

  "c": 1000     // iteration count

 }


The worker computes SHA-256 hashes until it finds one with enough leading zero bits (set by s). The result goes in the n field of your API request.


With s=2 (which is what I saw), it's fast -- barely noticeable. But hCaptcha can crank it up for suspicious traffic, and at scale the compute cost adds up.



MOTIONDATA AND ANTI-BOT SIGNALS

--------------------------------


This is where hCaptcha gets serious about detecting bots. The motionData field in checkcaptcha is a JSON string packed with behavioral telemetry.


Mouse/pointer events:


 pm    Pointer move events -- up to 256 of them, each [x, y, timestamp]

 pm-mp   Mean period between pointer moves (mine was ~16.5ms)

 mm/mm-mp Mouse move events and mean period

 md/md-mp Mouse down events (clicks/drags start)

 mu/mu-mp Mouse up events (clicks/drags end)

 st    Session start timestamp

 tc    Touch data -- empty object on desktop, populated on mobile


Browser fingerprint (topLevel object):


 sc  Screen info: availWidth, availHeight, colorDepth, pixelDepth

 wi  Window inner dimensions [width, height]

 nv  Navigator properties -- user agent, plugins, maxTouchPoints, etc.

 pel  The actual HTML of the parent element that embeds hCaptcha

 dr  document.referrer

 or  Orientation (portrait/landscape)

 inv  Whether invisible mode is active


What they're checking for:


Bots move the mouse in straight lines with perfectly uniform timing. Humans don't. My capture had 256 pointer events with a mean period of 16.5ms and plenty of jitter in the coordinates. Three mouse-downs and three mouse-ups -- matching the checkbox click plus two drag operations.


If you're using a headless browser, the screen dimensions might be weird (800x600 or 0x0), the navigator object will have automation-related properties, and the referrer might be empty. All dead giveaways.



TOKEN LIFECYCLE

---------------


1. Widget gets a token (prefixed P0_, P1_, E0_, or E1_)

2. Token goes into two hidden inputs: h-captcha-response and g-recaptcha-response (the second one is for reCAPTCHA compatibility)

3. Site form submits the token to its backend

4. Backend calls api.hcaptcha.com/siteverify to check it

5. Token expires after ~120 seconds

6. Each token works exactly once -- can't replay it



API REFERENCE

-------------


Base URL: https://api.hcaptcha.com


 /checksiteconfig       POST  Decides auto-pass vs challenge

 /getcaptcha/{sitekey}    POST  Gets the challenge (or auto-pass token)

 /checkcaptcha/{sitekey}/{key} POST Submits answers, returns pass token

 /siteverify         POST  Server-side token verification


All widget requests use these headers:


 Origin: https://newassets.hcaptcha.com

 Referer: https://newassets.hcaptcha.com/


Content-Type varies: application/json for checkcaptcha, application/x-www-form-urlencoded (Free) or application/octet-stream (Enterprise) for getcaptcha.


Asset URLs:


 SDK      https://js.hcaptcha.com/1/api.js

 Widget    https://newassets.hcaptcha.com/captcha/v1/{build}/static/hcaptcha.html

 HSW      https://newassets.hcaptcha.com/c/{hash}/hsw.js

 Challenge JS https://newassets.hcaptcha.com/captcha/v1/{build}/challenge/{type}/challenge.js

 Images    https://imgs3.hcaptcha.com/tip/{category_hash}/{image_hash}.{ext}

 Logos     https://{hex}.w.hcaptcha.com/logo.png



TOKEN PREFIXES

--------------


Fastest way to identify what you're dealing with:


 P0_  Free hCaptcha, auto-passed (no challenge)

 P1_  Free hCaptcha, challenge was solved

 E0_  Enterprise hCaptcha, auto-passed

 E1_  Enterprise hCaptcha, challenge was solved


First letter: P = Free, E = Enterprise. Number: 0 = no challenge needed, 1 = challenge was served.



FAQ

---


Q: How do I tell if a site uses Free or Enterprise hCaptcha?

A: Look at the checksiteconfig response. If features.enc_get_req is true, it's Enterprise. Or just check the token prefix -- P for Free, E for Enterprise.


Q: What does pass:true actually mean?

A: It depends. If the response also has generated_pass_UUID, you're fully auto-passed with a token. If there's no token, it just means the widget will skip the checkbox and go straight to getcaptcha. I found this out the hard way.


Q: What's rqdata?

A: Enterprise-only. An opaque token from the site's backend that carries custom risk signals. It gets embedded in the getcaptcha request so hCaptcha can adjust difficulty based on what the site knows about this user.


Q: What's hsw.js doing?

A: Computing a SHA-256 proof-of-work in a Web Worker. Every API call needs a fresh proof -- you can't reuse them. The difficulty is set by the s field in the JWT from checksiteconfig.


Q: What challenge types exist?

A: Five that I've seen: image_label_binary (3x3 grid), image_drag_drop (drag entities onto a scene), image_label_area_select (click a point), image_label_multiple_choice, and text_free_entry.


Q: What do P0, P1, E0, E1 mean?

A: P = Free, E = Enterprise. 0 = auto-passed without a challenge. 1 = a challenge was served and solved.


Q: What's enc_get_req?

A: A flag in the checksiteconfig response. When it's true, the getcaptcha body will be an encrypted binary blob instead of readable form params. This means Enterprise hCaptcha.


Q: What's in motionData?

A: Mouse movements, click timestamps, screen dimensions, navigator properties, referrer URL, and more. It's a JSON string sent with checkcaptcha so hCaptcha can check if the interaction looks human.



DISCLAIMER

----------


Everything here comes from watching client-side browser traffic in Chrome DevTools. No server-side code was decompiled or accessed. This is for educational use -- please respect hCaptcha's Terms of Service.


For more research like this, check out meshprivacy.com


Report Page