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