Arkose Labs' token flow: from session to verdict, and how difficulty scales
A user clicks submit. Somewhere between that click and your server deciding whether to honour the request, a short opaque string changes hands. It rides along in a hidden form field or an API body, your backend forwards it to a host you do not control, and a JSON object comes back telling you whether to trust the person on the other end. That string is an Arkose Labs session token, and the round trip it triggers is the whole product. The challenge you see, the spinning puzzle box, the audio fallback, the cases where nothing appears at all, are all downstream of one decision the platform already made about how hard to make this particular session work.
This post follows that token. Not the game art and not the solver economy, which the FunCaptcha internals piece already covers, but the plumbing: how the token gets minted on the client, what the Verify API hands back when your server presents it, and the part most write-ups skip, which is how Arkose decides a given session deserves transparent mode, a light proof-of-work, or a stack of visual waves. The difficulty is not a property of the challenge. It is a property of the verdict the platform reached before the challenge existed.
The rest walks the flow in order. First the client side: the script load, the configuration object, the telemetry blob, and the one-time token that falls out of onCompleted. Then the Verify API: the request shape, the solved field everyone keys off, and the much larger object hanging behind it. Then the risk model that drives difficulty, the two scores and the telltales that move them. Then proof of work and how its difficulty ladder maps onto risk. A closing section on what changed in 2026 and what a token actually proves.
The client side: from script tag to one-time token
Everything starts with one script. The integration loads Arkose’s client API from a per-customer host, keyed to the public key, with a global callback named in the tag:
<script src="//<company>-api.arkoselabs.com/v2/<PUBLIC_KEY>/api.js" data-callback="setupEnforcement"></script>The data-callback attribute names a function on window that the API invokes once it has loaded and initialised. Arkose is firm that the script load exactly once per page; a second load wires up duplicate event listeners and corrupts the session. When the callback fires it receives the enforcement object, and the integration calls setConfig on it. That single call is where the customer declares everything about how the challenge behaves.
The configuration object is small but load-bearing. selector points at the DOM element that either triggers a modal or hosts an inline challenge. mode chooses between lightbox and inline. A cluster of callbacks covers the session lifecycle: onReady when enforcement is armed and can be triggered, onShown the first time a visible challenge appears, onSuppress while the platform is still analysing intent and may decide no challenge is needed, onCompleted when the session resolves successfully, onFailed when the user exhausts their attempts, and onError when loading itself breaks. The one your backend cares about is onCompleted, because its response object carries response.token, described in Arkose’s own docs as a one-time-use token.
Underneath those callbacks the API has already done its real work. Before any visible challenge, the client collects a browser fingerprint and ships it to Arkose to create the session and run the detection engine. The collected payload travels as a parameter conventionally called bda, which independent researchers gloss as browser data and which carries the device and behavioural signals Arkose scores. The exact field layout of that payload is not public, and Arkose deliberately rotates and obfuscates the collection script, so what follows is inferred from public reverse-engineering work and from the fields Arkose later echoes in its Verify response rather than from any official schema.
What the response fields tell us is the shape of what gets collected, because the Verify API can hand back the parsed values. The browser characteristics it reports include browser_name, browser_version, color_depth, canvas_fingerprint, plus whether the client supports session_storage and indexed_database. The device characteristics include operating_system, screen_resolution, max_resolution_supported, cpu_class, platform, hardware_concurrency, and touch_support. There is a timezone_offset from the client and a ja4_hash, which Arkose documents as the TLS client fingerprint used to identify the application or browser. Anyone who has read the TLS fingerprinting piece will recognise that last one: the same ClientHello-derived hash that Akamai and DataDome key off shows up here as one input among many. The fingerprint is a feature, not the verdict.
The collection script is the part of the system researchers spend the most effort on, precisely because it is opaque. Public tooling exists to view and repackage these fingerprint blobs, and the recurring observation from that work is that the payload breaks on every script update, which is the point. Arkose treats the collection client as a moving target. The defensive reading is straightforward: a static, replayed bda blob is exactly the kind of thing the platform is built to notice, because a real session regenerates it against a script that may have changed since the last time anyone looked.
Public reverse-engineering also points at the behavioural half of the payload, the part that does not show up as a clean field in the Verify response. The recurring claim from solver researchers is that the blob carries timing and motion data, cursor paths and interaction cadence, the micro-variation a real hand produces and a scripted mouse move does not. That is consistent with how the rest of the industry collects signal and with Arkose’s own emphasis on detecting fraud farms, where the operator is a real human and the only thing separating them from a legitimate user is context and rhythm. None of this is documented at the field level, so the responsible thing is to treat the behavioural payload as real but unspecified: it exists, the platform scores it, and the exact encoding is something Arkose changes on purpose. A defender does not need the byte layout to reason about it. The relevant fact is that the signal regenerates per session against a script that rotates, which is why captured payloads age out rather than holding.
The session-creation request itself is the first thing that happens, before any of the callbacks fire. Researchers consistently describe an initial POST to a /fc/gt2/public_key/<public_key> path carrying the bda parameter, which mints the session and returns the token plus the configuration for whatever comes next. If a challenge is warranted, a follow-up to a game-fetch endpoint returns the challenge data, including a per-session script URL that expires. The exact paths are an implementation detail Arkose can and does change, so they are useful for understanding the shape of the flow rather than as a stable contract. The shape is the durable part: one request creates the session and gets the token, and everything after it, including whether a challenge appears at all, is decided server-side from what that first request carried.
The data exchange blob: server-provided signal
There is a second, separate piece of data the client can carry, and it is easy to confuse with the fingerprint payload. The configuration object accepts a data field, described in Arkose’s docs as encrypted arbitrary JSON sent to Arkose at the very start of a session’s classification. This is the Data Exchange feature, and it flows the other way: your server encrypts a small object of facts it already knows about the request, hands it to the page, and the client forwards it into the session so Arkose can score it alongside the browser signals.
Arkose encrypts this blob with AES-256-GCM, using a Data Exchange key that is distinct from the public and private API keys. GitLab’s public integration is a useful real-world reference here. Their signup flow stores three separate credentials, arkose_labs_public_api_key, arkose_labs_private_api_key, and arkose_labs_data_exchange_key, behind feature flags named arkose_labs_signup_challenge and arkose_labs_signup_data_exchange. The exact fields a customer puts in the blob are up to them; common choices are the request IP, the user agent the server saw, and a timestamp, so Arkose can cross-check the server’s view of the client against the client’s own claims. The specific field names GitLab chose are not enumerated in their public merge request, so treat the contents as customer-defined rather than fixed.
The Verify response confirms whether the loop closed. A data_exchange object carries blob_received and blob_decrypted, and customers monitor for both being true in their logs. That pair is the tell that the server-side signal arrived intact and Arkose could open it. If the blob is received but not decrypted, the key is wrong or the payload was tampered with in transit, and the cross-check that the feature exists to perform did not happen. The value of Data Exchange is that it gives the scoring engine a fact the browser cannot lie about: the IP and headers as the origin server actually observed them.
The Verify API: presenting the token
Now the token is on your server. The contract from here is deliberately simple at the surface and rich underneath. Your backend POSTs JSON to the Verify endpoint, https://verify-api.arkoselabs.com/api/v4/verify/ during development or a per-customer https://<company>-verify.arkoselabs.com/api/v4/verify/ in production, with two required fields:
{ "private_key": "_PRIVATE_KEY_", "session_token": "_SESSION_TOKEN_"}The private_key is the secret half of the key pair whose public half loaded the client script. The session_token is the value from response.token. Two optional fields ride along: log_data, a freeform string for your own correlation, and email_address, which only does anything if you have the Email Intelligence add-on. The verify call is mandatory for every session, whether or not the user ever saw a challenge, because a session that was waved through in transparent mode still has a token and still needs to be confirmed server-side. Skipping the verify step because no challenge appeared is the classic integration bug; it means a client can fabricate a token-shaped string and your backend never checks it.
Two properties of the token matter for anyone reasoning about the security boundary. It is one-time use: each token can be presented to the Verify API exactly once, and a second presentation comes back flagged as already verified. And it is server-validated only. The browser never learns whether it passed; the verdict lives entirely in the response your backend reads. That split is deliberate. It means a solved-looking token in the page is worth nothing until your server redeems it, and once redeemed it cannot be replayed. Compare this with cookie-based systems like Akamai’s _abck or Imperva’s reese84, where the proof of trust is a long-lived artefact the client carries; Arkose’s token is closer to a single-use receipt than a session pass.
What the verdict actually contains
The field nearly every integration reads is solved. Arkose is careful about what it means: when a session’s risk level does not qualify it for transparent mode and an interactive challenge is shown, solved reports whether that challenge was answered correctly. A naive integration treats solved: true as the green light and stops there. That works, but it throws away most of what the call returns, and the rest is the actual product.
Start with the session bookkeeping. session is the unique identifier for the whole lifecycle, something like 3595d2c014d3c5f01.1116018803. Timestamps cover the arc: session_created, check_answer, and verified. attempted says whether the user tried to solve. session_timed_out flags a session that ran past the default thirty-minute window without resolving. previously_verified catches the replay case where a token is presented twice. And session_is_legit is Arkose’s own one-line summary, true when it certifies there are no telltales of non-legitimate activity in the session.
Then comes the part that explains the difficulty you saw. suppressed is, in Arkose’s words, the old name for transparent mode, and it shows whether the user was offered the no-challenge path. challenge_type enumerates what actually ran: audio, transparent, visual, pow, pow+visual, or pow+audio. security_level is a numeric difficulty indicator, with an example value of 20, and the docs note it can be null, usually for audio sessions. There is a small armoury of anti-abuse fields too. punishable_actioned records when Arkose randomly fails an otherwise-correct answer as a mitigation tactic against solver farms, which is worth sitting with: under attack, a correct solve is not guaranteed to pass, by design. game_number_limit_reached flags sessions that hit a configured cap on solve attempts.
The telltale fields are where the scoring shows its hand. telltale_list is the set of candidate detection indicators that fired on the session, telltale_user is the winning one, and telltale_origin names the configuration it came from. These connect directly to the risk scores, which are the next section, because a telltale is the atomic unit of how Arkose decides a session is suspicious.
Surrounding all of that is a wide band of enrichment. The device block reports the parsed fingerprint values already listed. An IP intelligence block carries user_ip, country, region, city, isp, asn, and a set of booleans Arkose derives about the connection: is_tor, is_vpn, is_proxy, plus a proxy_type enum spanning values from data center to consumer-privacy to not a proxy, and a connection_type covering Mobile, WiFi, Wired, and others. There is even a network_info_rtt, an estimated round-trip time for the connection. None of these are the verdict on their own. They are the evidence the verdict was built from, exposed so a customer’s own systems can reason about it downstream.
Two scores and a band: the risk model
The difficulty of an Arkose session is a function of risk, and risk resolves to numbers. Arkose Bot Manager computes two scores per session, both on a 0-to-100 scale where 100 is the highest risk. The global score is built from telltales that fire across Arkose’s entire customer base, the shared intelligence. The custom score is built from telltales specific to the individual customer’s configuration. Each appears in the Verify response as an object with a score and a telltales array, and each telltale in that array has a name and a weight.
The naming convention is precise enough to be useful. Global telltale names start with g-, for example g-reputation-recent-abuse-proxy or g-h-cfp-1000000000; custom telltales do not carry the prefix. The weight is a string from "1" to "100". And Arkose’s docs include a quiet but important caveat: a telltale’s weight is not necessarily the same as the eventual total score. The scoring is not a plain sum of weights. There is additional logic, undocumented in its specifics, that turns a set of fired telltales into a final number. The honest statement is that the inputs are visible and the aggregation function is not.
The greater of the two scores decides the band. Arkose documents the cutoffs directly:
*Risk bands from the Arkose risk-score docs: Low 0-40, Medium 41-80, High 81-100. Difficulty climbs with the band, not in lockstep with the raw number.*Alongside the band sits a category, and the category is priority-ordered rather than additive. ALLOWLIST and DENYLIST come first, set by explicit allow or deny telltales. Then FRD-FRM for fraud farms, the captcha-solving operations that use real humans. Then BOT-ADV for advanced automation, BOT-STD for standard botnets, CUSTOM for sessions tripping only customer telltales, and NO-THREAT when nothing fires. A higher-priority category overrides a lower one, so a session that looks like a fraud farm is labelled FRD-FRM regardless of what else fired. When both scores are zero, risk_category is omitted and the band defaults to Low.
This is the layer that decides difficulty. A Low-band session with no telltales is a strong candidate for transparent mode: it gets suppressed: true and never sees a puzzle. As the band climbs, the platform escalates, from transparent, to a proof-of-work step, to a visual challenge, to combinations like pow+visual. The challenge_type field in the verdict is the record of where on that ladder the session landed. The reason the same site shows one person nothing and another person three spinning puzzles is not randomness. It is two different risk scores resolving to two different points on this ladder.
The transparency around these telltales is itself a product feature now. Arkose markets sharing of 175-plus telltale rules, full risk signals, and decision logic in real time, so customers can see what it sees and feed the labels into their own systems. That is a meaningful shift from the older posture where the verdict was a black box. The weights and the aggregation remain proprietary, but the fired telltales and their categories are exposed, which is why a careful integration reads telltale_list and the score objects rather than just solved.
Proof of work and the difficulty ladder
The newest rung on the ladder is proof of work, and it changes what difficulty even means. Instead of asking a human to rotate a picture, the client is asked to burn CPU on a cryptographic puzzle before the session can resolve. The cost is invisible to a legitimate user, a fraction of a second of computation, but it compounds brutally for an attacker running thousands of parallel sessions. Arkose frames this as cost asymmetry: the defender pays nothing noticeable, the attacker pays per session, and at scale the attacker’s bill becomes the deterrent.
The Verify response exposes the proof-of-work outcome in its own object, with challenged, attempted, and passed booleans, a transparent flag, and a difficulty_level that takes values like high or null. That difficulty_level is the key to how scaling works. Arkose’s proof-of-work engine has four tiers, Low, Medium, High, and Extreme, and it does not pick one globally. It scales difficulty per device, what Arkose calls device-adaptive difficulty. A high-end desktop is handed a harder puzzle because it can absorb the cost without the user noticing; a legacy mobile device gets an easier one so the experience stays usable. The difficulty is tuned to the hardware’s capacity to pay, then layered on top of the risk-driven decision about whether to challenge at all.
Proof of work also combines with the older challenge types rather than replacing them. The challenge_type enum already showed this: pow+visual and pow+audio are real states, where the session pays a computational cost and then solves a visual or audio puzzle. By 2026 Arkose describes its challenge stack as a sixth-generation system where proof of work and visual challenges work together against bots, AI systems, and human fraud farms at once. The reasoning is that any single defence has a counter, a solver farm beats a visual puzzle, a headless cluster beats nothing at all, but a session that demands both human-plausible interaction and a real CPU bill is expensive to defeat on every axis simultaneously.
There is a subtlety in how proof of work changes the economics of solving. A visual puzzle has a fixed human cost: a solver farm can answer it for a fraction of a cent because a person does it in seconds. Proof of work attacks the other side of the ledger. It does not care whether a human or a script is driving the session; it charges the machine for the privilege of resolving, and that charge does not get cheaper with a bigger labour pool. A fraud farm with a thousand workers still needs a thousand machines burning CPU, and at Extreme difficulty on capable hardware that bill is the deterrent. The two mechanisms are complementary because they tax different inputs. One taxes human attention, the other taxes compute, and an attacker has to pay both at once when the challenge_type reads pow+visual.
For a defender reading the verdict, the proof-of-work object is a useful honesty check on the rest. A session marked solved: true that also shows a passed Extreme-difficulty proof of work and a clean telltale list is a very different thing from a solved: true with a suppressed challenge and a high custom.score. Both pass the naive gate. Only one of them earned it. The transparent flag on the proof-of-work object is the tell for the quiet case, where the platform ran a computational check the user never perceived, so a session can carry a real proof-of-work result without ever having shown anything on screen.
IP velocity and the aggregation layer
One more block in the verdict deserves attention because it operates on a different axis than everything else. Most of the response describes a single session in isolation. The aggregations object describes the session in the context of others from the same source. It reports IP velocity over two windows: a short-term window with interval_minutes of 60 and a long-term window of 1440 minutes, a full day. Each carries a count of sessions seen and a threshold to compare against, with example thresholds of 360 for the hour and 100 for the day.
This is rate limiting expressed as risk signal rather than a hard block. A single IP that opens hundreds of sessions in an hour is not necessarily malicious, but it is anomalous, and the aggregation layer surfaces that anomaly into the same verdict object as the per-session signals. It connects to the IP intelligence fields naturally: a high session count from a data center proxy type with is_vpn: true reads very differently from the same count behind a residential connection_type of Mobile. The platform does not have to decide between these in the abstract. It exposes both and lets the scoring, and the customer, weigh them.
The deeper point is that Arkose’s verdict is not purely a property of one browser at one instant. The same telltale that means little on its own becomes meaningful when an IP has tripped it forty times in an hour. This is the collective-intelligence pattern that shows up across the industry, in HUMAN’s signal network and elsewhere: the signal that matters is often the one that only exists across sessions, not within one.
What 2026 changed, and what a token proves
Arkose spent the last two years moving the centre of gravity of its product. The visible puzzle, the thing the name FunCaptcha is attached to, became one option among several rather than the main event. Proof of work expanded from a narrow deployment against high-risk traffic to a wide one across risk levels, with the device-adaptive difficulty that lets it run quietly on legitimate users. And in January 2026 the company announced Arkose Titan, a repackaging that folds bot detection, device intelligence, email intelligence, scraping defence, and behavioural biometrics behind a single API call, with AI-agent detection as the headline. The stated problem Titan answers is the one CISOs kept asking: telling an authorised AI agent apart from a malicious one. The token flow described here is the substrate that question gets answered on top of.
What did not change is the shape of the contract. A session still resolves to a one-time token. Your server still presents it to a Verify API and reads a verdict. The solved field still answers the narrow question of whether a shown challenge was answered, and the larger object still carries the risk scores, the telltales, the device and IP enrichment, and now the proof-of-work and aggregation blocks that explain how hard the session was made to work and why. The difficulty was never in the puzzle. It was in the decision the platform reached before the puzzle, encoded in two scores and a band, and exposed, more openly than it used to be, in the JSON that comes back.
So the practical lesson for anyone integrating against this, or reasoning about it defensively, is to stop treating the token as a boolean. A token that verifies as solved with a suppressed challenge, a high custom score, a data-center proxy, and an IP that has opened three hundred sessions this hour is technically valid and substantively worthless. The platform already told you that, in the fields next to solved. The whole point of the verdict object growing this large is that the answer to whether you should trust a session was never one bit wide.
Sources & further reading
- Arkose Labs (2024), Verify API v4 — overview of the server-side verification step and its 1/0 or full-JSON response modes.
- Arkose Labs (2024), Calling Verify API — exact POST endpoint,
private_key/session_tokenrequest fields, and optionallog_dataandemail_address. - Arkose Labs (2024), Verify API Response Fields and Examples — the full verdict object:
solved,suppressed,challenge_type, device and IP blocks,aggregations, and theproof_of_workobject. - Arkose Labs (2024), Risk Score — global vs custom scores, the 0-100 scale, risk bands, category priority order, and the
g-telltale naming convention. - Arkose Labs (2024), Client-Side Instructions / Standard Setup — the
api.jsscript tag,data-callback, and theonCompletedcallback returningresponse.token. - Arkose Labs (2024), Configuration Object —
setConfigfields includingdata(Data Exchange),mode,selector, and the lifecycle callbacks. - Arkose Labs (2025), Proof of Work: Invisible Security, Visible Results — the Low/Medium/High/Extreme tiers, device-adaptive difficulty, and the cost-asymmetry rationale.
- Help Net Security (2026), Arkose Titan aims to make bot, scraping, and AI fraud economically unviable — the January 2026 unified-platform announcement, sixth-gen challenges, and AI-agent detection.
- GitLab (2023), Send Arkose Data Exchange payload on signup (MR 139070) — a real integration showing the three key types and the
blob_received/blob_decryptedlog checks. - unfuncaptcha (2024), bda — open-source tooling that documents, by example, the fragility and rotation of the client fingerprint payload.
Further reading
Cloudflare's managed challenge vs JS challenge vs interactive challenge
Traces Cloudflare's challenge taxonomy: the JS (non-interactive) challenge, the managed challenge, the deprecated interactive challenge, and the retired CAPTCHA, when each fires, what each measures, and how the clearance levels differ.
·21 min readArkose Labs FunCaptcha internals: the game challenge and risk-based enforcement
How Arkose Labs' FunCaptcha works: why it ships interactive games instead of text, the encrypted bda fingerprint that decides difficulty, the gt2/gfct/verify token flow, and the economic model behind the challenge design.
·20 min readreCAPTCHA v3 scoring: how the 0.0 to 1.0 score is computed and what feeds it
How reCAPTCHA v3 turns a page visit into a 0.0 to 1.0 risk score: the grecaptcha.execute flow, the action tags, the signals Google admits to, the reason codes, and why the score is really a reputation lookup.
·18 min read