Queue-it runs virtual waiting rooms. When a ticket sale or a product drop opens, visitors are held in a queue and let through to the site at a rate it can handle. BotDeflector is the bot check in front of that queue: before you get a place, it asks you to prove you are not a script, so queue positions can’t be taken in bulk.
BotDeflector is also sold on its own, outside Queue-it, as botdeflector.eu; this guide covers the Queue-it deployment. It is a flow of steps, and the one you will meet most often is the icon challenge: a strip of three small icons, a cluttered frame, and a request to click the icons in the strip’s order. It looks like a trivial vision task. Most of the work is in the protocol around it, and in one timing rule that makes a correct answer look wrong.
This guide walks through the tokens, every request in the icon flow, the frame, the timing floor, the error codes and a debugging checklist.
Where BotDeflector sits
A visitor reaching a protected sale is sent to a waiting room page, for example https://queue.shop.example.com/?c=shop&e=sale. Before that page gives them a queue position, it runs BotDeflector. Not every page runs the same steps: a plain queue page often asks only for the invisible proof-of-work step, while a softblock page usually asks for the icon challenge.
The page embeds a widget script from Queue-it’s challenge host and hands it a flow token, which says which steps this visitor must pass. The widget talks to the challenge host, and only the resulting tokens go back to the waiting room.
How to tell a site uses it
- A request to the shop redirects to a Queue-it waiting room page.
- The page’s source sets
botDeflectorJwtToken(a JWT) andbotDeflectorSource(a protocol-relative URL such as//dual.challenge.queue-it.net/...). - The network panel shows requests to
dual.challenge.queue-it.netoripv4.challenge.queue-it.net, on paths like/icon/get,/icon/verifyand/icons/<lib>/.... - Refusals are small JSON bodies with a letter-and-three-digits code:
{"error":"I024 Bad Request"}.
The flow token’s payload is plain JSON once base64url-decoded. Read it before anything else, because it names the steps you will face:
{
"accountId": "<account>",
"siteKey": "<site key>",
"sessionId": "<session>",
"challengeFlow": [
{ "type": "icon", "complexity": 1, "allowAudio": true }
],
"requestingIP": "203.0.113.7",
"iat": 1762850000,
"exp": 1762850120,
"autoStart": true
}
The step types are inv, icon and audio. If the flow has no icon step, the icon sections below don’t apply to that page.
Cookies and tokens
BotDeflector keeps its state in tokens. The waiting room’s own cookies are outside this guide’s scope; what matters is carrying one cookie jar from the page fetch through every leg.
| Cookie or token | Set by | What it means | Lifetime |
|---|---|---|---|
| Waiting room session cookies | The queue page | The session the flow token was minted for | — |
botDeflectorJwtToken (flow token) |
The queue page, in its source | Which steps to pass, for which account and session, and the address that asked (requestingIP) |
About two minutes |
challengeToken |
GET /icon/get |
One issued challenge: its type, session, icon library and image names | Carries an exp; length not covered here |
token |
POST /icon/verify |
Proof the icon step was passed, handed to the rest of the flow | — |
The two-minute flow token is the real budget: everything below has to fit inside it.
The flow, step by step
1. Read the flow token and the source
Fetch the waiting room page with the client you will use for every later request. Pull out botDeflectorJwtToken and botDeflectorSource, check that challengeFlow contains an icon step, and note exp and requestingIP.
2. Pick the challenge host
The widget takes the API base from the origin of its own script, so follow botDeflectorSource rather than hard-coding a host. Today that is https://dual.challenge.queue-it.net. When the token’s requestingIP is an IPv6 address (the widget’s test is whether it contains a colon), the widget switches to https://ipv4.challenge.queue-it.net.
3. Ask for an icon challenge
GET /icon/get HTTP/1.1
Host: dual.challenge.queue-it.net
x-tt-flow-token: eyJhbGciOi...<flow token>
Content-Type: application/json
Origin: https://queue.shop.example.com
Referer: https://queue.shop.example.com/?c=shop&e=sale
The challenge API is cross-origin to the waiting room, so a browser always sends Origin and Referer. The response is one field:
{ "challengeToken": "eyJhbGciOi...<challenge token>" }
Start a clock when this response arrives. The challenge token is another JWT carrying challengeType, sessionId, exp, lib (the icon library), ico and img. Decode it loosely: in an icon token exp arrives as a string and lib as a number.
4. Fetch the two images
When lib is set, ico and img are file names on the same host:
GET /icons/<lib>/ico/<ico> HTTP/1.1
Host: dual.challenge.queue-it.net
Referer: https://queue.shop.example.com/?c=shop&e=sale
GET /icons/<lib>/bgnd/<img> HTTP/1.1
Host: dual.challenge.queue-it.net
Referer: https://queue.shop.example.com/?c=shop&e=sale
When lib is absent, they are inline objects, {"mediaType": "...", "base64": "..."}. Either way you get the icon strip, an 86×27 PNG holding the three icons in click order, and the frame, a 300×200 WebP in which they hide among background shapes. Keep both exactly as served: no resizing, no re-encoding.
5. Find the three points, then wait
The answer is three points in the frame’s own pixels, origin top-left, in the strip’s order. Then wait until about 2.2 s have passed since /icon/get answered. Both are covered below.
6. Post the answer
POST /icon/verify HTTP/1.1
Host: dual.challenge.queue-it.net
Content-Type: application/json
Origin: https://queue.shop.example.com
Referer: https://queue.shop.example.com/?c=shop&e=sale
{
"solution": [{"x": 61, "y": 151}, {"x": 145, "y": 55}, {"x": 182, "y": 147}],
"solution_fingerprint": "<32 hex characters>",
"time": 2231,
"challengeToken": "eyJhbGciOi...<challenge token>",
"challengeFlowToken": "eyJhbGciOi...<flow token>"
}
time is milliseconds since the challenge arrived. There is no behavioural data in the body: the widget records mouse movement during the challenge but doesn’t send it. On success:
{ "token": "eyJhbGciOi...<icon step token>" }
The widget’s own timeout for both calls is 15 seconds.
7. Finish the flow
The icon step’s token is not a queue position. The rest of the flow (/verify-flow, then the waiting room’s own BotDeflector verify) still has to be posted, from the same egress, and any inv step the flow names still has to be solved.
The icon challenge in detail
Fourteen colours
Every frame is drawn from a fixed palette of 14 colours: seven muted ones for the background polygons and seven saturated ones for the icons. Quantise each pixel to its nearest palette entry and the icons can only be in seven colour planes; the polygons never need to be looked at. That is why re-encoding breaks a solver: WebP is lossy, and a second encode moves pixels off the palette.
The palette is also the single point of failure. A newer icon library draws its polygons in saturated colours close to the icon colours, and a solver that only knew the first palette read whole polygons as icons. Nothing in the server’s response says so; the pass rate just falls. Separate the two by area instead: a polygon is one body of thousands of pixels, an icon a few hundred, at most three per frame.
No decoy icons, in our sample
Across 125 frames we examined from two icon libraries, the icon planes held exactly the three targets: the clutter is polygons, not lookalike icons. Extra or missing blobs are artefacts (an icon split by WebP smear, a thin outline lost to chroma subsampling, two same-coloured icons touching), which shape matching tolerates and blob counting does not.
Where to click
The server accepts a click within about 15 px of the icon image’s centre: a disc, not the icon’s shape. For an asymmetric icon, the image centre and the centre of its visible pixels can be up to about 14 px apart, and a hollow icon may have no ink within 15 px of the right point. Icons can also appear at a different scale and rotation from the strip, so matching has to carry the image centre through both.
The 2.2 second floor
The server refuses a correct answer that arrives too soon, with I024, the same code as a missed click. The time field has no effect: what counts is when the request is sent. The floor varies per challenge. Of 12 challenges measured, the first accepted post came about 2.1 s after the challenge in 10, and at 2.7 s and 3.55 s in the other two.
A solver that posts as soon as it has an answer is refused, concludes it was wrong, and moves to a worse one. In our measurement, that turns a solver whose first answer is almost always right into one that passes about 1 challenge in 40.
The fix is a schedule, measured from when /icon/get answered:
- Wait until 2.2 s have passed.
- Post your best answer. On
I024orI026, post the same answer again at 2.7, 3.2, 3.8, 4.5, 5.5 and 7.0 s. - Only once that schedule is spent, try your second and third answers, once each. By then the clock is satisfied, so a refusal really is about the clicks.
To measure a solver’s accuracy, cap it at one answer but keep the schedule. A single post at 2.2 s scores a correct answer as wrong on roughly one challenge in six.
Error codes
| Code | Meaning | Retry? |
|---|---|---|
I014 |
The flow does not name this challenge type | No: check challengeFlow |
I020 |
A required field is missing from the verify body | No: fix the body |
I024 |
A click hit no icon, or the answer was posted too early | Yes: same answer on the schedule, then the next answer |
I026 |
The icons or their order are wrong | Yes, as for I024 |
I028 |
Not evaluated: usually solution_fingerprint is the literal "failed" |
No: fix the fingerprint |
I020 and I028 are not verdicts on the clicks, so posting again only burns the token. Log any other code in full: a new code is how a protocol change usually shows up.
The other steps: inv and audio
inv, the invisible step, is a graph-cycle proof of work: it costs CPU time rather than attention. Plain queue pages often ask for it alone.iconis the challenge above.audiois an audio challenge. Each step in the flow token carries anallowAudioflag.
The icon step can be requested and answered on its own even when the flow also names inv, but a finished queue pass needs every named step. Parsa solves the icon step only, not inv or audio.
What the client reports about itself
The widget bundles FingerprintJS (version 5.0.1 at the time of writing) and sends its visitor id as solution_fingerprint. The id is a 128-bit non-cryptographic hash, written as 32 hex characters, of a canonical string built from the collected components, so the same components always give the same id. They include:
- device-bound values that are expensive to fake: canvas rendering, audio processing, WebGL, installed fonts;
- identity values: user agent client hints,
platform,vendorand vendor flavours, screen resolution, languages, time zone; - hardware and feature flags: colour depth, hardware concurrency, device memory, CPU architecture, cookies enabled, the PDF viewer, Apple Pay availability,
osCpu.
When FingerprintJS fails to load, the widget sends the literal string "failed". The audio step accepts that; the icon step answers I028 however good the clicks are. Whether the server can tell an id computed from a plausible component set from one collected in a real browser has not been established; a well-formed, self-consistent id is the safe choice.
What the server checks beyond the answer
IP and session consistency
The flow token records the address that requested it (requestingIP), and the waiting room’s verify response echoes a sourceIp. The safe assumption is that every leg (page, /icon/get, images, /icon/verify, /verify-flow, final verify) leaves by one egress. A client that fetches the challenge from a different address can have the flow refused however good its clicks were, and it looks exactly like a bad solver.
Whether the icon legs enforce this has not been measured, and the widget itself moves to an IPv4-only host when the token records an IPv6 address, presenting a different address from the one in the token. Until it is measured, keep one egress. If you test it, run a same-egress control first: a second client is a second cookie jar as well as a second address.
TLS, headers and cookies
The challenge host sees your TLS handshake, and a language runtime’s default stack is an obvious non-browser. Wear a current Chrome handshake and a user agent naming the same browser: a Chrome handshake under a Firefox user agent is a free tell. How the host weighs these signals is not covered here; the Akamai guide goes deeper into JA3, JA4 and HTTP/2 fingerprints.
Send Origin and Referer for the waiting room page on every call, carry the page’s cookie jar through every leg, and keep the fingerprint’s time zone and languages plausible for your egress. Whether the server checks that last point is not established, but it costs nothing.
Measured pass rates
Live runs against Queue-it’s challenge endpoint on 2026-11-10, icon step only, posting on the schedule above:
| Icon library | n | First answer accepted | With up to 3 answers | Solve, median |
|---|---|---|---|---|
| Library A | 300 | 99.7% | 100% | about 1 s |
| Library B (newer) | 800 | 87.2% | 96.0% | about 1 s |
The first rate is the solver’s accuracy; the second is what you get with fallback answers. A decaying solver shows in the first long before the second.
The endpoint’s own rate drifts within a day. With the solver unchanged, a morning run on library B passed 97.8% and afternoon runs about 93.5%. Compare a change against a control run in the same hour. When library B first appeared, the pass rate on it fell to 36%; extending the solver’s palette brought the next live run to 100%.
Three answers is all one challenge allows. A fresh challenge draws a new frame, so in principle it is an independent draw: at 96.0% each, two challenges would give 99.84% and three 99.994%. Those figures are arithmetic, not measured. Whether one flow token may be spent on several challenges, and whether a second is judged like the first, has not been tested.
Debugging checklist
- Every answer is refused with
I024, even ones you are sure of: you are posting before the floor. Measure from/icon/get, wait 2.2 s, repost the same answer on the schedule. - Correct-looking answers fail about one time in six: you post once at 2.2 s and give up. The floor sometimes sits near 2.7 s or 3.55 s.
- Every post returns
I028: yoursolution_fingerprintis"failed"or malformed. I020: a verify field is missing. Check all five names and thatsolutionholds three{x, y}objects.I014: the flow has noiconstep; this page may beinv-only.- Clicks land near the icons but miss: you are aiming at the ink rather than the image centre, or your coordinates are in a scaled copy of the frame.
- Pass rate collapses with no code change: a new icon library. Log
libfrom every challenge token and check the frame’s colours against your palette. - Perfect clicks, refused flow: the legs left by different egresses or cookie jars.
- Challenge host requests fail outright: check whether
requestingIPis IPv6 and you should be on the IPv4 host. - The flow token is rejected: it lives about two minutes. Fetch a fresh page rather than retrying a spent token.
Solving Queue-it BotDeflector with Parsa
Parsa solves the icon step. Your client runs the protocol above from its own egress and sends only the two images to POST https://api.parsalabs.io/botdeflector/icon, with icon (the strip PNG, base64), background (the frame WebP, base64) and optionally answers (how many ranked answers, 1 to 12, default 3). It never sees your flow token or your network. It does not solve inv or audio.
The response carries solution (three points in the strip’s order), answers (the ranked alternatives, solution first), submit_after_ms and submit_schedule_ms (when to post, counted from when /icon/get answered), accept_radius and took_ms. A frame the solver can’t read returns 422 and costs nothing; only 200s are billed. See the benchmarks and pricing.