> ## Documentation Index
> Fetch the complete documentation index at: https://docs.roolink.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Solve your first Akamai challenge

> Go from an empty project to a valid cookie on a site protected by Akamai Bot Manager. About fifteen minutes.

<div className="rl-intro">
  <p>**What you will build.** A small script that loads a protected page the way a browser does, asks Roolink for the sensor the page expects, posts it back, and ends with a cookie the site trusts. That cookie is what lets your next request through.</p>

  <p>**What you need.** A Roolink account with an API key, a residential proxy that keeps the same IP for a session, [powhttp](https://powhttp.com) installed, and Python, Go or Node. Pick your language in any code block; every block on the page follows.</p>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>1</span>Set up</div>

    <p>Copy your API key from the [dashboard](https://www.roolink.io/dashboard) and put it in an environment variable, along with your proxy URL. Then install two packages: the Roolink SDK, which talks only to Roolink, and a TLS client, which talks to the target site.</p>

    <p>Open [powhttp](https://powhttp.com), start recording, and load your target page in Chrome. Keep that recording open. Every request you make below has a matching one in it, and you will copy the headers from there.</p>

    <Note>
      The TLS client is not optional. Akamai fingerprints the connection and the exact order of your headers before the sensor matters. The libraries below are the ones we recommend; [Set up your HTTP client](/http-client) explains each setting.
    </Note>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```bash Python theme={null}
      export ROOLINK_API_KEY="roo_..."
      export PROXY_URL="http://user:pass@host:port"

      pip install roolink git+https://github.com/Nintendocustom/Python-Tls-Client.git
      ```

      ```bash Go theme={null}
      export ROOLINK_API_KEY="roo_..."
      export PROXY_URL="http://user:pass@host:port"

      go get github.com/roolinkio/roolink-go
      go get github.com/bogdanfinn/tls-client
      ```

      ```bash TypeScript theme={null}
      export ROOLINK_API_KEY="roo_..."
      export PROXY_URL="http://user:pass@host:port"

      npm install @roolink/sdk tlsclientwrapper
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>2</span>Load the page and find the script</div>

    <p>Start the way a person would: open the page that comes before the protected action. For a shop, that is the product page before add to cart. Send it through your proxy with the headers Chrome sent for that page in your powhttp recording, in the same order.</p>

    <p>Two things come back. The response sets the `_abck` and `bm_sz` cookies, and the HTML contains one `<script>` tag whose path looks random, near the end of the body. That path is different for every site and changes over time, so read it from the page each run.</p>

    <p>**You should see** a `200` and a script path such as `/aB3dE/fG7/hI/jK/LmNoPqRsTuVwXyZ`.</p>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```python Python theme={null}
      import json, os, re
      import tls_client
      from roolink import RoolinkClient, WebSensorRequest

      PAGE = "https://www.example.com/product/123"
      ORIGIN = "https://www.example.com"
      UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"

      roolink = RoolinkClient(os.environ["ROOLINK_API_KEY"])
      site = tls_client.Session(client_identifier="chrome_152", random_tls_extension_order=True)
      site.proxies = {"http": os.environ["PROXY_URL"]}

      page = send("GET", PAGE, PAGE_HEADERS)
      script_path = re.search(r'<script[^>]+src="(/[^"]+)"[^>]*defer', page.text).group(1)
      script_url = ORIGIN + script_path
      print(page.status_code, script_path)
      ```

      ```go Go theme={null}
      const (
      	page   = "https://www.example.com/product/123"
      	origin = "https://www.example.com"
      	ua     = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"
      )

      rl := roolink.NewClient(os.Getenv("ROOLINK_API_KEY"))

      jar := tls_client.NewCookieJar()
      site, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
      	tls_client.WithClientProfile(profiles.Chrome_152),
      	tls_client.WithRandomTLSExtensionOrder(),
      	tls_client.WithCookieJar(jar),
      	tls_client.WithProxyUrl(os.Getenv("PROXY_URL")),
      )
      if err != nil {
      	log.Fatal(err)
      }

      html := get(site, page, pageHeaders(""))
      scriptPath := regexp.MustCompile(`<script[^>]+src="(/[^"]+)"[^>]*defer`).FindStringSubmatch(html)[1]
      scriptURL := origin + scriptPath
      fmt.Println(scriptPath)
      ```

      ```typescript TypeScript theme={null}
      import { ModuleClient, SessionClient } from "tlsclientwrapper";
      import { RoolinkClient } from "@roolink/sdk";

      const PAGE = "https://www.example.com/product/123";
      const ORIGIN = "https://www.example.com";
      const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36";

      const roolink = new RoolinkClient(process.env.ROOLINK_API_KEY!);
      const moduleClient = new ModuleClient();
      const site = new SessionClient(moduleClient, {
        tlsClientIdentifier: "chrome_146",
        withRandomTLSExtensionOrder: true,
        proxyUrl: process.env.PROXY_URL,
      });

      const page = await send("GET", PAGE, pageHeaders());
      const scriptPath = page.body.match(/<script[^>]+src="(\/[^"]+)"[^>]*defer/)![1];
      const scriptUrl = ORIGIN + scriptPath;
      console.log(page.status, scriptPath);
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>3</span>Fetch and parse the script</div>

    <p>Download the script through the same session, using the headers from the script request in your recording. Then hand its body to Roolink's `/parse` endpoint. Roolink reads the site's configuration out of the script and returns a small object, called the script data, that every sensor for this site needs.</p>

    <p>The result stays valid until the site changes its script, so cache it and reuse it across sessions.</p>

    <p>**You should see** an object with `ver`, `key`, `dvc` and `din` fields.</p>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```python Python theme={null}
      script = send("GET", script_url, SCRIPT_HEADERS)
      script_data = roolink.parse_script(script.text)
      print(script_data["ver"])
      ```

      ```go Go theme={null}
      scriptBody := get(site, scriptURL, scriptHeaders())
      scriptData, err := rl.ParseScript(context.Background(), []byte(scriptBody))
      if err != nil {
      	log.Fatal(err)
      }
      fmt.Println(scriptData.Ver)
      ```

      ```typescript TypeScript theme={null}
      const script = await send("GET", scriptUrl, scriptHeaders());
      const scriptData = await roolink.parseScript(script.body);
      console.log(scriptData.ver);
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>4</span>Generate the sensor and post it</div>

    <p>Now ask Roolink for the sensor. Send the page URL, the same User-Agent your session uses, the current `_abck` and `bm_sz` cookies, the script URL and the script data. Set `stepper` to true and pass the attempt number as `index`, starting at 0: that way each sensor continues the same visitor's timeline instead of starting a new one. Roolink returns one long string.</p>

    <p>Post that string to the script path on the site, wrapped as `{"sensor_data": "..."}` with a `text/plain` content type. The site replies `{"success": true}` and sends back an upgraded `_abck` cookie.</p>

    <p>Check the cookie. If it contains `~0~`, you are through. If it still contains `~-1~`, generate and post another sensor with the new cookie values. Three posts is the ceiling: if the cookie has not flipped by then, the problem is the connection, not the sensor. Start with [Troubleshoot a block](/troubleshoot).</p>

    <p>**You should see** `{"success": true}` from the site and a `_abck` value containing `~0~`.</p>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```python Python theme={null}
      for attempt in range(3):
          resp = roolink.generate_web_sensor(WebSensorRequest(
              url=PAGE,
              user_agent=UA,
              abck=site.cookies.get("_abck", ""),
              bm_sz=site.cookies.get("bm_sz", ""),
              script_url=script_url,
              script_data=script_data,
              language="en-US",
              stepper=True,
              index=attempt,
          ))
          posted = send("POST", script_url, SENSOR_HEADERS, json.dumps({"sensor_data": resp.sensor}))
          print(attempt + 1, posted.text)
          if "~0~" in site.cookies.get("_abck", ""):
              break
      ```

      ```go Go theme={null}
      for attempt := 1; attempt <= 3; attempt++ {
      	resp, err := rl.GenerateWebSensor(context.Background(), roolink.WebSensorRequest{
      		URL:        page,
      		UserAgent:  ua,
      		Abck:       cookie(jar, page, "_abck"),
      		BmSz:       cookie(jar, page, "bm_sz"),
      		ScriptURL:  scriptURL,
      		ScriptData: scriptData,
      		Language:   "en-US",
      		Stepper:    true,
      		Index:      attempt - 1,
      	})
      	if err != nil {
      		log.Fatal(err)
      	}
      	body, _ := json.Marshal(map[string]string{"sensor_data": resp.Sensor})
      	fmt.Println(attempt, post(site, scriptURL, sensorHeaders(), string(body)))
      	if strings.Contains(cookie(jar, page, "_abck"), "~0~") {
      		break
      	}
      }
      ```

      ```typescript TypeScript theme={null}
      for (let attempt = 0; attempt < 3; attempt++) {
        const { sensor } = await roolink.generateWebSensor({
          url: PAGE,
          userAgent: UA,
          _abck: await cookie("_abck"),
          bm_sz: await cookie("bm_sz"),
          scriptUrl,
          scriptData,
          language: "en-US",
          stepper: true,
          index: attempt,
        });
        const posted = await send("POST", scriptUrl, sensorHeaders(), JSON.stringify({ sensor_data: sensor }));
        console.log(attempt + 1, posted.body);
        if ((await cookie("_abck")).includes("~0~")) break;
      }
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>5</span>Make the protected request</div>

    <p>Use the same session for the request you came for. Nothing changes except that the site now trusts your `_abck` cookie.</p>

    <p>Most sites invalidate the cookie after a protected action, so repeat step 4 before the next one. The script data from step 3 does not need to be fetched again.</p>

    <p>**You should see** a `200`. That is your first solved challenge.</p>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```python Python theme={null}
      cart = send("POST", ORIGIN + "/cart/add", PAGE_HEADERS, json.dumps({"sku": "123"}))
      print(cart.status_code)
      ```

      ```go Go theme={null}
      fmt.Println(post(site, origin+"/cart/add", pageHeaders(page), `{"sku":"123"}`))
      ```

      ```typescript TypeScript theme={null}
      const cart = await send("POST", ORIGIN + "/cart/add", pageHeaders(), JSON.stringify({ sku: "123" }));
      console.log(cart.status);

      await site.destroySession();
      await moduleClient.terminate();
      ```
    </CodeGroup>
  </div>
</div>

## The complete file

The whole program in one file. The three header sets are left for you to fill from your powhttp recording: one for the page, one for the script, one for the sensor post. Copy each list in the order powhttp shows it. Header order matters as much as the values, and it is the one thing this guide will not give you, because your capture is always more current than any list here.

<CodeGroup>
  ```python quickstart.py theme={null}
  import json, os, re
  import tls_client
  from roolink import RoolinkClient, WebSensorRequest

  PAGE = "https://www.example.com/product/123"
  ORIGIN = "https://www.example.com"
  UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"

  # Paste each header list from your powhttp recording, in the order shown there.
  # Use the same User-Agent as UA above, and PAGE / ORIGIN for referer and origin.
  PAGE_HEADERS = {
      # headers Chrome sent when loading the page
  }

  SCRIPT_HEADERS = {
      # headers Chrome sent when loading the Akamai script
  }

  SENSOR_HEADERS = {
      # headers Chrome sent when posting the sensor; content-type is text/plain;charset=UTF-8
  }

  roolink = RoolinkClient(os.environ["ROOLINK_API_KEY"])
  site = tls_client.Session(client_identifier="chrome_152", random_tls_extension_order=True)
  site.proxies = {"http": os.environ["PROXY_URL"]}


  def send(method, url, headers, data=None):
      """Send headers in exactly the order they are written."""
      site.header_order = list(headers)
      if method == "GET":
          return site.get(url, headers=headers)
      return site.post(url, headers=headers, data=data)


  def main():
      # 2. Load the page and find the script
      page = send("GET", PAGE, PAGE_HEADERS)
      script_path = re.search(r'<script[^>]+src="(/[^"]+)"[^>]*defer', page.text).group(1)
      script_url = ORIGIN + script_path

      # 3. Fetch and parse the script
      script = send("GET", script_url, SCRIPT_HEADERS)
      script_data = roolink.parse_script(script.text)

      # 4. Generate the sensor and post it, up to three times
      for attempt in range(3):
          resp = roolink.generate_web_sensor(WebSensorRequest(
              url=PAGE,
              user_agent=UA,
              abck=site.cookies.get("_abck", ""),
              bm_sz=site.cookies.get("bm_sz", ""),
              script_url=script_url,
              script_data=script_data,
              language="en-US",
              stepper=True,
              index=attempt,
          ))
          send("POST", script_url, SENSOR_HEADERS, json.dumps({"sensor_data": resp.sensor}))
          if "~0~" in site.cookies.get("_abck", ""):
              break

      # 5. Make the protected request
      cart = send("POST", ORIGIN + "/cart/add", PAGE_HEADERS, json.dumps({"sku": "123"}))
      print(cart.status_code)
      site.close()


  main()
  ```

  ```go main.go theme={null}
  package main

  import (
  	"context"
  	"encoding/json"
  	"fmt"
  	"io"
  	"log"
  	"net/url"
  	"os"
  	"regexp"
  	"strings"

  	http "github.com/bogdanfinn/fhttp"
  	tls_client "github.com/bogdanfinn/tls-client"
  	"github.com/bogdanfinn/tls-client/profiles"
  	roolink "github.com/roolinkio/roolink-go"
  )

  const (
  	page   = "https://www.example.com/product/123"
  	origin = "https://www.example.com"
  	ua     = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"
  )

  // Paste each header list from your powhttp recording, in the order shown there,
  // and repeat that order in http.HeaderOrderKey. Use ua for user-agent and
  // page / origin for referer and origin.
  func pageHeaders(referer string) http.Header {
  	h := http.Header{
  		// headers Chrome sent when loading the page
  		http.HeaderOrderKey: {},
  	}
  	if referer != "" {
  		h.Set("referer", referer)
  		h.Set("sec-fetch-site", "same-origin")
  	}
  	return h
  }

  func scriptHeaders() http.Header {
  	return http.Header{
  		// headers Chrome sent when loading the Akamai script
  		http.HeaderOrderKey: {},
  	}
  }

  func sensorHeaders() http.Header {
  	return http.Header{
  		// headers Chrome sent when posting the sensor; content-type is text/plain;charset=UTF-8
  		http.HeaderOrderKey: {},
  	}
  }

  func get(c tls_client.HttpClient, u string, h http.Header) string {
  	req, _ := http.NewRequest(http.MethodGet, u, nil)
  	req.Header = h
  	return do(c, req)
  }

  func post(c tls_client.HttpClient, u string, h http.Header, body string) string {
  	req, _ := http.NewRequest(http.MethodPost, u, strings.NewReader(body))
  	req.Header = h
  	return do(c, req)
  }

  func do(c tls_client.HttpClient, req *http.Request) string {
  	resp, err := c.Do(req)
  	if err != nil {
  		log.Fatal(err)
  	}
  	defer resp.Body.Close()
  	b, _ := io.ReadAll(resp.Body)
  	fmt.Println(req.Method, req.URL.Path, resp.StatusCode)
  	return string(b)
  }

  func cookie(jar http.CookieJar, rawURL, name string) string {
  	u, _ := url.Parse(rawURL)
  	for _, c := range jar.Cookies(u) {
  		if c.Name == name {
  			return c.Value
  		}
  	}
  	return ""
  }

  func main() {
  	rl := roolink.NewClient(os.Getenv("ROOLINK_API_KEY"))

  	jar := tls_client.NewCookieJar()
  	site, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
  		tls_client.WithClientProfile(profiles.Chrome_152),
  		tls_client.WithRandomTLSExtensionOrder(),
  		tls_client.WithCookieJar(jar),
  		tls_client.WithProxyUrl(os.Getenv("PROXY_URL")),
  	)
  	if err != nil {
  		log.Fatal(err)
  	}

  	// 2. Load the page and find the script
  	html := get(site, page, pageHeaders(""))
  	scriptPath := regexp.MustCompile(`<script[^>]+src="(/[^"]+)"[^>]*defer`).FindStringSubmatch(html)[1]
  	scriptURL := origin + scriptPath

  	// 3. Fetch and parse the script
  	scriptBody := get(site, scriptURL, scriptHeaders())
  	scriptData, err := rl.ParseScript(context.Background(), []byte(scriptBody))
  	if err != nil {
  		log.Fatal(err)
  	}

  	// 4. Generate the sensor and post it, up to three times
  	for attempt := 1; attempt <= 3; attempt++ {
  		resp, err := rl.GenerateWebSensor(context.Background(), roolink.WebSensorRequest{
  			URL:        page,
  			UserAgent:  ua,
  			Abck:       cookie(jar, page, "_abck"),
  			BmSz:       cookie(jar, page, "bm_sz"),
  			ScriptURL:  scriptURL,
  			ScriptData: scriptData,
  			Language:   "en-US",
  			Stepper:    true,
  			Index:      attempt - 1,
  		})
  		if err != nil {
  			log.Fatal(err)
  		}
  		body, _ := json.Marshal(map[string]string{"sensor_data": resp.Sensor})
  		post(site, scriptURL, sensorHeaders(), string(body))
  		if strings.Contains(cookie(jar, page, "_abck"), "~0~") {
  			break
  		}
  	}

  	// 5. Make the protected request
  	post(site, origin+"/cart/add", pageHeaders(page), `{"sku":"123"}`)
  }
  ```

  ```typescript quickstart.ts theme={null}
  import { ModuleClient, SessionClient } from "tlsclientwrapper";
  import { RoolinkClient } from "@roolink/sdk";

  const PAGE = "https://www.example.com/product/123";
  const ORIGIN = "https://www.example.com";
  const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36";

  // Paste each header list from your powhttp recording, in the order shown there.
  // Use UA for user-agent and PAGE / ORIGIN for referer and origin.
  const pageHeaders = () => ({
    // headers Chrome sent when loading the page
  });

  const scriptHeaders = () => ({
    // headers Chrome sent when loading the Akamai script
  });

  const sensorHeaders = () => ({
    // headers Chrome sent when posting the sensor; content-type is text/plain;charset=UTF-8
  });

  const roolink = new RoolinkClient(process.env.ROOLINK_API_KEY!);
  const moduleClient = new ModuleClient();
  const site = new SessionClient(moduleClient, {
    tlsClientIdentifier: "chrome_146",
    withRandomTLSExtensionOrder: true,
    proxyUrl: process.env.PROXY_URL,
  });

  // Send headers in exactly the order they are written.
  async function send(method: "GET" | "POST", url: string, headers: Record<string, string>, body?: string) {
    const options = { headers, headerOrder: Object.keys(headers) };
    return method === "GET" ? site.get(url, options) : site.post(url, body ?? null, options);
  }

  // Read one cookie from the session's jar for the page's domain.
  async function cookie(name: string): Promise<string> {
    const res = await site.getCookiesFromSession(site.getSession(), PAGE);
    return res.cookies?.find((c) => c.name === name)?.value ?? "";
  }

  // 2. Load the page and find the script
  const page = await send("GET", PAGE, pageHeaders());
  const scriptPath = page.body.match(/<script[^>]+src="(\/[^"]+)"[^>]*defer/)![1];
  const scriptUrl = ORIGIN + scriptPath;

  // 3. Fetch and parse the script
  const script = await send("GET", scriptUrl, scriptHeaders());
  const scriptData = await roolink.parseScript(script.body);

  // 4. Generate the sensor and post it, up to three times
  for (let attempt = 0; attempt < 3; attempt++) {
    const { sensor } = await roolink.generateWebSensor({
      url: PAGE,
      userAgent: UA,
      _abck: await cookie("_abck"),
      bm_sz: await cookie("bm_sz"),
      scriptUrl,
      scriptData,
      language: "en-US",
      stepper: true,
      index: attempt,
    });
    await send("POST", scriptUrl, sensorHeaders(), JSON.stringify({ sensor_data: sensor }));
    if ((await cookie("_abck")).includes("~0~")) break;
  }

  // 5. Make the protected request
  const cart = await send("POST", ORIGIN + "/cart/add", pageHeaders(), JSON.stringify({ sku: "123" }));
  console.log(cart.status);

  await site.destroySession();
  await moduleClient.terminate();
  ```
</CodeGroup>

## If something goes wrong

<AccordionGroup>
  <Accordion title="The very first page request is blocked or returns an Akamai error page">
    Roolink is not involved yet. This is the connection itself: an HTTP library that does not look like Chrome, headers in the wrong order, or a proxy that changed IP. Route your script through powhttp and compare its requests with Chrome's from your recording, header by header. [Troubleshoot a block](/troubleshoot) matches each symptom to its cause.
  </Accordion>

  <Accordion title="Roolink answers 401">
    The `x-api-key` header is missing or the key is wrong. Check the environment variable and that the SDK was created with it.
  </Accordion>

  <Accordion title="Roolink answers 422 with `no device profile for user agent`">
    Roolink builds sensors from real browser profiles and needs a User-Agent it has one for. Current desktop Chrome is the safe choice; send the identical string to the site. See [Supported browsers](/supported-browsers).
  </Accordion>

  <Accordion title="The site says success but _abck never contains ~0~">
    The sensor was accepted but the connection is not trusted. Compare your page, script and sensor requests with Chrome's in powhttp; header order and the `sec-fetch-*` values are the usual culprits. Some sites never flip the cookie to `~0~`; on those, post three sensors and proceed to step 5.
  </Accordion>

  <Accordion title="The protected request fails even though `_abck` is valid">
    Check whether the site also runs SBSD, Sec-CPT or a pixel request in a browser; each has its own guide under Akamai Web. Confirm the IP did not change between steps, and keep every cookie the site set, not only `_abck`.
  </Accordion>
</AccordionGroup>

## Next steps

<div className="rl-rows">
  <div className="rl-row"><a href="/troubleshoot">Troubleshoot a block</a><span>Which layer said no first, and what to change.</span></div>
  <div className="rl-row"><a href="/akamai-web/sensor-data">Sensor data</a><span>The flow in depth: cookies, script rotation, stepper mode and every field.</span></div>
  <div className="rl-row"><a href="/akamai-web/sbsd">SBSD</a><span>The second check many Akamai sites run alongside the sensor.</span></div>
  <div className="rl-row"><a href="/akamai-web/sec-cpt">Sec-CPT</a><span>The proof-of-work challenge behind a 428.</span></div>
</div>
