Guide 1 of 4 · Getting started

Getting started and how the poller works

The poller is a small Python program that asks BookMyShow every minute whether any show of one movie has seats, and sends a push message and an email when seats appear. It works the same on macOS, Linux and Windows. It can run in the background on your own computer or on a small cloud server.

  • 11 October 2026
  • 15 min read
  • For whoever sets up or runs the poller, including someone new to Python
B4–5
Row B, seats 4 and 5: two free seats side by side. That is the moment the poller exists to catch.

01What to install

The poller is plain Python and runs on macOS, Linux and Windows. Every command box in these guides has a tab for each system. Your computer is picked automatically, and you can change it once in the sidebar to switch every box at the same time.

You type commands into a terminal: the Terminal app on macOS, a terminal window on Linux, or PowerShell on Windows.

Tool Why Check it worked
A package manager Installs everything else with one command: Homebrew on macOS, apt on Ubuntu or Debian, winget on Windows brew --version, apt --version or winget --version
Python 3.12 or 3.13 Runs the poller python3 --version (on Windows: python --version)
git Saves the history of your code (git) git --version
GitHub CLI Log in to GitHub, make keys, open pull requests from the terminal gh --version
VS Code The editor, with the Python and Claude Code extensions code --version
Node.js 20 or newer Only for the version 2 website and the mobile app, not for the poller node --version

Install them all:

# Homebrew, the macOS package manager (skip if "brew --version" already works)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# The tools
brew install python@3.13 git gh node
brew install --cask visual-studio-code

Playwright, the browser the poller uses to read seat maps, is installed inside the project in section 3. If gh is not found on an older Linux, follow GitHub's Linux instructions.

Note. use a normal Python release, not a pre-release ("rc" or "beta"), so Playwright's downloads match. If your .venv folder was built with a pre-release, delete it and create it again.

02Git and GitHub setup

The code lives in a private GitHub repository. In the commands below, replace YOUR-GITHUB-NAME with the account that owns it. These steps recreate the setup on any machine. If git is new to you, read GitHub's Hello World first (20 minutes).

  1. Tell git who you are (once per machine):

    bash
    git config --global user.name "Your Name"
    git config --global user.email "you@example.com"
    git config --global init.defaultBranch main
  2. Log in to GitHub (the same on every system). The tool creates an SSH key for you, so you never type a password again:

    bash
    gh auth login        # choose: GitHub.com, SSH, generate a new key, log in with browser
    gh auth status       # should say "Logged in to github.com"
    ssh -T git@github.com
  3. Download (clone) the code and create the private settings file:

    git clone git@github.com:YOUR-GITHUB-NAME/bms-poller.git
    cd bms-poller
    cp .env.example .env     # then fill in your values; .env is never uploaded
  4. Make every change on its own branch, then open a pull request:

    bash
    git checkout main && git pull
    git checkout -b fix/blank-links         # a new branch named after the change
    # edit files, run, test
    git add -A && git commit -m "Skip alert when scraper finds zero groups"
    git push -u origin fix/blank-links
    gh pr create --fill                     # opens a pull request; merge it on GitHub
  5. Never upload secrets. The .gitignore file already keeps out .env, state.json, notified_pairs.json, bms_state.json, logs/, diag/ and dump_*.json. If a token ever ends up in a commit, treat it as stolen: log out of BookMyShow in that browser, log in again, and remove the commit from history with git filter-repo (GitHub's guide).

03Install and run the poller

The first run takes about ten minutes. The slow part is copying your BookMyShow login from Chrome, which this version still needs. The bug guide shows how to replace that with a one-time login.

  1. Go to the project folder, create the virtual environment, turn it on, and install the packages:

    cd ~/Desktop/"Book myshow room"/bms-poller      # or wherever you cloned it
    python3 -m venv .venv
    source .venv/bin/activate
    pip install -r requirements.txt
    playwright install chromium                      # only needed when V2_SCRAPER_ENABLED=1

    While the virtual environment is on, your prompt starts with (.venv). In a new terminal window, run the cd line and the activate line again before using the poller. From here on, python means the virtual environment's Python on every system.

  2. Copy .env.example to .env and set the target: EVENT_CODE (the ET000… code in the movie's BookMyShow web address), DATE_CODE as year-month-day like 20261012, REGION_CODE (HYD) and REGION_SLUG (hyderabad).

  3. Copy your login from Chrome. Open in.bookmyshow.com while logged in, press F12 (Windows, Linux) or Option-Command-I (macOS) to open DevTools, choose the Network tab, type api/ in the filter box, click any request, and copy these request headers into .env: x-access-token, x-lsid, x-member-id, x-bms-id, x-advertiser-id, plus your mobile number and email.

  4. Optional, for alerts that name the exact seats: in the DevTools Console tab on a BookMyShow page run

    js
    JSON.stringify({cookies: document.cookie, ls: {...localStorage}, ss: {...sessionStorage}})

    Save the output as a file named test.txt in the folder that contains bms-poller (one level up), run python build_state.py to create bms_state.json, then set V2_SCRAPER_ENABLED=1 and GROUP_SIZE=2 (or 3, 4) in .env.

  5. Set up the two alert channels: NTFY_TOPIC (install the ntfy app on your phone and subscribe to the same topic name) and a 16-character Gmail app password in SMTP_APP_PASSWORD.

  6. Run it (the same on every system, with the virtual environment on):

    bash
    python poller.py

    A healthy start prints monitoring only venues matching: [...] and loaded state: N prior sessions. Then every cycle prints something like showtimes: 12/270 sessions match filter (9 bookable), followed by either no changes this cycle or ALERTING on N changes, then sleeping 60s.

  7. Stop with Ctrl-C (Control-C on macOS too). The poller remembers what it saw in state.json, so restarting does not send the same alerts again.

Note. the README mentions probe.py as a first check, but it no longer matches the code and crashes. Run poller.py directly. The fix is bug 6 in the bug guide.

04Settings: the .env file

Everything the poller does is controlled by the .env file. It is read once at start, so after changing it, restart the poller.

Setting Used by What it controls
EVENT_CODE poller.py The movie, for example ET00505091. One movie per running poller.
DATE_CODE poller.py Show date as YYYYMMDD. A date in the past makes BookMyShow answer error 400 on every poll.
REGION_CODE, REGION_SLUG bms.py City: HYD and hyderabad.
BMS_ACCESS_TOKEN, BMS_LSID, BMS_MEMBER_ID, BMS_ID, BMS_ADVERTISER_ID, BMS_MOBILE, BMS_EMAIL bms.py Your login, copied from DevTools. All seven must exist or the program crashes at start with KeyError. (They are not actually needed for polling; see bug 2.)
MONITORED_VENUES poller.py Theatre names to watch, comma-separated, any case, partial match allowed: AMB Cinemas: Gachibowli, Allu Cinemas: Kokapet. Empty means every theatre in the city.
V2_SCRAPER_ENABLED poller.py 1 makes the poller open the seat map after a change and count free seats. Needs bms_state.json.
GROUP_SIZE poller.py How many seats side by side count as a group (at least 2).
NTFY_TOPIC notifier.py The ntfy topic for phone push. Anyone who guesses the name can read your alerts, so use a long random one.
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_APP_PASSWORD, EMAIL_RECIPIENTS notifier.py Gmail sending. Recipients are comma-separated.
POLL_INTERVAL_SEC, POLL_JITTER_SEC poller.py Seconds between polls, plus a random few seconds so requests do not land at exact minutes. Minimum 5.
BACKOFF_START_SEC, BACKOFF_MAX_SEC poller.py After a 403 or 429 (blocked, too many requests) the wait doubles from start up to max.
FINAL_HOUR_INTERVAL_SEC, SEAT_CALL_DELAY_SEC nothing Present in .env but no code reads them. Safe to delete (bug 8).

The movie title in the alert text is typed into the code as "Spider-Man: Brand New Day". There is no MOVIE_NAME setting yet (bug 7).

05The big picture (architecture)

One Python program, five files of code, three memory files, and three outside services.

.envmovie, date, theatres,login headers, email, ntfystate.json + notified_pairs.jsonwhat was already seenand which seats were sentbms_state.jsonsaved browser loginfor the seat pageread at start, saved every pollpoller.py · the main loopevery ~60 s: fetch all showtimes, compare with last timewhen a show improves: read the seat map (V2), then alertbms.pybuilds the requestwith the pasted headersseat_scraper.pyopens the seat page,records every drawingcanvas_parse + adjacencydrawing → seat grid →runs of N free seatsnotifier.pyntfy push and Gmail,both at the same timefetch showtimeson a rise (V2)seat groupsalert textdrawing logBookMyShow APIshowtimes-by-eventone call, every theatreBookMyShow seat pageseats painted on acanvas, not as textntfy.sh + Gmailpush to your phoneemail to recipientsGET JSONheadless Chromepush + emailDashed boxes are outside the program. launchd, systemd or Task Scheduler restarts it if it stops.
The whole system today: one program, five code files, three memory files, three outside services.

Dashed boxes are outside the program. Nothing here is a server: there is no database, no web page and no login screen, which is why the login headers must be pasted by hand.

File Job
poller.py The loop: fetch, compare, decide, alert, save, sleep.
bms.py Builds the request to BookMyShow's showtimes API with the right headers.
seat_scraper.py Opens the seat page in a headless browser and records every drawing command on the canvas.
canvas_parse.py Turns the drawing commands into a grid of seats with a colour each.
adjacency.py Finds runs of free seats next to each other.
notifier.py Sends the push message and the email.
build_state.py One-off: turns the DevTools dump into bms_state.json.

06What happens every minute (flow)

Every ~60 secondsFetch every showtime of the movieone request to the showtimes APIDid BookMyShow answer OK?no401 · not logged inpush “token expired”, wait 1 hour403 or 429 · blockedwait a little longer each time400 or any other errorwritten to the log only, wait 5 minnobody is told · bug 3All three: sleep, then try again.yesKeep shows at your theatresthe MONITORED_VENUES filterNew bookable show, oravailability went up?no changeyesSeat reader (V2) on?offonRead the seat mapheadless Chrome counts groups of free seatsAny seat groupsnot sent before?all sent beforeyeszero groups, or the read failed · bug 1Send push + emailwith a link to the seat pageSave state.json + notified_pairs.jsonso the next poll can compareSleep, then repeat
One poll cycle. The red box and the red arrow are the two gaps behind bug 3 (silent errors) and bug 1 (blank alerts).

In words: each cycle is one request for every show of the movie in the city. The poller only opens a seat map when a show's availability number went up or a bookable show appeared for the first time. With the V2 scraper on, a show whose free groups were all announced before is skipped.

The red parts are the two gaps covered in the bug guide: a seat read that finds zero groups still sends an alert (that is the blank-alert bug), and an error 400 is printed to the log but never reported to you.

07Alerts and the memory files

Every alert goes out on two channels at the same time from notifier.py. Three JSON files next to the code remember what has been seen, so a restart does not repeat alerts.

Channels. notify() runs send_ntfy and send_email in parallel. ntfy gets a message with the topic, title, text and priority 5 (urgent). Email goes through Gmail with the app password. If one channel fails, the other still goes out, and the failure is written to the log.

What an alert contains. Movie name (fixed in the code), theatre and its code, show time and show id, date, the change (for example "sold out → few seats"), the seat groups if the V2 scraper found any, and a direct link to the seat page: https://in.bookmyshow.com/movies/{region}/seat-layout/{event}/{venue}/{session}/{date}.

File What is inside Why Written when
state.json For each show id, the last availability number seen So the next poll can notice an increase End of every successful poll
notified_pairs.json For each show id, the seat groups already announced, like "B#4-5" So the same seats are not announced twice After an alert with V2 on
bms_state.json The saved browser login (cookies and local storage) Lets the headless browser open the seat page as you By hand, with build_state.py

Availability numbers as the poller reads them: 0 sold out, 1 few seats, 2 filling fast, 3 available. The API also says whether the "Book" button works (cta.type is showTimeRedirect) or shows a sold-out message (toast). In the saved sample both agreed for all 270 shows.

Shows that disappear from one answer are removed from state.json right away. That keeps the file small, but it is also why "New show opened" alerts sometimes repeat (bug 4).

08Running in the background

Running python poller.py stops when you close the terminal. To keep it running, hand it to your system's background service manager. macOS uses launchd, Linux uses systemd, and Windows uses Task Scheduler. The original setup runs on a Mac with launchd and on an Oracle cloud server with systemd.

macOS

macOS: launchd. The template is deploy/com.sriram.bms-poller.plist. It points at the venv's Python and poller.py, and writes output to logs/poller.out.log and logs/poller.err.log. Open it first and change the four folder paths inside it to where your bms-poller folder is.

bash
cp deploy/com.sriram.bms-poller.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.sriram.bms-poller.plist     # start now and at every login
launchctl list | grep bms                                              # shows process id and last exit code
launchctl unload ~/Library/LaunchAgents/com.sriram.bms-poller.plist   # stop
tail -f logs/poller.out.log                                            # watch the log live

After editing .env, unload and load again; launchd does not notice the change by itself. Guide: launchd.info.

Linux

Linux: systemd. This is how the Oracle cloud server runs it. Copy the whole folder, including .env and bms_state.json, to /home/ubuntu/bms-poller, then run deploy/setup_vm.sh once. It installs Python, builds the venv, installs the service and starts it. On your own Linux PC, first change User= and the paths in deploy/bms-poller.service to your user name and folder.

bash
sudo systemctl status bms-poller
sudo journalctl -u bms-poller -f        # live log
sudo systemctl stop bms-poller          # stop
nano ~/bms-poller/.env && sudo systemctl restart bms-poller

The service restarts 30 seconds after any crash. On a server without a screen, Playwright also needs playwright install --with-deps chromium. Guides: systemd basics, Oracle Cloud free tier.

Windows

Windows: Task Scheduler. Run these once in PowerShell, from the project folder. They create a task that starts the poller when you log in and writes its output to logs\poller.out.log. If the task fails, Task Scheduler retries it every minute. If you ever find the poller stopped, Start-ScheduledTask starts it again.

powershell
$dir = (Get-Location).Path
New-Item -ItemType Directory -Force "$dir\logs" | Out-Null
$action   = New-ScheduledTaskAction -Execute "cmd.exe" -WorkingDirectory $dir `
            -Argument "/c .venv\Scripts\python.exe poller.py >> logs\poller.out.log 2>&1"
$trigger  = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) `
            -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName "BMS Poller" -Action $action -Trigger $trigger -Settings $settings
Start-ScheduledTask -TaskName "BMS Poller"

Everyday commands:

powershell
Get-ScheduledTask -TaskName "BMS Poller" | Get-ScheduledTaskInfo   # last run time and result
Stop-ScheduledTask  -TaskName "BMS Poller"                          # stop
Start-ScheduledTask -TaskName "BMS Poller"                          # start again, for example after editing .env
Unregister-ScheduledTask -TaskName "BMS Poller" -Confirm:$false     # remove the task completely
Get-Content logs\poller.out.log -Wait -Tail 50                      # watch the log live

A small black window stays open while the poller runs. Minimise it; closing it stops the poller. You can also see and edit the task in the Task Scheduler app. Guide: Register-ScheduledTask.

09When something goes wrong

Start by reading the last 50 lines of the log file. Every problem below has its own tell-tale line.

tail -n 50 logs/poller.out.log
Line in the log Meaning Fix
400 Client Error: Bad Request ... showtimes-by-event every 5 minutes DATE_CODE is in the past, or EVENT_CODE is wrong. This is the most common reason the poller goes quiet. Put a future date in .env, restart. The code should tell you about this instead of looping quietly (bug 3).
auth error: 401 plus a "BMS token expired" push BookMyShow rejected the login headers Copy fresh headers from DevTools into .env, restart
rate limited (403 ...) or (429 ...) Cloudflare or BookMyShow is blocking too many requests Wait, the poller slows down by itself. Raise POLL_INTERVAL_SEC if it keeps happening.
no showtime rows plucked — schema may have changed BookMyShow changed the shape of its answer Save the new answer with probe.py (after fixing it) and update _pluck_showtimes in poller.py
venue filter matched 0 rows The theatre name in MONITORED_VENUES is spelled differently from BookMyShow's Copy the exact venueName from a log line
seat spinner did not clear or scrape failed The headless browser could not load the seat page (old bms_state.json, Cloudflare, slow network) Rebuild bms_state.json; run python seat_scraper.py --diagnose --headed ... to watch it happen in a real window
notify: email failed Gmail rejected the login Make a new app password; check 2-step verification is on
KeyError: 'BMS_...' at start A required .env line is missing Compare with .env.example

Two quick checks. The first proves the install is fine. The second replays the last recorded seat map and prints the seat groups the code sees.

.venv/bin/python -c "import bms, poller"
.venv/bin/python adjacency.py diag/canvas_draw_log.json