Skip to content

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 /akamai and /dd are 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
TransportWebSocket, /akamai/sessionOne POST, /akamai/sbsd/generate-session
ShapeRounds until the cookie is acceptedA ledger of bodies, issued once
Bound toThe originOne document, for five minutes
Your jobRelay each submission through the browserRewrite 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 bundle

So 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.scriptSrc is the raw src, 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.

FieldTypeNotes
schemastringMust be "akamai-sbsd-ledger-request/v3"
bundleobjectThe SBSD script as served
documentobjectA snapshot of the live page
profileobjectThe identity you are claiming

bundle

FieldTypeNotes
scriptSrcstringThe 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.
sourcestringThe bundle source as served. Max 2 MiB.

document

FieldTypeNotes
urlstringThe live document's URL. HTTPS, no credentials, no fragment.
htmlstringThe document as served. Max 4 MiB.
cookieHeaderstringdocument.cookie — despite the name, the JavaScript-visible jar, not the HTTP header
epochMsintegerperformance.timeOrigin, rounded. Identifies the document; see staleness.
resourceEntriesarrayperformance.getEntriesByType('resource'), each with duration, initiatorType, name, startTime. Max 2048 entries.
runtimeobjectSee below

document.runtime — readings taken from the live document, in one pass:

FieldNotes
connectionInfodownlink, effectiveType, rtt, saveData
domResourceInventorycapturedAtPerformanceMs, plus imgSrc / linkHref / scriptSrc attribute lists
functionToStringdescriptor, length, name, prototypeKind, sourceClass, sourceSha256
historyLengthhistory.length, at least 1
memoryInfojsHeapSizeLimit, 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

FieldTypeNotes
idstringchrome-<version>-<os>, and registered on the container
chromeFullVersionstringFour-part version; must be the one the named profile declares
overridesobjectdeviceMemory, 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

json
{
  "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: wrong schema, an unknown key, a bundle or document over its size cap, a scriptSrc that doesn't resolve to the document's origin, an epochMs outside the freshness window, a chromeFullVersion that disagrees with profile.id, runtime readings that contradict each other.
  • 422 — the generator declined to produce a complete ledger for this snapshot. receipt and observedCount say 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.