> ## 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.

# Make your first app request

> Emulate one phone, get a sensor for it, and make a request the app's servers accept. About fifteen minutes.

<div className="rl-intro">
  <p>**What you will build.** A small program that asks Roolink for a mobile device and its sensor, connects to the app's servers the way that phone would, and makes one request that Akamai accepts. Along the way you keep the device so you can use it again.</p>

  <p>**What you need.** A Roolink account with an API key, the **app key** for your target app, a residential proxy that keeps the same IP for a session, Python, Go or Node, and a capture of the real app's traffic so you can copy its headers.</p>
</div>

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

    <p>Put your Roolink key, your app key and your proxy in environment variables. Install the Roolink SDK and a TLS client, then get the Roolink TLS profiles: a Go module for Go, and a `profiles.json` file for Python and Node. Both live in the same repository, [roolinkio/tlsprofiles](https://github.com/roolinkio/tlsprofiles).</p>

    <p>Before you write code, record the real app once. Run the app on a phone routed through [powhttp](/capture-requests) and perform the request you want to automate. You will copy its headers, in order, in step 4.</p>
  </div>

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

      pip install roolink typing_extensions git+https://github.com/Nintendocustom/Python-Tls-Client.git
      # copy profiles.json from github.com/roolinkio/tlsprofiles into your project
      ```

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

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

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

      npm install @roolink/sdk tlsclientwrapper
      # copy profiles.json from github.com/roolinkio/tlsprofiles into your project
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>2</span>Ask Roolink for a device and a sensor</div>

    <p>One call does both. Send the app key, the proxy and the language you want the phone to have. Roolink initializes a device, runs Akamai's SDK handshake through your proxy, and returns the sensor plus everything about the device it emulated: the iOS version, the hardware identifier, the screen size, the cookies the SDK collected, and the `deviceId`.</p>

    <p>Leave `android` unset for an iPhone. Set `android: true` for an Android device or `ipad: true` for an iPad.</p>

    <p>**You should see** a response with a long `sensor` string, an `ios` version such as `26.2`, and a `deviceId`.</p>
  </div>

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

      rl = RoolinkClient(os.environ["ROOLINK_API_KEY"])

      dev = rl.generate_bmp_sensor(BMPSensorRequest(
      app=os.environ["ROOLINK_APP"],
      proxy=os.environ["PROXY_URL"],
      language="en-US",
      ))
      print(dev.ios, dev.machine_id, dev.device_id)
      ```

      ```go Go theme={null}
      rl := roolink.NewClient(os.Getenv("ROOLINK_API_KEY"))

      dev, err := rl.GenerateBMPSensor(context.Background(), roolink.BMPSensorRequest{
      	AppName:  os.Getenv("ROOLINK_APP"),
      	Proxy:    os.Getenv("PROXY_URL"),
      	Language: "en-US",
      })
      if err != nil {
      	log.Fatal(err)
      }
      fmt.Println(dev.IOS, dev.MachineID, dev.DeviceID)
      ```

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

      const rl = new RoolinkClient(process.env.ROOLINK_API_KEY!);

      const dev = await rl.generateBMPSensor({
        app: process.env.ROOLINK_APP!,
        proxy: process.env.PROXY_URL!,
        language: "en-US",
      });
      console.log(dev.ios, dev.machineId, dev.deviceId);
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>3</span>Connect like that phone</div>

    <p>Create a client with the TLS profile for the device's iOS version, the same proxy you gave Roolink, and the cookies from the response. Use the Standard profile first; the [TLS profiles](/akamai-bmp/tls-profiles) page explains when to switch to Secondary.</p>

    <p>The proxy must be the same one. Roolink ran the SDK handshake through it, so Akamai already associates this device with that IP.</p>

    <p>In Python and Node the profile is an entry from `profiles.json`, passed as a custom TLS client. The profiles fix the extension order themselves, so leave randomization off, and disable HTTP/3, which the app never uses.</p>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```python Python theme={null}
      PROFILES = json.load(open("profiles.json"))

      app = AppSession(PROFILES[profile_for(dev.ios)], os.environ["PROXY_URL"])
      seed_cookies(app, dev.cookies or [])
      ```

      ```go Go theme={null}
      jar := tls_client.NewCookieJar()
      app, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
      	tls_client.WithClientProfile(profileFor(dev.IOS)),
      	tls_client.WithCookieJar(jar),
      	tls_client.WithProxyUrl(os.Getenv("PROXY_URL")),
      	tls_client.WithDisableHttp3(),
      )
      if err != nil {
      	log.Fatal(err)
      }
      seedCookies(jar, dev.Cookies)
      ```

      ```typescript TypeScript theme={null}
      const profiles = JSON.parse(readFileSync("profiles.json", "utf8"));
      const moduleClient = new ModuleClient();

      const app = new SessionClient(moduleClient, {
        customTlsClient: profiles[profileFor(dev.ios)],
        withRandomTLSExtensionOrder: false,
        disableHttp3: true,
        proxyUrl: process.env.PROXY_URL,
        timeoutSeconds: 30,
      });
      await seedCookies(app, dev.cookies ?? []);
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>4</span>Make the app's request</div>

    <p>Build the request the app made in your capture: same URL, same headers, same order. Put the sensor in the `X-acf-sensor-data` header. Wherever the app's headers describe the phone, use the values from the response instead of the ones in your capture: the OS version, the hardware identifier, the app version, the screen size. The device Roolink emulated and the device your headers describe must be the same phone.</p>

    <p>**You should see** the response a real user would get. An Akamai block usually arrives as a `403` or a `429` with an Akamai reference in the body.</p>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```python Python theme={null}
      HEADERS = {
      # Paste the app's headers from your capture, in order.
      # Replace device values with dev.ios, dev.machine_id, dev.app_version.
      "x-acf-sensor-data": dev.sensor,
      }
      app.header_order = list(HEADERS)
      resp = app.post(os.environ["APP_ENDPOINT"], headers=HEADERS, data=json.dumps({"example": True}))
      print(resp.status_code)
      ```

      ```go Go theme={null}
      req, _ := http.NewRequest(http.MethodPost, os.Getenv("APP_ENDPOINT"), strings.NewReader(body))
      req.Header = http.Header{
      	// Paste the app's headers from your capture, in order.
      	// Replace device values with dev.IOS, dev.MachineID, dev.AppVersion.
      	"x-acf-sensor-data": {dev.Sensor},
      	http.HeaderOrderKey: {},
      }
      resp, err := app.Do(req)
      if err != nil {
      	log.Fatal(err)
      }
      fmt.Println(resp.StatusCode)
      ```

      ```typescript TypeScript theme={null}
      const headers: Record<string, string> = {
        // Paste the app's headers from your capture, in order.
        // Replace device values with dev.ios, dev.machineId, dev.appVersion.
        "x-acf-sensor-data": dev.sensor,
      };
      const resp = await app.post(process.env.APP_ENDPOINT!, {
        headers,
        headerOrder: Object.keys(headers),
        body: JSON.stringify({ example: true }),
      });
      console.log(resp.status);
      ```
    </CodeGroup>
  </div>
</div>

<div className="rl-step">
  <div className="rl-step-prose">
    <div className="rl-step-title"><span>5</span>Keep the device</div>

    <p>Save the `deviceId`, the cookies and the proxy URL together. Next time, pass the `deviceId` to `/sensor` and Roolink continues the same device: faster, and with a fingerprint Akamai has already seen. Keep using one device until the app blocks it, then start a fresh one. [Sessions](/akamai-bmp/sessions) explains what belongs together and how long a device lives.</p>
  </div>

  <div className="rl-step-code">
    <CodeGroup>
      ```python Python theme={null}
      again = rl.generate_bmp_sensor(BMPSensorRequest(
      app=os.environ["ROOLINK_APP"],
      proxy=os.environ["PROXY_URL"],
      language="en-US",
      device_id=dev.device_id,
      ))
      print(again.device_id == dev.device_id)
      ```

      ```go Go theme={null}
      next, err := rl.GenerateBMPSensor(context.Background(), roolink.BMPSensorRequest{
      	AppName:  os.Getenv("ROOLINK_APP"),
      	Proxy:    os.Getenv("PROXY_URL"),
      	Language: "en-US",
      	DeviceID: dev.DeviceID,
      })
      if err != nil {
      	log.Fatal(err)
      }
      fmt.Println(next.DeviceID == dev.DeviceID)
      ```

      ```typescript TypeScript theme={null}
      const again = await rl.generateBMPSensor({
        app: process.env.ROOLINK_APP!,
        proxy: process.env.PROXY_URL!,
        language: "en-US",
        deviceId: dev.deviceId,
      });
      console.log(again.deviceId === dev.deviceId);
      ```
    </CodeGroup>
  </div>
</div>

## The complete file

The helpers pick the TLS profile from the `ios` version and copy Roolink's cookies into the client. In Python, `AppSession` also adapts the library's custom-client payload to the current shared library; [TLS profiles](/akamai-bmp/tls-profiles) explains why.

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

  PROFILES = json.load(open("profiles.json"))


  class AppSession(tls_client.Session):
      """A tls_client.Session driven by one entry of profiles.json."""

      def __init__(self, spec, proxy):
          self._spec = spec
          self.trust_anchors_payload = None
          super().__init__(
              client_identifier=None,
              ja3_string=spec["ja3String"],
              h2_settings=spec["h2Settings"],
              h2_settings_order=spec["h2SettingsOrder"],
              pseudo_header_order=spec["pseudoHeaderOrder"],
              connection_flow=spec["connectionFlow"],
              supported_signature_algorithms=spec["supportedSignatureAlgorithms"],
              supported_versions=spec["supportedVersions"],
              key_share_curves=spec["keyShareCurves"],
              disable_http3=True,
          )
          self.proxies = {"http": proxy}
          self.timeout = 30

      def _build_request_payload(self, *args, **kwargs):
          payload = super()._build_request_payload(*args, **kwargs)
          custom = payload["customTlsClient"]
          custom["alpnProtocols"] = self._spec["alpnProtocols"]
          custom["certCompressionAlgos"] = self._spec["certCompressionAlgos"]
          custom.pop("certCompressionAlgo", None)
          return payload


  def profile_for(ios: str) -> str:
      """Standard profile for the device's iOS version. Switch to the Secondary
      variants if the app rejects the session."""
      if ios.startswith("26.") and not ios.startswith(("26.0", "26.1")):
          return "StandardIOS26_2"
      if ios.startswith("26."):
          return "StandardIOS26"
      return "StandardIOS"


  def seed_cookies(session, cookies):
      by_domain = {}
      for c in cookies:
          by_domain.setdefault(c.domain, []).append({"name": c.name, "value": c.value, "domain": c.domain, "path": "/"})
      for domain, items in by_domain.items():
          session.add_cookies_to_session("https://" + domain.lstrip("."), items)


  def main():
      rl = RoolinkClient(os.environ["ROOLINK_API_KEY"])

      # 2. Ask Roolink for a device and a sensor
      dev = rl.generate_bmp_sensor(BMPSensorRequest(
          app=os.environ["ROOLINK_APP"],
          proxy=os.environ["PROXY_URL"],
          language="en-US",
      ))

      # 3. Connect like that phone
      app = AppSession(PROFILES[profile_for(dev.ios)], os.environ["PROXY_URL"])
      seed_cookies(app, dev.cookies or [])

      # 4. Make the app's request
      headers = {
          # Paste the app's headers from your capture, in order. Replace device
          # values with dev.ios, dev.machine_id and dev.app_version so the
          # headers describe this device.
          "x-acf-sensor-data": dev.sensor,
      }
      app.header_order = list(headers)
      resp = app.post(os.environ["APP_ENDPOINT"], headers=headers, data=json.dumps({"example": True}))
      print("app responded", resp.status_code)

      # 5. Keep the device: reuse it on the next sensor call
      again = rl.generate_bmp_sensor(BMPSensorRequest(
          app=os.environ["ROOLINK_APP"],
          proxy=os.environ["PROXY_URL"],
          language="en-US",
          device_id=dev.device_id,
      ))
      print("same device:", again.device_id == dev.device_id)
      app.close()


  main()
  ```

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

  import (
  	"context"
  	"fmt"
  	"log"
  	"net/url"
  	"os"
  	"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"
  	"github.com/roolinkio/tlsprofiles"
  )

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

  	// 2. Ask Roolink for a device and a sensor
  	dev, err := rl.GenerateBMPSensor(context.Background(), roolink.BMPSensorRequest{
  		AppName:  os.Getenv("ROOLINK_APP"),
  		Proxy:    os.Getenv("PROXY_URL"),
  		Language: "en-US",
  	})
  	if err != nil {
  		log.Fatal(err)
  	}

  	// 3. Connect like that phone
  	jar := tls_client.NewCookieJar()
  	app, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
  		tls_client.WithClientProfile(profileFor(dev.IOS)),
  		tls_client.WithCookieJar(jar),
  		tls_client.WithProxyUrl(os.Getenv("PROXY_URL")),
  		tls_client.WithDisableHttp3(),
  	)
  	if err != nil {
  		log.Fatal(err)
  	}
  	seedCookies(jar, dev.Cookies)

  	// 4. Make the app's request
  	body := `{"example": true}`
  	req, _ := http.NewRequest(http.MethodPost, os.Getenv("APP_ENDPOINT"), strings.NewReader(body))
  	req.Header = http.Header{
  		// Paste the app's headers from your capture, in order, and repeat the
  		// order in http.HeaderOrderKey. Replace device values with dev.IOS,
  		// dev.MachineID and dev.AppVersion so the headers describe this device.
  		"x-acf-sensor-data": {dev.Sensor},
  		http.HeaderOrderKey: {},
  	}
  	resp, err := app.Do(req)
  	if err != nil {
  		log.Fatal(err)
  	}
  	defer resp.Body.Close()
  	fmt.Println("app responded", resp.StatusCode)

  	// 5. Keep the device: reuse it on the next sensor call
  	next, err := rl.GenerateBMPSensor(context.Background(), roolink.BMPSensorRequest{
  		AppName:  os.Getenv("ROOLINK_APP"),
  		Proxy:    os.Getenv("PROXY_URL"),
  		Language: "en-US",
  		DeviceID: dev.DeviceID,
  	})
  	if err != nil {
  		log.Fatal(err)
  	}
  	fmt.Println("same device:", next.DeviceID == dev.DeviceID)
  }

  // profileFor returns the Standard profile for the device's iOS version.
  // Switch to the Secondary variants if the app rejects the session.
  func profileFor(ios string) profiles.ClientProfile {
  	switch {
  	case strings.HasPrefix(ios, "26.") && !strings.HasPrefix(ios, "26.0") && !strings.HasPrefix(ios, "26.1"):
  		return tlsprofiles.StandardIOS26_2
  	case strings.HasPrefix(ios, "26."):
  		return tlsprofiles.StandardIOS26
  	default:
  		return tlsprofiles.StandardIOS
  	}
  }

  func seedCookies(jar tls_client.CookieJar, cookies []roolink.Cookie) {
  	byDomain := map[string][]*http.Cookie{}
  	for _, c := range cookies {
  		byDomain[c.Domain] = append(byDomain[c.Domain], &http.Cookie{Name: c.Name, Value: c.Value, Domain: c.Domain, Path: "/"})
  	}
  	for domain, list := range byDomain {
  		u, _ := url.Parse("https://" + strings.TrimPrefix(domain, "."))
  		jar.SetCookies(u, list)
  	}
  }
  ```

  ```typescript main.ts theme={null}
  import { readFileSync } from "node:fs";
  import { ModuleClient, SessionClient } from "tlsclientwrapper";
  import { RoolinkClient } from "@roolink/sdk";
  import type { Cookie } from "@roolink/sdk";

  const profiles = JSON.parse(readFileSync("profiles.json", "utf8"));

  // Standard profile for the device's iOS version. Switch to the Secondary
  // variants if the app rejects the session.
  function profileFor(ios: string): string {
    if (ios.startsWith("26.") && !ios.startsWith("26.0") && !ios.startsWith("26.1")) return "StandardIOS26_2";
    if (ios.startsWith("26.")) return "StandardIOS26";
    return "StandardIOS";
  }

  async function seedCookies(session: InstanceType<typeof SessionClient>, cookies: Cookie[]) {
    const byDomain = new Map<string, Cookie[]>();
    for (const c of cookies) byDomain.set(c.domain, [...(byDomain.get(c.domain) ?? []), c]);
    for (const [domain, list] of byDomain) {
      await session.addCookiesToSession(session.getSession(), `https://${domain.replace(/^\./, "")}`,
        list.map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: "/" })));
    }
  }

  const rl = new RoolinkClient(process.env.ROOLINK_API_KEY!);

  // 2. Ask Roolink for a device and a sensor
  const dev = await rl.generateBMPSensor({
    app: process.env.ROOLINK_APP!,
    proxy: process.env.PROXY_URL!,
    language: "en-US",
  });

  // 3. Connect like that phone
  const moduleClient = new ModuleClient();
  const app = new SessionClient(moduleClient, {
    customTlsClient: profiles[profileFor(dev.ios ?? "")],
    withRandomTLSExtensionOrder: false,
    disableHttp3: true,
    proxyUrl: process.env.PROXY_URL,
    timeoutSeconds: 30,
  });
  await seedCookies(app, dev.cookies ?? []);

  // 4. Make the app's request
  const headers: Record<string, string> = {
    // Paste the app's headers from your capture, in order. Replace device
    // values with dev.ios, dev.machineId and dev.appVersion so the headers
    // describe this device.
    "x-acf-sensor-data": dev.sensor,
  };
  const resp = await app.post(process.env.APP_ENDPOINT!, {
    headers,
    headerOrder: Object.keys(headers),
    body: JSON.stringify({ example: true }),
  });
  console.log("app responded", resp.status);

  // 5. Keep the device: reuse it on the next sensor call
  const again = await rl.generateBMPSensor({
    app: process.env.ROOLINK_APP!,
    proxy: process.env.PROXY_URL!,
    language: "en-US",
    deviceId: dev.deviceId,
  });
  console.log("same device:", again.deviceId === dev.deviceId);

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

## If something goes wrong

<AccordionGroup>
  <Accordion title="Roolink answers 400 or 422 on the sensor call">
    Read the `error` field. The usual causes are an app key that is not enabled on your account, a proxy URL without a scheme, or a `deviceId` from the other platform.
  </Accordion>

  <Accordion title="Roolink answers 429">
    Akamai BMP is not enabled on your key or its quota is used up. Check the plan in the [dashboard](https://www.roolink.io/dashboard).
  </Accordion>

  <Accordion title="The app's server blocks the request">
    Work through these in order. Same proxy for Roolink and for the app request. TLS profile matches the `ios` version, and you have tried the Secondary variant. Headers match your capture in order, and the device values in them come from the response. Cookies from the response are in the client. Sensor is fresh: generate a new one for each protected request.
  </Accordion>

  <Accordion title="The connection fails or falls back to HTTP/1.1 in Python or Node">
    The profile did not apply. In Node, confirm `customTlsClient` is set and `withRandomTLSExtensionOrder` is `false`. In Python, confirm you construct the client through `AppSession` and not `tls_client.Session` directly. A fingerprint echo such as `https://tls.peet.ws/api/all` should report `h2` and a JA4 that starts with `t13d`.
  </Accordion>

  <Accordion title="The device disappears between runs">
    Devices are cached for a limited time after each use. Pass the `deviceId` regularly to keep it alive, and expect to start fresh after a long pause. See [Sessions](/akamai-bmp/sessions).
  </Accordion>
</AccordionGroup>

## Next steps

<div className="rl-rows">
  <div className="rl-row"><a href="/akamai-bmp/sessions">Sessions</a><span>What makes up a device session, how long it lives, when to reuse and when to rotate.</span></div>
  <div className="rl-row"><a href="/akamai-bmp/tls-profiles">TLS profiles</a><span>Standard and Secondary, per iOS version, in Python, Go and Node, and the Android profile.</span></div>
  <div className="rl-row"><a href="/api-reference/akamai-bmp/sensor">API reference</a><span>Every field of the sensor request and response.</span></div>
</div>
