On Radisson, yes. The prices come from a JSON rates API. A scraper that sends only the page, Akamai and API requests, with Chrome's TLS fingerprint and header order, returned rates for 490 of 500 planned searches in our test.
How to Scrape Hotel Prices Without a Browser: Reverse Engineering Radisson's Request Flow
A browser is the easy way to scrape a site protected by bot detection. It is also the slowest and the most expensive one. In this post we build the other kind of scraper for Radisson Hotels: one that sends only the HTTP requests that return the data, and gets past Akamai Bot Manager without ever running a browser.
Radisson is a good test case for hotel price scraping. The prices aren't in the HTML, they come from a JSON API that the page calls after it loads, and the site runs three of Akamai's checks before it lets those calls through.
We'll show how we got there. We recorded one real browser session, found the four API calls that matter among 148 requests to Radisson, worked out which of Akamai's checks the site runs and where, and rebuilt the whole flow in Go. Then we ran it through Massive residential proxies in the UK.
Key Takeaways
- Radisson's search results come from four JSON endpoints. Two of them, the city lookup and the rates call, are where Akamai blocks a bad session.
- The site runs three Akamai checks on page load: SBSD, the
_abcksensor and a pixel challenge. A request-based scraper has to answer all three, in the order the page does. - Over 100 sessions through Massive UK residential IPs, every session cleared Akamai, and 490 of 500 planned searches returned rates (98%).
- Clearing Akamai cost 520 KB of proxy traffic per session, and three quarters of that was two JavaScript files. A search after that cost 12.5 KB.
- A session that clears Akamai keeps working. Five sessions ran 100 searches each without being stopped once, at 18 KB per search all in.
Why request-based
A request-based scraper skips everything a browser does that the scraper doesn't need: rendering, images, fonts, maps, analytics and the page's own JavaScript. What's left is a handful of HTTP requests, so it's faster, it runs hundreds of sessions on one machine, and it moves far less traffic through your proxies.
The catch is that you have to know exactly which requests to send. Bot detection doesn't only look at what you request. It looks at how: the TLS handshake, the order of the HTTP/2 headers, the cookies and the JavaScript challenges a real browser answers in the background. So the work is in reverse engineering the request flow first.
Step 1: Record a real session
We started by doing a search on Radisson in Chrome, with the traffic going through powhttp, an HTTP debugging proxy. We opened the home page, typed "Amsterdam", picked dates and searched.
We use powhttp rather than the browser's DevTools for one reason. DevTools shows you a cleaned up version of each request. powhttp shows what actually went over the wire: the real header order, the HTTP/2 pseudo-headers, the TLS fingerprint and every cookie header the browser sent. Those details are exactly what bot detection checks, and what a scraper has to copy.
That one search produced 568 requests to 46 hosts. Most were noise: Google Maps, analytics, a consent banner, images. Filtering on Radisson's own domains left 148 requests: JavaScript bundles, images, Akamai's scripts, and 38 calls to JSON APIs.
Step 2: Find the requests that return the hotel prices
The hotel prices weren't in the HTML. The results page is a JavaScript application that loads them after it renders, so we went through the 38 API calls and looked at what each one returned. Four of them carry everything a search needs:
GET /zimba-api/autocomplete/amsterdamreturns the place you typed, as a Google place ID.GET /zimba-api/search-result/hotels/search?placeId=…returns the codes of the Radisson hotels for that place, with distances.POST /availability-api/multiple-hotels-ratesreturns the cheapest rate of each hotel for the dates and guests.GET /zimba-api/search-result/hotels?code=…returns a hotel's name, address, stars and review score.
The rates call is the one that matters. It takes the dates, the guests and the list of hotel codes as a JSON body, and returns a price per hotel, the total with tax, and how many rooms are left.
The other 34 calls load labels, currencies, exchange rates, phone numbers and loyalty content for the page. A scraper doesn't need any of them.
Step 3: Copy the requests exactly
Next we looked at the headers of the four calls. Most are what any Chrome fetch sends. A few are specific to Radisson:
x-source-of-business: desktopon every call, andx-version: V2andx-currencyon the rates call.transactionid, a 40-character value that's the same on every call in one visit.
The transactionid looked like it could be a token from the server. It isn't. Searching the site's JavaScript in powhttp for transactionId found the function that makes it: 40 random digits in base 15, created on the first page load and kept in the browser's session storage. So our scraper generates one per session the same way.
Then we copied the header order. Chrome sends headers in a fixed order that differs by request type: a page load, a script, a fetch() and a form post each have their own. We copied each order from the recording, including where the cookie header goes.
For the connection itself we used tls-client for Go, which opens connections with Chrome's TLS and HTTP/2 fingerprint. The standard Go HTTP client has a TLS fingerprint no browser has, and the TLS fingerprint is one of the first things bot detection looks at.
Step 4: Find where Akamai sits
With the data requests known, the next question was what Akamai needs before it lets them through. The answer is in the home page HTML and in the requests the page makes right after it loads.
The page includes three Akamai scripts:
- The sensor script, at a long random-looking path such as
/hLJOkaPJ/…/XMmkuCU0. It collects data about the browser and posts it back to the same path, three times. Each answer updates the_abckcookie, and once the cookie contains~0~, Akamai considers the session valid. - The SBSD script, at a similar path with a
?v=parameter. SBSD (State Based Scraping Detection) is a second Akamai check. The page posts payloads to the same path while it runs, three times in our recording, and each answer updates thebm_scookie. Our scraper posts two, which was enough. - The pixel script, at
/akam/13/…, with a number written into the page. The page answers it once with a form post to the same path withpixel_in front.
Two details matter here. The script paths change, so a scraper has to read them from every home page it loads rather than hardcode them. During our tests the SBSD path changed several times. And on some IPs, the home page isn't the home page at all. It's a short SBSD challenge page with a t= parameter on the script. The scraper has to answer that challenge and load the page again before it can do anything else.
None of these checks can be passed by copying a recorded payload. Each one is generated from the script, the cookies, the user agent and the IP of the current session. We generate them with the Hyper Solutions API: the scraper downloads the script through its proxy, sends it to the API with the session's cookies and IP, and posts the payload the API returns. We used our Go SDK for that.
How each of these checks works on its own, with SDK code in Go, Python and JavaScript, is covered in our guide to Akamai without a headless browser.
Step 5: Check your scraper against the browser
Before scaling up, we pointed the scraper at powhttp too, so its requests landed next to the browser's in the same tool. That makes the comparison direct:
- The TLS fingerprint matched Chrome's. The JA4 hashes of the browser and the scraper differed only in the random GREASE value Chrome inserts on purpose.
- The header order matched on every request type.
- The cookie header was split into one header per cookie, as Chrome does over HTTP/2.
If something is off, this is where you find it, and it's much cheaper to find it here than in a block rate later.
The finished flow
Put together, one scraping session does this:
- Look up its own exit IP, because the Akamai payloads are generated for it.
- Load the home page. If it's the SBSD challenge page, answer the challenge and load it again.
- Download the SBSD script and post two SBSD payloads.
- Download the sensor script and post sensor data until
_abckcontains~0~, answering the pixel challenge after the first post. - Search: look up the city once, then for each search fetch the hotel list and post the rates request.
A session can keep searching after that. If Akamai answers a protected call with a challenge, the session answers it and retries once.
The code is open source: github.com/Hyper-Solutions/radisson-scraper. It's a Go package and a small command-line tool that prints the hotels and rates for a city as JSON:
Setting up the Massive proxies
Every session needs its own residential IP, and it has to keep that IP until it's done. Akamai's payloads are generated for one IP, so if the IP changes in the middle of a session, the session is no longer valid.
Massive's dashboard has a Proxy Generator that builds the connection string for you. We picked residential proxies over HTTP and set the rotation type to Sticky, with a session TTL of 30 minutes.
Then, under Geotargeting, we picked the United Kingdom. The generator puts all of it in the proxy username:
country-GBpicks UK residential IPs.session-<id>makes the session sticky. Every request with the same ID leaves from the same IP, and a new ID gets a new IP. The generator fills in a random ID; our scraper puts a new one in for each session.sessionttl-30keeps that IP for the session for 30 minutes, longer than any of our sessions ran.
The results
We ran 100 sessions through Massive UK residential IPs, 10 at a time. Each session cleared Akamai and then ran five searches, each for a random city from a list of 16 and random dates two to 12 weeks out. A counting proxy on our machine measured every byte between the scraper and Massive. We ran the test on October 22, 2026.
- Sessions that cleared Akamai: 100 of 100.
- Sessions that completed all five searches: 98 of 100.
- Searches that returned rates: 490 of 500 planned (98%).
- Median time to clear Akamai: 2.6 seconds.
- Median time per search: 1.5 seconds.
The two sessions that failed both cleared Akamai and were then refused on their first rates call. Both were blocked for good: clearing Akamai again on the same IP didn't help. That's an IP Akamai doesn't trust, and the fix is a new session on a new IP.
What it costs in proxy traffic
Clearing Akamai, which we call the bootstrap, is by far the most expensive part of a session: 520 KB. A search costs 12.5 KB: 5.2 KB for the hotel list and 7.2 KB for the rates. Looking up a city costs 6.4 KB, once per city.
Almost all of the bootstrap is two downloads: the SBSD script (189 KB) and the sensor script (195 KB). Together they're 74% of it. The home page is 64 KB, and all the posts together are under 60 KB.
With five searches per session, that works out to 122 KB of proxy traffic per search, everything included. The longer a session lives, the less the bootstrap weighs. In a second test, five sessions each ran 100 searches on a single bootstrap. All 500 searches returned rates, Akamai never stopped any of them, and the traffic came to 18 KB per search, everything included.
Practical tips
- Record with a tool that shows the wire. Header order, HTTP/2 pseudo-headers and the TLS fingerprint aren't visible in DevTools. They're what bot detection checks.
- Look for the JSON, not the HTML. If a page is a JavaScript application, its data comes from an API. Calling that API directly is cheaper and more stable than parsing the page.
- Read the site's JavaScript before you guess. Values like
transactionidlook like server tokens and turn out to be random. A search through the bundle answers that in minutes. - Parse Akamai's script paths every time. They change.
- Verify your scraper in the same tool as the browser. A side-by-side comparison finds a wrong header order or fingerprint before it shows up as a block rate.
- Keep one IP per session. Use sticky sessions, check the exit IP, and start a new session on a new IP when one gets blocked.
- Reuse sessions. The bootstrap is the expensive part, so run as many searches on one session as the site lets you.
Methodology notes
- Dates and scale. We ran the tests on October 22, 2026, through Massive UK residential IPs: 100 sessions, 10 at a time, five searches each. The longer test ran five more sessions, five at a time, with 100 searches each.
- Searches. A search is the hotel list for a place plus one rates request for all of its hotels, for two adults and one to three nights. The hotel content (names and addresses) changes rarely, so we left it out of the per-search cost.
- What counts. Traffic is what our counting proxy saw between the scraper and Massive, including the connection setup, TLS handshakes and failed attempts. The calls to the Hyper Solutions API went straight from our machine, not through the proxy, so they're not counted.
- Browser identity. The scraper presents itself as Chrome 155 on Windows, with the headers and header order of the recorded session.
Run it yourself
- Code: the radisson-scraper repository has the full Go flow.
- Proxies: set up a sticky residential session in Massive's Proxy Generator as shown above.
- Akamai payloads: the Hyper Solutions Akamai API generates the sensor, SBSD and pixel payloads; start with the Akamai getting started docs. Every new Hyper Solutions account starts with a free one-week trial.
Frequently Asked Questions
Akamai's payloads are generated for one IP. If the IP changes in the middle of a session, the session is no longer valid, so each session needs to keep one residential IP until it's done.
In our test, clearing Akamai cost 520 KB per session and each search 12.5 KB. With five searches per session that's 122 KB per search all in; with 100 searches on one session it dropped to 18 KB per search.
