API reference
Two endpoints. POST /v3 returns sensor data for the
_abck cookie, POST /sbsd returns the SBSD
body. Device selection, script analysis and challenge solving all
happen on our side.
The request format is Hyper's, so an existing hyper_sdk
integration works by changing the base URL and nothing else.
Base URL
Requests are JSON over HTTPS. There is no version in the path.
https://sold-out.dev
# sensor data POST /v3 POST /v2/sensor # same handler, older path # sbsd body POST /sbsd
Authentication
Every solve call carries an API key. Three headers are accepted so a client written against another provider does not have to change how it builds requests. All three resolve the same credential.
x-api-keystringThe default.
x-keystringAlternative spelling, for clients that already send it.
AuthorizationstringBearer token form.
Keys are created in the dashboard and stay visible in your key list, so you can copy one again at any time. If a key is compromised, delete it and create a new one.
x-api-key: ak_live_7f3c… # or x-key: ak_live_7f3c… Authorization: Bearer ak_live_7f3c…
SDKs
Official client libraries for Go, JavaScript and Python wrap authentication and the request format, so you call a method instead of building JSON by hand. They speak the same Hyper format the raw API does — nothing below changes.
Find them, with install and usage instructions, on our GitHub: github.com/sold-out-dev.
Prefer HTTP? Every endpoint on this page works directly with any HTTP client — the SDKs are a convenience, not a requirement.
# Go go get github.com/sold-out-dev/sold-universal-sdk-go # JavaScript / TypeScript npm install sold-universal-sdk-js # Python pip install sold-universal-sdk
https://github.com/orgs/sold-out-dev/repositories
/v3Sensor data
Returns the payload to post to the sensor endpoint, which turns your
_abck cookie valid. Also reachable at
/v2/sensor, the same handler under the path older Hyper
clients use.
Body parameters
userAgentstringrequiredDesktop Google Chrome. The major version decides which fingerprint pool is drawn from.
pageUrlstringrequiredThe page the sensor is posted from.
abckstringrequiredCurrent _abck cookie value.
bmszstringrequiredCurrent bm_sz cookie value. It also pins the session
to one fingerprint, so a session keeps the same device
throughout.
scriptstringfirst callThe sensor script body. Send it once per session, then send
context instead. null is accepted and
means the same as omitting it.
contextstringOpaque token from the previous response. Carries the script
analysis forward so repeat calls skip the expensive part.
null is accepted and means the same as omitting
it.
ipstringThe address the sensor will be posted from. Used for geo-consistent values.
acceptLanguagestringMatches the header you will send.
scriptUrlstringPath the script was served from.
iterationinteger0-based index of this sensor within the session. Defaults to 0.
Response
payloadstringPost this to the sensor endpoint as-is.
contextstringKeep it and send it back on the next call in the same session.
curl https://sold-out.dev/v3 \ -H "x-api-key: $SOLDOUT_KEY" \ -H "content-type: application/json" \ -d '{ "pageUrl": "https://target.com/", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64…", "abck": "<_abck cookie>", "bmsz": "<bm_sz cookie>", "script": "<sensor script>", "ip": "1.2.3.4" }'
import httpx r = httpx.post( "https://sold-out.dev/v3", headers={"x-api-key": SOLDOUT_KEY}, json={ "pageUrl": page_url, "userAgent": user_agent, "abck": jar["_abck"], "bmsz": jar["bm_sz"], "script": script, # first call only "ip": proxy_ip, }, timeout=120, ) sensor = r.json()["payload"] context = r.json()["context"] # reuse next call
const r = await fetch("https://sold-out.dev/v3", { method: "POST", headers: { "x-api-key": process.env.SOLDOUT_KEY, "content-type": "application/json", }, body: JSON.stringify({ pageUrl, userAgent, abck: jar._abck, bmsz: jar.bm_sz, script, // first call only ip: proxyIp, }), }); const { payload, context } = await r.json();
{ "payload": "3;0;1;0;3490105;Li9UX…", "context": "bSQ0lf…" }
/sbsdSBSD body
The full SBSD body, derived from the script and the bm_so
cookie. One device for the whole session, not a fresh fingerprint per
request.
Body parameters
uuidstringrequiredThe UUID from the SBSD script. Without it there is nothing to encrypt against.
userAgentstringrequiredDesktop Google Chrome, same rule as /v3.
pageUrlstringrequiredThe page the body is posted from.
ostringrequiredThe bm_so cookie value.
scriptstringfirst callThe SBSD script body.
contextstringOpaque token from the previous response, same role as on
/v3.
ipstringThe address the body will be posted from.
acceptLanguagestringMatches the header you will send.
scriptUrlstringPath the script was served from.
indexinteger0-based index of this body within the session. Defaults to 0.
Response
bodystringThe SBSD body to post.
payloadstringThe same string as body. The Hyper SDK reads this
key and older clients read body, so both are
returned and neither has to change.
contextstringSend back on the next call in the same session.
curl https://sold-out.dev/sbsd \ -H "x-api-key: $SOLDOUT_KEY" \ -H "content-type: application/json" \ -d '{ "uuid": "9f2c1b7e-…", "pageUrl": "https://target.com/", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64…", "o": "<bm_so cookie>", "script": "<sbsd script>", "ip": "1.2.3.4" }'
import httpx r = httpx.post( "https://sold-out.dev/sbsd", headers={"x-api-key": SOLDOUT_KEY}, json={ "uuid": uuid, "pageUrl": page_url, "userAgent": user_agent, "o": jar["bm_so"], "script": script, # first call only "ip": proxy_ip, }, timeout=120, ) sbsd = r.json()["body"]
const r = await fetch("https://sold-out.dev/sbsd", { method: "POST", headers: { "x-api-key": process.env.SOLDOUT_KEY, "content-type": "application/json", }, body: JSON.stringify({ uuid, pageUrl, userAgent, o: jar.bm_so, script, // first call only ip: proxyIp, }), }); const { body } = await r.json();
{ "body": "eyJ6IjoiN…", "payload": "eyJ6IjoiN…", "context": "bSQ0lf…" }
/pixelPixel body
The Akamai pixel challenge body. Give it the two values parsed from the
page and the pixel script; you get back the form body to POST to the
pixel_ endpoint.
Body parameters
userAgentstringrequiredDesktop Google Chrome, same rule as /v3.
htmlVarnumberrequiredThe bazadebezolkohpepadr value from the page HTML.
scriptVarstringrequiredThe dynamic value from the pixel script (the g=_[N] string).
ipstringThe egress IP of the request that will carry the result.
acceptLanguagestringThe Accept-Language header you send.
curl https://sold-out.dev/pixel \ -H "x-api-key: $SOLDOUT_KEY" \ -H "content-type: application/json" \ -d '{ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64…", "htmlVar": 32, "scriptVar": "94621f30b57f5db88ca303259e1882fe", "ip": "1.2.3.4" }'
import httpx r = httpx.post( "https://sold-out.dev/pixel", headers={"x-api-key": SOLDOUT_KEY}, json={ "userAgent": user_agent, "htmlVar": html_var, "scriptVar": script_var, "ip": proxy_ip, }, timeout=120, ) pixel = r.json()["payload"]
const r = await fetch("https://sold-out.dev/pixel", { method: "POST", headers: { "x-api-key": process.env.SOLDOUT_KEY, "content-type": "application/json", }, body: JSON.stringify({ userAgent, htmlVar, scriptVar, ip: proxyIp, }), }); const { payload } = await r.json();
{ "payload": "ap=true&bt=%7B…&t=…&u=…" }
Sessions
A session is one browser as far as Akamai is concerned. Two things keep it coherent.
bm_sz pins the fingerprint. The same cookie draws the
same device from the pool for the life of the session, so the screen,
the GPU and the plugin list do not change underneath.
context carries the script analysis. Send
script on the first call, keep the context
that comes back, and send it instead of the script from then on. The
expensive work then happens once per session rather than once per
request.
# 1. first sensor, send the script POST /v3 { …, "script": "<568 KB>" } → { "payload": …, "context": "bSQ0lf…" } # 2. and after, send the context instead POST /v3 { …, "context": "bSQ0lf…", "iteration": 1 } → { "payload": …, "context": "bSQ0lf…" } # 3. same bm_sz throughout means the same device POST /v3 { …, "context": "bSQ0lf…", "iteration": 2 }
Errors
Every failure is a JSON object with an error string that
says what was wrong with the request. Server-side faults are the one
exception: they answer internal error, because the detail
is about our internals and belongs in our logs, not in your client.
400bad requestMalformed JSON, a missing required field, or a user agent we do not support. Anything that is not desktop Chrome is refused, and so is a Chrome major we hold no fingerprints for. Never billed.
401unauthorizedNo key presented.
402payment requiredBalance is at zero. Nothing queues and nothing accrues, so the account cannot go into debt.
403forbiddenKey is unknown, revoked or expired.
502bad gatewayNo solver could take the request, or the solver failed on it. The reason is in our logs, not in the response. Retry, and open a ticket if it persists. Never billed.
504timeoutNo worker answered within 120 seconds. Retry. Never billed.
Only a 200 is charged. Failures are still recorded so you
can see your own error rate in the dashboard, at a cost of zero.
Request bodies are capped at 8 MB, which a sensor script fits into
comfortably.
400 Bad Request { "error": "userAgent is required" } 402 Payment Required { "error": "insufficient balance, top up to keep solving" } 504 Gateway Timeout { "error": "no worker answered within 120s" }
User agents
The userAgent you send must be one we hold fingerprints
for. Only desktop Chrome on Windows is supported, majors 149–151.
Anything else is a 400, never billed.
Send the string exactly as listed — same casing and spacing.
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
Start with €2 and a key.
Prepaid, per request, stops at zero. No card on file, no minimum, no call required. That one is only for unlimited.