SurSolver

Shape Security request headers

Generate the headers for a protected app request, using the page, loader, endpoint and HTTP method that belong together.

$0.70 / 1K tasksCharged when a task is accepted

On this page

What is Shape Security and how to recognise it

Shape Security, part of F5, adds browser-generated headers to protected requests. The names are integration-specific; a captured request can contain a family of x-... headers rather than a familiar fixed captcha field.

Inspect the request triggered by the protected action, not only the page load. The page's loader and the API call it protects serve different roles. The solve needs both so it can generate headers for the intended endpoint.

This is not a visible form widget. Its result is a set of request headers, even though the API task names use ShapeCookie and ShapeCookieProxyLess.

Which page, loader, endpoint and method to send

Capture a normal page load and the action you intend to repeat. Send the page as websiteURL, its exact Shape loader as scriptUrl, and the protected endpoint as targetUrl.

Set method to the method of that endpoint. It defaults to POST, but a request that uses GET, PUT, PATCH or DELETE should declare the actual method. Optional title is the document title of the initiating page.

Use a fresh loader URL from the integration, exactly as the page's own <script src> includes it, query string included: some loaders only boot with their query parameters and stamp nothing without them. A URL copied from a different host, or an endpoint that is not protected by that loader, can also produce no useful headers. The Shape API reference gives the full field list.

Where the headers go

Parse solution.token as JSON. Each key is an HTTP header name and each value is the value to send on the protected request. Apply the names as returned; do not hardcode a header family from an unrelated example.

If cookies is returned, parse its newline-separated cookie objects and load them into the same client session. Preserve each cookie's domain and path rather than assigning every cookie to a broad domain.

Match userAgent and acceptLanguage when supplied. These describe the browser identity used to generate the result and should travel with the header set.

Common mistakes

The first is treating token as a plain token string or cookie value. For Shape it contains a JSON header map. The second is generating for the page URL when the protected action is an API endpoint; that endpoint belongs in targetUrl.

A method mismatch is another source of confusion. Generate for the request you will actually send, not the default method of a copied example.

Keep one result's headers, cookies and browser identity together. Mixing values from different solves does not reproduce the same client session.

Solve it with SurSolver

Create a ShapeCookie or ShapeCookieProxyLess task with the page, loader and protected target URL. Poll getTaskResult, parse the header JSON, and replay the request with the accompanying session values.

The Shape API reference contains the request example and output shape. For a request carrying fixed x-kpsdk-... headers instead, use the Kasada guide.

The Playwright script below solves for the page and endpoint, parses the returned header JSON into the request headers, adds the returned cookies and browser identity, and checks the response for text only an accepted request produces. The Shape Playwright guide walks through the full flow.

shape-playwright.js
// shape-playwright.js - solve Shape Security and send the protected request with the returned
// headers.
// Install: npm install playwright && npx playwright install chromium
// Run: node shape-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 loads the Shape script.
const TARGET_URL = 'https://example.com/page';
// The Shape loader script of the page (DevTools, Network, the script before the API call).
const SCRIPT_URL = 'https://example.com/challenge.js';
// The protected endpoint the page calls (DevTools, Network).
const API_URL = 'https://example.com/api/endpoint';
// The HTTP method of that endpoint (GET, POST, PUT, PATCH or DELETE); defaults to POST.
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 !== ''),
  );
}

// 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 } : {}),
    }));
}

async function main() {
  const browser = await chromium.launch();
  try {
    const solution = await solve({
      type: 'ShapeCookieProxyLess',
      websiteURL: TARGET_URL,
      scriptUrl: SCRIPT_URL,
      targetUrl: API_URL,
      ...withoutEmpty({
        method: API_METHOD,
      }),
    });

    // `token` is a JSON map of header names to values; send them exactly as returned.
    const headers = {
      ...(API_HEADERS ?? {}),
      ...JSON.parse(solution.token),
      'User-Agent': solution.userAgent,
      'Accept-Language': solution.acceptLanguage,
    };
    // Replay with the browser identity the solve presented.
    const context = await browser.newContext({ userAgent: solution.userAgent });
    if (solution.cookies) await context.addCookies(jarToCookies(solution.cookies));
    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

What does a Shape task return?

token is a JSON string containing the generated request headers. The result can also include cookies, userAgent and acceptLanguage for the replay.

Does Shape need a proxy?

No. ShapeCookie and ShapeCookieProxyLess do not require a caller proxy, and a supplied proxy is not used.

What is the difference between websiteURL and targetUrl?

websiteURL is the page that loads the protection. targetUrl is the protected request that needs generated headers, such as the login API endpoint.

Which HTTP method should I send?

Use the method of the protected request. The default is POST; supported methods are GET, POST, PUT, PATCH and DELETE.

Should I send token as one HTTP header?

No. Parse the token JSON and apply each returned header name and value. Do not send the complete JSON string under a header named token.