Purdue Housing 2023
This is a script to automatically search for open housing spots on the Purdue housing portal.
Notes:
Lower room rate
should be the minimum rate of the room (default: $1,000).Upper room rate
should be the maximum rate of the room (default: $20,000).Delay
should be the delay, in milliseconds, the script waits before attempting to look for rooms again. Please keep this number reasonable; too many requests will crash the server and no one will get rooms.- Check the corresponding boxes for the buildings and room types you want to filter for. The script will indiscriminately attempt to add the first room it sees that matches the selected criteria to your cart, so make sure to only filter for rooms you would be OK with living in.
Using this script is a bit more complicated than just pasting it into the console in the housing portal; see the Using this script section for more details on how it works.
Code:
1const roomTypeIds = [165, 167, 166];
2const buildingIds = [1, 34, 16, 6, 4, 5];
3const lowerRoomRate = 1000;
4const upperRoomRate = 20000;
5const delay = 1000;
6
7let id;
8;(async () => {
9 // Parse URL token from search param after login
10 const params = new URLSearchParams(location.search);
11 const urlToken = params.get('UrlToken');
12
13 const dateStart = params.get('DateStart');
14 const dateEnd = params.get('DateEnd');
15 const termId = params.get('TermID');
16 const classificationId = params.get('ClassificationID');
17
18 const verificationToken = document.body.innerHTML.match(/<input name="__RequestVerificationToken" type="hidden" value="(.+)"\s*\/?>/)?.[1];
19 if (!verificationToken) return log('Could not parse request verification token!');
20
21 const data = document.body.innerHTML.match(/<div class="" data-actualaddingroomtocartoption=".*" data-canaddroomstocart=".*" data-classificationid=".+" data-currencysymbol=".*" data-currentpagenumber="(.+)" data-dateend="(.+)" data-datestart="(.+)" data-filterresultshash="(.+)" data-invalidfieldresponse=".*" data-isauthenticated=".*" data-lowerroomratevalue=".*" data-maximumroomratefilterarialabel=".*" data-maximumroomratefiltervalue=".*" data-minimumroomratefilterarialabel=".*" data-minimumroomratefiltervalue=".*" data-mustselectroommessage=".+" data-portalpageid=".+" data-portalrulestatus=".*" data-processid="(.+)" data-roombaseids=".*" data-roomrateid=".*" data-roomselectionpageid="(.*)" data-showfilterswithoutrooms=".*" data-termid=".+" data-unknowninvalidfieldresponse=".*" data-upperroomratevalue=".*" data-useroommateclassifications=".*" id="page-container">/)
22 if (!data) return log('Could not parse other page data!');
23 const [, currentPageNumber, dateEndISO, dateStartISO, filterHash, processId, roomSelectionPageId] = data;
24
25 // Construct filter request body
26 const filterBody = {
27 classificationID: Number(classificationId),
28 currentPageNumber: Number(currentPageNumber),
29 termID: Number(termId),
30 filters: {
31 DateStart: dateStartISO,
32 DateEnd: dateEndISO,
33 ProfileItemID: roomTypeIds, // Room type IDs
34 RoomLocationID: buildingIds, // Building IDs
35 RoomBaseIDs: [],
36 RoomLocationAreaID: [],
37 RoomLocationFloorSuiteID: [],
38 RoomTypeID: [],
39 UseRoommateClassifications: false,
40 LowerRoomRateValue: lowerRoomRate,
41 UpperRoomRateValue: upperRoomRate,
42 }
43 }
44
45 id = setInterval(queryRooms, delay);
46
47 async function queryRooms() {
48 const res = await fetch(`https://purdue.starrezhousing.com/StarRezPortalX/General/RoomSearch/roomsearch/GetFilterResultsAuthenticated?hash=${filterHash}&pageID=${roomSelectionPageId}&processID=${processId}`, {
49 method: 'POST',
50 body: JSON.stringify(filterBody),
51 headers: {
52 __RequestVerificationToken: verificationToken,
53 'Content-Type': 'application/json',
54 'X-Requested-With': 'XMLHttpRequest'
55 }
56 });
57 if (!res.ok) return log('Error while fetching filter endpoint');
58
59 const parsed = await res.json();
60 if (!('ResultsHtml' in parsed)) {
61 log('Error response when fetching filter endpoint');
62 return console.log(parsed);
63 }
64
65 console.log(parsed.ResultsHtml);
66
67 // Search HTML response for "add to cart" buttons
68 const matches = [...parsed.ResultsHtml.matchAll(/<button aria-label="Add (.+) To Cart" class="ui-add-room-to-cart sr_button_primary sr_button" data-hash="(.+)" data-url="(.+)" title="Add To Cart" type="button">.+<\/button>/g)];
69 if (!matches.length) return log('No rooms found');
70
71 for (const [, name, hash, url] of matches) {
72 log(`Found room ${name}`);
73
74 // Match room base and rate ID from broken url data param
75 const roomMatch = url.match(/roomBaseID=(.+?)&roomRateID=(.+?)&/);
76 if (!roomMatch) {
77 log(`Couldn't find base and rate ID for room ${name}`)
78 continue;
79 }
80
81 // Add room to cart
82 const [, roomBaseId, roomRateId] = roomMatch;
83 const res = await fetch(`https://purdue.starrezhousing.com/StarRezPortalX/General/RoomSearch/roomsearch/AddRoomToCartForTerm?hash=${hash}&checkInDate=${dateStartISO}&checkOutDate=${dateEndISO}&termID=${termId}&pageID=${roomSelectionPageId}&roomBaseID=${roomBaseId}&roomRateID=${roomRateId}`, {
84 method: 'POST',
85 body: JSON.stringify({}),
86 headers: {
87 __RequestVerificationToken: verificationToken,
88 'Content-Type': 'application/json',
89 'X-Requested-With': 'XMLHttpRequest'
90 }
91 });
92 if (!res.ok) {
93 log(`Error adding room ${name} to cart`);
94 continue;
95 }
96
97 // Construct next page request body
98 const continueBody = {
99 RoomLocationID: buildingIds,
100 ProfileItemID: roomTypeIds,
101 PageWidgetData: []
102 }
103 for (const id of [1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 16, 17, 33, 34])
104 continueBody[`${id}-checkbox`] = buildingIds.includes(id);
105 for (const id of [165, 166, 167, 168, 169, 198])
106 continueBody[`${id}-checkbox`] = roomTypeIds.includes(id);
107
108 // Fetch next page URL and open it in a new tab if everything is successful
109 const checkoutRawUrl = await (await fetch(window.location.href, {
110 method: 'POST',
111 body: JSON.stringify(continueBody),
112 headers: {
113 __RequestVerificationToken: verificationToken,
114 'Content-Type': 'application/json',
115 'X-Requested-With': 'XMLHttpRequest'
116 }
117 })).text();
118
119 window.open(checkoutRawUrl.slice(1, -1).replaceAll('\\u0026', '&'));
120 clearInterval(id);
121 }
122 }
123})();
124
125function log(m) {
126 console.log(`[${new Date().toLocaleTimeString()}]: ${m}`);
127}
Because of how the housing portal's request hashes and dynamic routes work, this script only works on the List Rooms page (ie. the page that looks like the image on the right):


Left: Initial Selection page with no rooms available; Right: List Rooms page
If you have navigated to the Initial Selection page (left) but cannot go further due to there being no available rooms, run the following code in the console to jump to the next page:
Code:
1const rawUrl = await (await fetch(window.location.href, {
2 method: 'POST',
3 body: JSON.stringify({PageWidgetData: []})
4})).text()
5
6window.location = rawUrl.slice(1, -1).replaceAll('\\u0026', '&');
Once you're at the list rooms page, copying the original script into your console should start the room search process.
The script sends a lot of logs in the console, mostly for debugging purposes. You'll know the script has found a room when it starts opening a bunch of tabs (an imperfect solution, but one that works given the time constraints and limited opportunities to test it). These tabs will be the bed assignment pages for the room the script has just added to your cart; wait for one to load, fill it out as normal, and pray that the request goes through. Remember to hit confirm as normal on the proceeding page as well.
During execution, the script makes a request to the filter rooms endpoint (the same one the list rooms webpage uses) every delay
milliseconds. If, for whatever reason, you want to stop sending new requests, you can clear the interval with
Code:
1clearInterval(id)
Note that at peak times, response times from the API will greatly exceed the rate at which the script makes new fetches to it (the worst I've seen were response times of almost 3 minutes). Clearing the interval does not prevent fetches that are still pending from resolving, only the creation of new ones. The list of pending requests can easily range in the thousands during peak times; to clear all requests immediately, reload the page or close the tab.
If, during execution, you want to add or remove buildings or room types from your filter, simply mutate the roomTypeIds
and buildingIds
arrays directly:
Code:
1buildingIds.push(1) // Add Cary to the building id filter
Code:
1roomTypeIds.splice(i, 1) // Remove one id from the room type filter, where `i` is the index of the element to remove
The full list of IDs corresponding to buildings and rooms can be referenced here:
Building name | Building ID |
---|---|
Cary Quadrangle | 1 |
Earhart | 2 |
First Street Towers | 3 |
Frieda Parker | 34 |
Harrison | 4 |
Hawkins | 5 |
Hillenbrand | 6 |
Honors | 17 |
McCutcheon | 8 |
Meredith | 9 |
Meredith South | 33 |
Owen | 10 |
Shreve | 12 |
Tarkington | 13 |
Wiley | 14 |
Windsor | 15 |
Winifred Parker | 16 |
Room type | Room type ID |
---|---|
Apartment | 166 |
Double | 165 |
Quad | 198 |
Single | 169 |
Suite | 167 |
Triple | 168 |