Deobfuscating BookMyShow's Ticket Flow & Automating It

Securing first-day movie tickets can be a nightmare, especially for blockbusters. When Oppenheimer was about to release, my friend and I were determined to get the best seats. If you've ever frantically refreshed a booking page when tickets drop, you know the pain.
After missing out on tickets a few times too many, I decided there had to be a better way. The engineer in me couldn't accept defeat by a booking website.
The BookMyShow Challenge
People have attempted to automate BookMyShow using browser automation tools like Selenium and Puppeteer. These approaches typically face several challenges:
- They depend on flaky DOM selectors
- Browser automation is inherently slower than direct API communication
- They struggle with the encrypted seat map format, often resorting to position-based clicking
This approach differs by completely reverse engineering their API Flow along with Seat encryption this is much faster and less flaky, you're essentially sniping them.
Deobfuscating the API Flow
First, every booking follows a specific sequence of API calls
const response = await fetch(
`https://in.bookmyshow.com/serv/getData?cmd=GETSHOWINFOJSON&vid=PVPH&ssid=${SessionId}&format=json`,
{
credentials: "include",
mode: "cors",
referrer: BOOKING_URL,
},
);
This initial call unlocks everything you need: the show information including crucial values like AreaCatCode
and PriceCode
that are required for subsequent calls.
The most surprising discovery was that BookMyShow exposes their encryption key client-side. This is a serious oversight in their security model:
const { seatLayoutEncryptionKey } = await page.evaluate(() => {
return {
seatLayoutEncryptionKey: window.seatLayoutEncryptionKey,
};
});
Even more concerning, they use a static initialization vector (IV) for their AES encryption:
const iv = CryptoJS.lib.WordArray.create(0, 128);
Once I had the encryption key and understood their AES-CBC implementation, I could decrypt the seat layouts directly. This gave me full visibility into seat availability without relying on their UI.
Selecting the Perfect Seats
The real trick was figuring out the seat selection format. After a lot of trial and error, I discovered that seats are represented in a specific pattern:
- Rows are separated by
|
- Each row starts with
rowIdentifier:
- Seats within a row are separated by
:
- Each seat is represented as
seatCode+seatNumber
I wrote a parsing function that could identify specific seats based on row letters and seat numbers:
function findSeats(input: string, row: string, seatNumbers: number[]) {
const rows = input.split("|");
const rowString = rows.find((r) => r.includes(`${row}:`));
const rowNumber = rowString!.split(":")[0];
const seats = rowString!.split(":").slice(1);
const selectedSeats = seatNumbers.map((seatNumber) => {
const seat = seats.find((s) => s.endsWith(`+${seatNumber}`));
const seatWithoutNumber = seat!.split("+")[0];
const indexZero = seatWithoutNumber.indexOf("0");
const seatWithZero = seatWithoutNumber.slice(indexZero);
return seatWithZero;
});
return {
selectedSeats,
rowNumber,
};
}
After parsing the seat layout, we craft the final payload:
let seatLayout = noOfTickets.toString();
seatsAndRow.selectedSeats.forEach((seat) => {
seatLayout += `|${areaCatCode}|3|${seatsAndRow.rowNumber}|${seat}`;
});
seatLayout = `|${seatLayout}|`;
This crafted payload is then sent to the server:
Putting It All Together
The final step was chaining everything together and setting up a payment method. BookMyShow's payment system accepts a payment string format that specifies the provider:
const paymentString = `|TYPE=UPI|UPITYPE=GOOGLEPAY|IMAGEURL=https://assets-in.bmscdn.com/paymentcms/gpay.jpg|MOBILE=${MOBILE}|PROCESSTYPE=REQUEST|LSID=|MEMBERID=|CLIENTID=|`;
const setPayment = await page.evaluate(
async (transactionID, paymentString, EMAIL, MOBILE) => {
const response = await fetch(
"https://in.bookmyshow.com/serv/doSecureTrans.bms",
{
headers: {
"content-type": "application/x-www-form-urlencoded",
},
referrer: BOOKING_URL,
referrerPolicy: "strict-origin-when-cross-origin",
body: `a=WEB&v=&t=${transactionID}&c=SETPAYMENT&p1=${paymentString}&p2=|ETICKET=Y|MTICKET=Y|&p3=${EMAIL}&p4=${MOBILE}&p5=&p6=&p7=&p8=&p9={"platform":"WEB","strAppVersion":"1000"}&p10=&customHeaders=`,
method: "POST",
mode: "cors",
credentials: "include",
},
);
return response.status;
},
transactionID,
paymentString,
EMAIL,
MOBILE,
);
To handle the UPI payment that would follow, I integrated the Twilio API to place a phone call that would wake me up. I left the script running at midnight, went to sleep, and woke up to the Twilio call in the morning. By the time I groggily reached for my phone, the payment notification was waiting for me.
We got perfect center seats for the IMAX. Mission accomplished.

Lessons Learned
This project taught me a few important things:
- Even seemingly complex websites follow predictable API patterns
- Client-side encryption is never truly secure
- Modern websites leak a lot of implementation details if you know where to look
Disclaimer: This information is provided for educational purposes only. The techniques described should not be used for any unauthorized or malicious activities. Always respect the terms of service and legal boundaries of any system or platform.
See all posts