SurSolver

Akamai SBSD interstitial cookies

Solve the interstitial with its scripts and session cookies, then keep the same proxy for the protected request.

$0.40 / 1K tasksCharged when a task is accepted

On this page

What is Akamai SBSD and how to recognise it

Akamai SBSD places a short interstitial before the requested page. The interstitial can set bm_s, bm_sc and bm_so cookies and load scripts that belong to that challenge layer.

Treat the interstitial as a separate flow, not as another name for _abck sensor generation. The required inputs and proxy policy differ. A capture of the normal page alone may not include the state needed to solve the interstitial.

Use the Akamai web guide for _abck cookies, or the Akamai BMP guide for Android x-acf-sensor-data headers.

Which page, scripts, cookies and proxy to send

Request the protected page through the proxy you intend to keep. Capture the interstitial response and the scripts it loads. Send the protected page as websiteURL and the full script URLs as scriptUrls in execution order.

Preserve each script's query string. If there is only one script, send a one-element array rather than switching to scriptUrl.

You can also supply the captured interstitial HTML as pageData and that response's cookie state as cookies. Optional title is the page's document title. If you use verify, choose a protected path on the same origin, such as /account.

The SBSD API reference shows the accepted proxy fields and optional capture inputs.

Where the cookies go

Load solution.cookies into the HTTP or browser session that will make the protected request. The cookie jar is newline-separated JSON; preserve the domain and path of each cookie.

Use the returned userAgent when replaying. Send the request through the same proxy exit used for the challenge. A direct request from your own IP does not reproduce that session.

A ready solve is not a promise that the application will accept a different business request. Its own authentication, request body and route requirements still apply.

Common mistakes

Do not select the proxyless _abck task for an SBSD interstitial. The task type is AkamaiSbsdCookie, and it has no proxyless twin.

Do not mix a new proxy with cookies from an earlier capture. The page markup, interstitial cookies and scripts should come from the same challenge session.

Truncated script URLs, reordered scripts and omitted query strings can describe a different challenge. Capture the actual requests rather than guessing a static loader path.

Solve it with SurSolver

Create an AkamaiSbsdCookie task with the protected page, interstitial script array and proxy details. Include the current response's HTML and cookies when you have them, then poll getTaskResult.

Replay the returned cookie jar with its user agent through the same proxy. Use the SBSD API reference for a request example, or return to the Akamai web guide if the integration needs _abck rather than an interstitial solve.

The Playwright script below opens the interstitial through your proxy, captures its scripts, HTML and cookies for the task, then loads the solved jar into the same proxied browser session with the solve's user agent and waits for the protected page.

akamai-sbsd-playwright.js
// akamai-sbsd-playwright.js - solve Akamai SBSD and open the protected page with the returned
// cookie jar.
// Install: npm install playwright && npx playwright install chromium
// Run: node akamai-sbsd-playwright.js

const { chromium } = require('playwright');
const { setTimeout: sleep } = require('node:timers/promises');

// Your SurSolver API key.
const CLIENT_KEY = 'YOUR_API_KEY';
// The protected page that answers with the SBSD interstitial.
const TARGET_URL = 'https://example.com/page';
// The proxy for the solve and the replay: http://user:pass@host:port or socks5://host:port.
const PROXY_URL = 'http://username:password@proxy.example.com:8080';
// Optional, read from the page when empty: every script URL the interstitial loads, in order,
// with query strings.
const SCRIPT_URLS = undefined;
// A protected same-origin path that must answer 200 after the solve, such as /account.
const VERIFY_PATH = '';
// A selector that matches only after the site accepted the result.
const SUCCESS_SELECTOR = '.PASTE_ACCEPTED_SELECTOR';

const API = 'https://api.sursolver.com';
const POLL_ATTEMPTS = 60;

// API errors are HTTP 200 with errorId 1; anything else is a transport failure.
async function call(path, body) {
  const res = await fetch(`${API}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
  return data;
}

async function solve(task) {
  const created = await call('/createTask', { clientKey: CLIENT_KEY, task });

  for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
    const result = await call('/getTaskResult', {
      clientKey: CLIENT_KEY,
      taskId: created.taskId,
    });
    if (result.status === 'ready') return result.solution;
    await sleep(3000);
  }
  throw new Error(`task ${created.taskId} was not ready in ${POLL_ATTEMPTS} polls`);
}

// Optional task fields are sent only when they have a value.
function withoutEmpty(fields) {
  return Object.fromEntries(
    Object.entries(fields).filter(([, value]) => value !== undefined && value !== ''),
  );
}

// A value the page did not show must be pasted into its constant at the top.
function need(name, value) {
  if (value === undefined || value === null || value === '') {
    throw new Error(`Could not read ${name} from the page; fill in the ${name} constant.`);
  }
  return value;
}

// Chromium takes the proxy as a server plus credentials rather than one URL.
// It cannot send credentials to a SOCKS5 proxy; use an http:// proxy for browser runs.
function proxyOption(proxyUrl) {
  const url = new URL(proxyUrl);
  return {
    server: `${url.protocol}//${url.host}`,
    ...(url.username
      ? { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password) }
      : {}),
  };
}

// The jar holds one JSON cookie per line; Playwright needs a domain and a path on each.
function jarToCookies(jar) {
  return jar
    .split('\n')
    .filter(Boolean)
    .map((line) => JSON.parse(line))
    .map((cookie) => ({
      name: cookie.name,
      value: cookie.value,
      domain: cookie.host_only ? cookie.host : `.${cookie.host.replace(/^\./, '')}`,
      path: cookie.path,
      secure: cookie.secure,
      httpOnly: cookie.http_only,
      ...(cookie.persistent && cookie.expires_ms ? { expires: cookie.expires_ms / 1000 } : {}),
    }));
}

// Opens the interstitial through the proxy and reads its scripts, HTML and cookies.
async function readPage(browser) {
  const context = await browser.newContext();
  const page = await context.newPage();
  try {
    const response = await page.goto(TARGET_URL);
    const scriptUrls = await page.evaluate(
      () => [...document.scripts]
        .map((script) => script.src)
        .filter(Boolean),
    );
    const cookies = (await context.cookies())
      .map((cookie) => `${cookie.name}=${cookie.value}`)
      .join('; ');
    return { pageUrl: page.url(), scriptUrls, pageData: await response.text(), cookies };
  } finally {
    await context.close();
  }
}

async function main() {
  const browser = await chromium.launch({ proxy: proxyOption(PROXY_URL) });
  try {
    const found = await readPage(browser);
    const solution = await solve({
      type: 'AkamaiSbsdCookie',
      websiteURL: found.pageUrl,
      scriptUrls: need('scriptUrls', SCRIPT_URLS || found.scriptUrls),
      pageData: found.pageData,
      ...withoutEmpty({ cookies: found.cookies }),
      ...withoutEmpty({
        verify: VERIFY_PATH,
      }),
      proxy: PROXY_URL,
    });
    if (!solution.cookies) throw new Error('The solve returned no cookie jar.');

    // Replay with the user agent the solve presented, through the same proxy.
    const context = await browser.newContext(
      solution.userAgent ? { userAgent: solution.userAgent } : {},
    );
    await context.addCookies(jarToCookies(solution.cookies));
    const page = await context.newPage();
    await page.goto(found.pageUrl, { waitUntil: 'domcontentloaded' });
    await page.locator(SUCCESS_SELECTOR).first().waitFor({ timeout: 15000 });
    console.log('The site served the protected page.');
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

FAQ

How is SBSD different from the Akamai _abck solver?

SBSD is a separate interstitial flow. It requires a caller proxy and uses the interstitial scripts and cookies. AkamaiCookie generates web _abck state without a caller proxy.

Does AkamaiSbsdCookie have a proxyless variant?

No. Provide an HTTP or SOCKS5 proxy and replay through the same exit IP that the solve used.

Should I send scriptUrl or scriptUrls?

Use scriptUrls: a non-empty array in execution order, including the query strings of the actual interstitial scripts. An array is required even when there is only one script.

Which cookies belong in the task?

When available, supply the cookies from the same interstitial response. Do not mix cookies or markup from a different proxy session.

What does verify mean?

verify is an optional protected path on the same origin that should return HTTP 200 after the solve. It is not an unrelated external URL.

From the blog

No posts yet — read the blog for new guides and release notes.