SurSolver

Kasada tokens and request headers

Find the Kasada loader, generate its request headers, and replay the protected call with a fresh challenge response.

$0.70 / 1K tasksCharged when a task is accepted

On this page

What is Kasada and how to recognise it

Kasada checks the browser signals attached to a protected request. In a captured session, look for its p.js loader and headers such as x-kpsdk-ct and x-kpsdk-cd. A login or API request can carry these values without showing a checkbox or image challenge.

The loader URL belongs to the protected site and its current integration. Finding a familiar filename is only the starting point: collect the actual page URL and loader together so that the solve uses the right origin.

A Kasada result is not an Akamai cookie or a reCAPTCHA response. Its token and challenge response must be sent as request headers.

Which page URL and p.js loader to send

Start with the page that initiates the protected action. Open its Network panel, find the Kasada p.js request, and copy that complete URL as scriptUrl. Send the initiating page as websiteURL.

Do not substitute the final API endpoint for the page URL, guess a loader from an older capture, or send the loader as scriptUrls: this type takes a single scriptUrl string. Optional title is the page's document title.

Neither KasadaToken nor KasadaTokenProxyLess requires your proxy. The Kasada API reference shows the exact request and solution fields.

Where the headers go

Send solution.token as x-kpsdk-ct and solution.kpsdkCd as x-kpsdk-cd. If the result includes kpsdkH and kpsdkV, send them as x-kpsdk-h and x-kpsdk-v respectively. The protected call is an HTTP request with these headers: Kasada rejects a replay that navigates a browser to the URL, so send it from an HTTP client or your browser tool's request client instead.

Use the returned userAgent for User-Agent and acceptLanguage for Accept-Language. These values describe the browser identity used by the solve; do not silently replace them with the defaults of your HTTP library.

The kpsdkCd value is returned unused and is for one request. If you need another protected call, obtain a fresh challenge response rather than sharing one value between parallel requests.

Common mistakes

A common integration error is applying only token. The protected request also needs its challenge response and the returned browser headers. Another is treating the result as a cookie because a different solver type returns cookies.

Check that the page and loader came from the same current integration. If the site changes its script path, update the captured URL rather than keeping a hardcoded example.

Do not assume a successful solve validates the rest of your request. Application authentication, request formatting and endpoint-specific requirements still apply.

Solve it with SurSolver

Create a KasadaToken or KasadaTokenProxyLess task with the protected page URL and current loader URL. Poll getTaskResult, then apply the returned header values to one protected request.

Use the Kasada API reference for the request example. If your capture instead contains a JSON-shaped set of rotating request headers from F5, compare it with the Shape Security guide before selecting a task.

The Playwright script below solves for the initiating page and sends the protected API request with the returned x-kpsdk-ct, x-kpsdk-cd, User-Agent and Accept-Language values, then checks the response for text only an accepted request produces. The Kasada Playwright guide walks through the full flow.

kasada-playwright.js
// kasada-playwright.js - solve Kasada and send the protected request with the returned headers.
// Install: npm install playwright && npx playwright install chromium
// Run: node kasada-playwright.js

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

// Your SurSolver API key.
const CLIENT_KEY = 'YOUR_API_KEY';
// The page that starts the protected action.
const TARGET_URL = 'https://example.com/page';
// Optional, read from the page when empty: the p.js request of the page that starts the
// protected action.
const SCRIPT_URL = '';
// The protected request the page makes after Kasada ran.
const API_URL = 'https://example.com/api/endpoint';
// Its HTTP method; GET when unset.
const API_METHOD = '';
// Its JSON body for POST, PUT and PATCH requests.
const API_BODY = '';
// Other headers the protected request needs, as a JSON object, such as {"x-app-id":"web"}.
const API_HEADERS = undefined;
// Text that appears only in a response the site accepted.
const SUCCESS_TEXT = 'PASTE_ACCEPTED_MARKER';

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;
}

// Opens the page once and reads what the solve needs from it.
async function readPage(browser) {
  const page = await browser.newPage();
  const requests = [];
  page.on('request', (request) => requests.push(request));
  try {
    await page.goto(TARGET_URL, {
      waitUntil: 'domcontentloaded',
      timeout: 45000,
    });
    // Requests made after load count too; wait for them, but never hang on a busy page.
    await page.waitForLoadState('networkidle', { timeout: 5000 })
      .catch(() => undefined);
    const urls = (matches) =>
      [...new Set(requests.filter(matches).map((request) => request.url()))];
    return {
      pageUrl: page.url(),
      scriptUrl: urls((request) => /\/p\.js(\?|$)/.test(request.url()))[0],
    };
  } finally {
    await page.close();
  }
}

async function main() {
  const browser = await chromium.launch();
  try {
    const found = await readPage(browser);
    const solution = await solve({
      type: 'KasadaTokenProxyLess',
      websiteURL: found.pageUrl,
      scriptUrl: need('scriptUrl', SCRIPT_URL || found.scriptUrl),
    });

    const headers = {
      ...(API_HEADERS ?? {}),
      'x-kpsdk-ct': solution.token,
      'x-kpsdk-cd': solution.kpsdkCd,
      'User-Agent': solution.userAgent,
      'Accept-Language': solution.acceptLanguage,
    };
    // The challenge returns these two only when the site uses them.
    if (solution.kpsdkH) headers['x-kpsdk-h'] = solution.kpsdkH;
    if (solution.kpsdkV) headers['x-kpsdk-v'] = solution.kpsdkV;
    // Replay with the browser identity the solve presented.
    const context = await browser.newContext({ userAgent: solution.userAgent });
    if (API_BODY) headers['Content-Type'] = 'application/json';
    const response = await context.request.fetch(API_URL, {
      method: API_METHOD || 'GET',
      headers,
      ...(API_BODY ? { data: API_BODY } : {}),
    });
    const text = await response.text();
    if (!response.ok() || !text.includes(SUCCESS_TEXT)) {
      throw new Error(`The site did not accept the headers (HTTP ${response.status()}).`);
    }
    console.log('The site accepted the headers.');
  } finally {
    await browser.close();
  }
}

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

FAQ

Does a Kasada task need a proxy?

No. KasadaToken and KasadaTokenProxyLess both work without a caller proxy. A supplied proxy is not used by this type.

Which script URL does Kasada need?

Send the exact p.js loader URL loaded by the protected page as scriptUrl. Use the full URL from the page source or Network panel, not an invented path or a URL from another site.

Where does the Kasada token go?

Send token as x-kpsdk-ct and kpsdkCd as x-kpsdk-cd on the protected request. When returned, kpsdkH and kpsdkV become x-kpsdk-h and x-kpsdk-v. Match the returned userAgent and acceptLanguage too.

Can I reuse x-kpsdk-cd for several requests?

No. The returned challenge response is for one protected request. Do not reuse it across retries or parallel requests.

Is a Kasada result a browser cookie?

No. This task returns a token and request-header values. Apply those values to the protected request rather than storing token as a cookie.