Akamai SBSD API Reference
SBSD is Akamai's second scoring channel. A property that uses it serves a bundle script, and that bundle POSTs its own bodies back to the path it was served from — separately from, and in addition to, the _abck sensor. Where the bundle lives varies by property; see finding the bundle.
Both
/akamaiand/ddare described in one machine-readable spec: download openapi.yml · browse interactively.
How it differs from the sensor channel
The sensor channel is a conversation. SBSD is a single request:
Sensor (_abck) | SBSD | |
|---|---|---|
| Transport | WebSocket, /akamai/session | One POST, /akamai/sbsd/generate-session |
| Shape | Rounds until the cookie is accepted | A ledger of bodies, issued once |
| Bound to | The origin | One document, for five minutes |
| Your job | Relay each submission through the browser | Rewrite the page's own carrier POSTs |
You do not send anything new. The page already POSTs to the bundle's own path; your integration intercepts those requests and swaps the body for the next row of the ledger. Ordering is FIFO and is not negotiable.
Finding the bundle
Some properties serve the bundle from /.well-known/sbsd. Many do not — the common alternative is an obfuscated per-property path, sitting alongside the _abck sensor script under the same random-looking prefix, with nothing in either path naming the channel:
/78jHEHB-.../6LncB the _abck sensor
/.well-known/sbsd/953343??v=<uuid> the SBSD bundle
/bJ21n/.../fQUZPAE/Q01H/JFVIT0YB the _abck sensor
/bJ21n/.../K0ciPAE/NQdp/VyRmPU8Y?v=<uuid> the SBSD bundleSo detect the bundle rather than assuming a path. What identifies it, wherever it is served from, is the UUID v= on its src — that value seeds the bundle's codec rather than versioning a file, so an ordinary ?v=3.5.6 cache-buster does not look like it. The carrier POSTs then go to that same pathname, usually with a different query string or none at all.
Two consequences worth planning for:
- A site that looks unprotected at its apex may not be. Region choosers and marketing shells are frequently unprotected in front of a protected application. Detect on the document you actually want.
bundle.scriptSrcis the rawsrc, query included. Reconstructing a tidy URL and dropping the?v=produces a ledger the page cannot use.
POST /akamai/sbsd/generate-session
Issues a ledger for the document described in the request.
Request body
Every field below is required, and no others are accepted — an unknown key is a 400 rather than being ignored.
| Field | Type | Notes |
|---|---|---|
schema | string | Must be "akamai-sbsd-ledger-request/v3" |
bundle | object | The SBSD script as served |
document | object | A snapshot of the live page |
profile | object | The identity you are claiming |
bundle
| Field | Type | Notes |
|---|---|---|
scriptSrc | string | The raw src attribute, query string included — the ?v= is not decoration. Must resolve to the document's own HTTPS origin and carry a pathname. Max 8 KiB. |
source | string | The bundle source as served. Max 2 MiB. |
document
| Field | Type | Notes |
|---|---|---|
url | string | The live document's URL. HTTPS, no credentials, no fragment. |
html | string | The document as served. Max 4 MiB. |
cookieHeader | string | document.cookie — despite the name, the JavaScript-visible jar, not the HTTP header |
epochMs | integer | performance.timeOrigin, rounded. Identifies the document; see staleness. |
resourceEntries | array | performance.getEntriesByType('resource'), each with duration, initiatorType, name, startTime. Max 2048 entries. |
runtime | object | See below |
document.runtime — readings taken from the live document, in one pass:
| Field | Notes |
|---|---|
connectionInfo | downlink, effectiveType, rtt, saveData |
domResourceInventory | capturedAtPerformanceMs, plus imgSrc / linkHref / scriptSrc attribute lists |
functionToString | descriptor, length, name, prototypeKind, sourceClass, sourceSha256 |
historyLength | history.length, at least 1 |
memoryInfo | jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize; must satisfy 0 ≤ used ≤ total ≤ limit |
sessionStorage | { akBmTabId } — 8–64 lowercase base36 characters |
speechSynthesisVoices | { localCount, totalCount }, with localCount ≤ totalCount |
Take these in one evaluate call. They are compared against each other, and reading them across three round-trips describes a page that never existed.
functionToString is Function.prototype.toString as the page sees it, stringified through a pristine child realm so a wrapper on the page cannot describe itself as native.
profile
| Field | Type | Notes |
|---|---|---|
id | string | chrome-<version>-<os>, and registered on the container |
chromeFullVersion | string | Four-part version; must be the one the named profile declares |
overrides | object | deviceMemory, hardwareConcurrency, languages, screen, timezone, timezoneOffsetMinutes |
Declare the identity, don't measure it
profile.overrides should carry the same identity you declare on the sensor channel — not values read back off a page whose viewport is emulated.
Playwright's Emulation.setDeviceMetricsOverride leaves screen.availLeft and screen.availTop at 0, and availHeight equal to height. On macOS that is impossible: the menu bar is always there. A payload built from those readings describes a browser that cannot exist, and nothing errors — _abck simply never leaves ~-1~, which is the same symptom as a dozen unrelated causes.
Success response 200
{
"schema": "akamai-sbsd-ledger/v1",
"complete": true,
"ok": true,
"expectedCap": 3,
"ordering": "sensor-emission-fifo",
"runNonce": "b6278f3f-8160-4df7-ab42-b2aeb3bce4c6",
"ledgerId": "b6278f3f-8160-4df7-ab42-b2aeb3bce4c6",
"generatedAtEpochMs": 1772376021395,
"submissions": [
{
"index": 0,
"body": "...",
"bytes": 965,
"method": "POST",
"resolvedUrl": "https://www.hilton.com/.well-known/sbsd",
"url": "/.well-known/sbsd",
"sha256": "...",
"surface": "xhr"
}
],
"binding": { "…": "digests of the inputs this ledger was computed from" },
"receipt": { "…": "what the generator resolved for each surface" }
}Send submissions in index order, one per carrier POST. complete: true is the only ledger safe to use.
Rows are capacity, not a promise
expectedCap says how many rows the ledger carries. The page emits as many carriers as it emits. If it emits more than you have rows for, fail closed — abort the extra request. Letting the native body through hands Akamai a payload from an uninstrumented page alongside yours, which is worse than sending nothing.
The ledger is bound to one document
The bodies are computed from the snapshot you sent: that HTML, those cookies, those resource timings, those runtime readings. They are not portable. Replaying a ledger against a second document, a second tab or a later load is a mismatch, and the server refuses a snapshot more than five minutes old (or more than a short window in the future).
Generate per document, use in order, discard.
Hold the first carrier
document.runtime.sessionStorage.akBmTabId only exists once the bundle has run. So the carrier POST that triggers generation is also the one that has to wait for it — hold it, generate, then release it with row 0 as its body.
If your page emits carriers concurrently, memoize the in-flight promise rather than the result. Two carriers that each start a request produce two ledgers, the second replaces the first, and the page ends up submitting row 0 of one snapshot followed by rows 1–2 of another.
Error responses
All non-200 responses carry complete: false and an error object with a code and a message.
400— validation failure: wrongschema, an unknown key, a bundle or document over its size cap, ascriptSrcthat doesn't resolve to the document's origin, anepochMsoutside the freshness window, achromeFullVersionthat disagrees withprofile.id, runtime readings that contradict each other.422— the generator declined to produce a complete ledger for this snapshot.receiptandobservedCountsay what it could not reconcile; the usual answer is a snapshot taken before the bundle had run.500— the container's own configuration or a malformed internal result.503— the deadline expired while waiting for queue capacity.504— generation did not complete before the deadline.
This channel needs a real browser
The sensor channel has a server-side option (POST /akamai/solve) for setups with no browser. SBSD does not, and cannot: the integration rewrites the body of a POST the page makes, so with no page there is no carrier to rewrite and no document to snapshot.
For the same reason there is no HTTP-client or Python variant, and Lightpanda cannot drive it either — document.runtime requires performance.memory, navigator.connection and speechSynthesis, and Lightpanda implements none of the three. Use Playwright (or another CDP-driven Chrome) for this channel.
Worked example
src/akamai/sbsd in the examples repo drives both channels against hilton.com, which runs both: SBSD gates the first document, and the _abck sensor gates everything after it.