parsa
Get started

How Akamai Bot Manager works: sensor data, _abck and sbsd explained

How Akamai Bot Manager decides: the v3 sensor, sbsd and pixel scripts, the _abck and bm_sz cookies, what sensor_data holds, and how to debug a rejected session.

Updated 11 min read

In short

  • Akamai Bot Manager decides from a script on the page, not from one request: the script reports the device and the visit as sensor_data, and the verdict lives in the _abck cookie.
  • An unsolved _abck carries ~-1~; once a sensor post is accepted it gains a ~0~ segment and the protected requests go through.
  • Three scripts: the sensor (v3) at a script URL with no query string, sbsd at a URL with ?v= and the bm_so cookie, and the rarer pixel under /akam/13/.
  • The payload is necessary, not sufficient: the edge also checks the TLS and HTTP/2 fingerprint, header order and the IP against the user agent.

Akamai Bot Manager is the bot defence on Akamai’s edge. It judges a run, not a request: a page load, a script fetch, a few posts from that script describing the device and the visit, and the requests that follow. The verdict lives in cookies, mostly _abck, which the edge checks on the endpoints the site chose to protect.

A plain HTTP client never runs the script, so never earns a valid cookie. One that does post can still fail, because the edge also reads the TLS handshake, HTTP/2 settings, header order and IP.

This guide covers Akamai’s three scripts, its cookies, the request flows, what the scripts collect, what the edge checks outside the payload, and how to debug a session that will not validate.

How to tell a site uses Akamai

Akamai ships three client scripts, and one page can name more than one. Tell them apart by the shape of the <script src>:

On the page Script What fetching it does
A same-origin path of two or more random-looking segments, no query string Sensor v3 Sets _abck with ~-1~
The same kind of path with ?v=<uuid>, sometimes &t=<id> sbsd Never sets _abck; its cookie is bm_so or sbsd_o
A path under /akam/13/, with a bazadebezolkohpepadr variable in an inline script Pixel —

Sensor v3 is the one most sites run. Its path is randomised per site and changes over time, so match on the shape. If fetching the script sets _abck with ~-1~, you have found v3.

sbsd (“state based scraping detection”) is a separate protocol, run beside v3 or alone. The query tells you which situation you are in:

  • ?v=<uuid> alone: the page is watched. You have the real page. The script posts a signal block on load, then telemetry on a timer.
  • ?v=<uuid>&t=<id>: the page is held. You have an interstitial, about 2.7 KB in our captures, with the real body replaced by a hidden container. One accepted post releases it.

Pixel is the rarest. The page writes a number into bazadebezolkohpepadr and loads /akam/13/<hex>. A browser makes one form post from it, with no chain.

Some deployments add an HTTP 428 challenge from /_sec/cp_challenge/ (often written sec-cpt), a countdown or a proof of work, tracked by the sec_cpt cookie. While it is open, sensor posts get 201, not 200.

The cookies

The edge reissues these as you go, so read each from the jar right before use.

Cookie Set by What it means Lifetime
_abck The page response; reset to ~-1~ by the v3 script fetch; updated by every sensor post The session’s state at the edge. ~-1~ means not validated; an accepted run gives it a ~0~ segment —
bm_sz The page response Seeds the v3 payload’s cipher. A payload built against a stale value will not open —
bm_so (sbsd_o on some sites) The edge, reissued during the sbsd flow The sbsd post’s integrity value is computed over its current value —
ak_bmsc The edge; reissued in answer to the pixel post Session state the pixel post refreshes —
sec_cpt The edge, with a 428 challenge Its second field is the challenge state: 1 open, 2 answered, 3 resolved —

The ~-1~ you see after the script fetch is expected; it does not mean you are blocked. For sec_cpt, a 2 only counts if the session saw a 1 first.

The flow, step by step

Every request below shares one TLS fingerprint, header order, proxy IP and cookie jar.

v3: the sensor_data loop

Fetch the page, then the script, once per session:

GET / HTTP/2
Host: shop.example.com
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
accept-language: en-US,en;q=0.9

HTTP/2 200
set-cookie: _abck=<value>; …
set-cookie: bm_sz=<value>; …
GET /aBc/dEf/gHi HTTP/2
Host: shop.example.com
cookie: _abck=<value>; bm_sz=<value>

Post sensor_data to the same URL the script came from. The body is a JSON object, but the content type is plain text, because that is what the script’s XHR sends:

POST /aBc/dEf/gHi HTTP/2
Host: shop.example.com
content-type: text/plain;charset=UTF-8
cookie: _abck=<value>; bm_sz=<value>

{"sensor_data":"<opaque string>"}

Then read _abck from the jar. If it carries ~0~, make the real request. If not, build the next payload against the current cookies and post again. The edge scores the run in order, so each post continues the last: same device, a later clock, more pointer movement. If a handful of posts has not validated the session, another will not; go to the checklist.

sbsd: a held page

The page is an interstitial naming /aBc/dEf?v=<uuid>&t=<id>. Fetch that script, then post to the script URL without its v, keeping the t. The body is JSON with one body key:

POST /aBc/dEf?t=<id> HTTP/2
Host: shop.example.com
cookie: bm_so=<value>; …

{"body":"<encoded payload>"}

Then fetch the page again; if accepted, you get the real document.

sbsd: a watched page

No t. The script posts its signal block on load, then two telemetry posts on its own timer: the first about two seconds after the collector starts, the second about ten seconds later. Then it stops. All three go to the script path without its query:

POST /aBc/dEf HTTP/2
Host: shop.example.com
cookie: bm_so=<value>; …

{"body":"<signal block, encoded>"}

Each telemetry post states its timer tick, so a post sent early claims a moment that has not happened yet.

The sbsd post returns 200 whatever you send. In our live runs a junk body got a 200 too. The next page told them apart: after our posts it came back 200 and about 373 KB, four sessions out of four; after junk, 403 and under 4 KB, four out of four. Judge sbsd by the next page, not the post.

Pixel

Fetch /akam/13/<hex>, then post a form to /akam/13/pixel_<suffix> on the page’s origin, with the page as referer, about half a second after load, as a browser does. The post states the wait.

POST /akam/13/pixel_<suffix> HTTP/2
Host: shop.example.com
content-type: application/x-www-form-urlencoded
referer: https://shop.example.com/tickets/

<form fields>

HTTP/2 200
set-cookie: ak_bmsc=<value>; …

The edge answers an empty 200 and reissues ak_bmsc. The post’s two inputs move: the bazadebezolkohpepadr number every few days, and a 32-character hex value from the script’s string array with the script.

What the scripts collect

The v3 sensor

In our field-by-field reading of a current build, one sensor_data payload carries 187 fields, 64 at the top level and the rest in five nested blocks. About a third describe the machine. By category:

  • Machine: graphics adapter (WebGL vendor and renderer), CPU cores, device memory, screen size, window size, display scaling, installed speech voices.
  • Agent and locale: user agent, platform, and navigator.languages, which must agree with your accept-language header.
  • Clock: time zone, offset and a rendered date, with the zone named as Chrome’s ICU data names it (Asia/Calcutta, not Asia/Kolkata).
  • Page and session: the page URL, a segment of the script’s own path (the edge checks it, so a payload for one script path cannot serve another), the position in the run, and values tied to the session’s cookies.
  • Behaviour: pointer movement, key and touch events, scroll and timings, accumulated across the run.
  • Derived values: sums and checks over other fields. Our reading checks 24 such agreements; break one and the payload is inconsistent even if every value is plausible.
  • Build values: a version marker and the field order the build expects.

How it is encoded. sensor_data is a short semicolon-separated header (it starts 3;0;1;0;, the version first) followed by the ciphered content. The content’s colon-separated elements are reordered by a small pseudo-random generator seeded from a hash of the script build, then its characters are substituted by the same generator, seeded from a hash read out of bm_sz. Before the edge has set bm_sz, on a session’s very first post, the script uses a fixed default instead. That binds a payload to one build and one session: built against another build, another session’s bm_sz or an older value of your own, it will not decrypt, and the response will not say why. The script’s strings sit behind per-build keys, so field order and keys come from each build.

sbsd

The sbsd signal block holds 120 signals, overlapping v3’s machine and locale data and going deeper:

  • WebGL limits, a canvas digest, system colours;
  • an audio analyser node’s properties, and font metrics from measureText;
  • the properties on navigator, as a hash and a count (Chrome exposes 199);
  • network connection information;
  • a second reading of the agent, platform and renderer from inside a SharedWorker, so the two readings can be compared;
  • digests of the URLs the page’s own tags name;
  • the zone Intl reports and a rendered new Date().toString().

Telemetry posts add the mouse, keys, touch and scroll seen, plus an integrity value the script’s bytecode computes over them, the timestamp and the current bm_so.

How it is encoded. The block is serialised to JSON, its comma-separated parts shuffled, its characters substituted, and the result wrapped as {"body": "…"}. Both stages are keyed from the script’s v UUID and a per-build seed. The seed, the signal names and the integrity logic rotate with the build, strings are decoded through call-path-dependent getters, and the integrity logic runs in a small virtual machine.

Pixel

The pixel post has 24 fields: a few hashes, a hash of the plugin list, values each Chrome build states, and a timing measurement over several rounds of a busy loop. Everything follows from the two inputs and the browser you claim.

Builds and rotation

The v3 and sbsd scripts rotate every few hours: the page names a new path, or yesterday’s payloads stop validating. On one site we saw two sbsd builds in a day that differed only in version, signal-name prefix and shuffle seed. Treat anything learned from a build as that build’s only.

What the edge checks beyond the payload

A correct payload in a request that does not look like a browser fails, and looks like a rejected payload.

TLS fingerprint. Your ClientHello lists cipher suites, extensions in order, supported groups, signature algorithms, ALPN protocols and TLS versions. That is stable enough per library to identify it, as a JA3 or JA4 hash. Go’s net/http, Python’s requests and plain curl are each recognisably not a browser. Use a client with an impersonation target (utls or tls-client in Go, curl_cffi in Python, curl-impersonate), matched to the user agent you claim.

HTTP/2 and header order. Chrome sends pseudo-headers as :method :authority :scheme :path; some libraries send :method :path :authority :scheme, and that alone is visible, as are SETTINGS and HPACK behaviour. After them, a Chrome document request has a stable order: the sec-ch-ua headers, upgrade-insecure-requests, user-agent, accept, the sec-fetch-* headers, accept-encoding, accept-language, cookie. The sensor post is an XHR, so content-type appears, sec-fetch-mode is cors, sec-fetch-dest is empty, and upgrade-insecure-requests and sec-fetch-user drop out. Treat this as a shape: capture the Chrome version you claim through an order-preserving proxy (devtools normalise headers for display). Extra headers give you away too.

IP and time zone. Your IP is not in the payload, since JavaScript cannot read it. But the edge scores it, and the payload’s clock and locale should fit it. German and Berlin’s clock from a Virginia datacentre is consistent inside and wrong outside. Use one sticky exit IP per session; a rotating endpoint splits one run across addresses and produces failures that look random. Residential and mobile ranges get more leeway than datacentre ones.

Cookie jar continuity. The jar that fetched the page posts the payloads and makes the protected request. Since bm_sz and bm_so key the payloads, one built from another session’s cookies is wrong however good the rest is.

Other vendors work alike; see DataDome and Incapsula reese84.

Common failure modes and how to debug them

Start at the top; the first items are the cheapest to fix.

  • Sensor post returns 200, _abck keeps ~-1~: likely the connection. Check your JA3/JA4 with an echo service, then header order with an order-preserving capture.
  • One request in a session fails: a retry or pool fell back to the default transport. Use one impersonating client throughout.
  • user-agent header differs from the payload’s agent: the edge reads the pair. Send the exact agent the payload was built for, every time.
  • Failures come and go: a rotating proxy, or a cached cookie. Pin one exit IP per session and re-read _abck, bm_sz and bm_so before every payload.
  • Some exits pass, others fail: the clock or locale. The payload’s time zone should be the exit’s, and accept-language plausible for that country.
  • Sensor post returns 201, or a page returns 428: the sec_cpt challenge, not a payload rejection.
  • sbsd post returns 200, page is still the interstitial: not accepted. Check bm_so was fresh, you posted to the path without v (with t on a held page), and the payload was built for the URL with both parameters.
  • Posts go out, the run never validates: order. Post one run’s payloads in the order they were built, never interleave two runs, never send sbsd telemetry before its tick.
  • Worked yesterday, refused today: the script rotated. Fetch it again.
  • Everything checks out, still refused: try a different proxy pool.

Solving Akamai with Parsa

Parsa builds the payloads; your client makes every request to the site. For sensor v3 you send page_url, script_url, the script on the first call (later, the build name a reply gave), the current abck and bmsz, and your ip; user_agent is optional. You get back body to post as is, a context for the next call, the run’s index and the user_agent to claim. sbsd takes bm_so as cookie, needs user_agent and accept_language on the first call, and returns body, the endpoint to post it to, and due_at for telemetry. Pixel takes html_var and script_var and returns one form body and its endpoint, for Chrome 128 to 158.

It does not solve the 428 sec_cpt challenge, refuses a user agent no machine in our corpus runs (unknown_agent), and cannot fix your TLS or header order. Timings are under benchmarks; you pay per 200 only, see pricing.

Akamai solver APISkip the browser. Send the challenge, post the answer.Sensor (v3), SBSD, Pixel. Pay per successful solve, never for a failure.

More guides