Getting the best deals on Uber Eats (Part 1)
Disclaimer: The sole purpose of this article is to educate how to extract structured data from websites, how to use APIs and how developers or power-users can leverage these in order to perform periodic checks and trigger notifications on certain conditions.
The reason for this article
Have you ever found yourself missing on the best deals of your favorite restaurant?! The purpose of this article is to understand how Uber Eats (webapp) works and reverse-engineer it, getting the structured data, and check if our favorite restaurant has the “Buy One, Get One Free” promotion going on. Finally, we want trigger this check at let’s say 12pm and 19pm and get email-notified 🔔 if there’s something special for us 🍔
NOTE: We’ll be using the Uber Eats for Portugal but all the logic below can be applied to other locations.
Reverse-engineering
So the first page we’re presented with when visiting Uber Eats is this, or something very similar:
If you have visited it before and entered your exact delivery location, you’ll notice that this is not the first page shown to you. This is because the website already setup some preference cookies to get you directly to the restaurants page every time you access Uber Eats. If this is the case, please open an incognito tab in order to be able to follow the article.
By now, you want to have your DevTools open and if you go to Application tab
> Storage
> Cookies
> https://www.ubereats.com
you would see something like below - these are preference/session cookies.
Cookie Name | Cookie Value |
---|---|
marketing_vistor_id | … |
uev2.gg | true |
uev2.id.session | e9c52385…. |
uev2.ts.session | 163364…. |
uev2.id.xp | …. |
uev2.mwebAppInstallFullScreenBannerClosed | …. |
Ok, so now go ahead and enter a location. We’ll use Marquês de Pombal Square. If you ever visited Lisbon you’ll probably recognize it.
Take a close look on the Network Tab
and the first thing that pops to the eye are the XHR calls. These are usually calls to API to get or set data and by inspecting the response body of them, the most interesting would probably be this: https://www.ubereats.com/api/getFeedV1?localeCode=pt
This is the source that will populate the page with the current available restaurants. feedItems
is the most interesting property. If we dig deeper into each of these feed items, we’ll find one property called signposts
. Sometimes it’s populated with null
and sometimes you get something like this:
{
"signposts": [
{
"backgroundColor": {
"color": "#05A357"
},
"text": "Compre 1, receba 1 grátis",
"textColor": {
"color": "#FFFFFF"
}
}
]
}
If you haven’t understood yet, the text “Compre 1, receba 1 grátis” in portuguese means “Buy 1, Get 1 free”. Bingo ! We have found the specific data-point we were looking for. Now let’s jump into the code.
The code
In order to automate the process, we’ll need to write some code. We’ll use Python, but we could use Node.js for the case. We won’t go into the specific syntax of the language but in pseudo-code what we want to do is something like this:
Set restaurants_with_deals: []
Call the API endpoint, check if http status code is 200
Iterate the property feedItems
If "The best pizza restaurant" in item.store.title AND item.store.signposts contains "Compre 1, receba 1 grátis"
push item.store.title into restaurants_with_deals
If restaurants_with_deals length greater than zero
Send email notification with restaurants_with_deals
Calling the API
One of the best ways to replicate the API calling is to use cURL. It’s a pretty basic command-line tool, yet very powerful to debug and reproduce HTTP requests. It even comes installed on Windows.
So let’s try calling it with the same JSON payload. One trick you can use is copying the XHR request using DevTools. Just right-click over the XHR request and do Copy
>Copy as cURL
. You’ll see this giant blob of information like this.
curl "https://www.ubereats.com/api/getFeedV1?localeCode=pt" ^
-H "x-csrf-token: x" ^
-H "user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" ^
-H "content-type: application/json" ^
-H "accept: */*" ^
-H "origin: https://www.ubereats.com" ^
-H "sec-fetch-site: same-origin" ^
-H "sec-fetch-mode: cors" ^
-H "sec-fetch-dest: empty" ^
-H "referer: https://www.ubereats.com/pt/feed?pl=JTdCJTIyYWRkcmVzcyUyMiUzQSUyMk1hcnF1JUMzJUFBcyUyMGRlJTIwUG9tYmFsJTIyJTJDJTIycmVmZXJlbmNlJTIyJTNBJTIyQ2hJSmVYRGZMWGN6R1EwUmo0clhYbzMtaUwwJTIyJTJDJTIycmVmZXJlbmNlVHlwZSUyMiUzQSUyMmdvb2dsZV9wbGFjZXMlMjIlMkMlMjJsYXRpdHVkZSUyMiUzQTM4LjcyNDk0MTQlMkMlMjJsb25naXR1ZGUlMjIlM0EtOS4xNTA5OTY4JTdE" ^
-H "accept-language: pt-PT,pt;q=0.9" ^
-H "cookie: uev2.id.xp=a28c52ba-e720-40c1-93b3-3e9d0ee1988a; dId=d9021049-d7af-4ad9-920a-bc84db56d133; uev2.id.session=5127e00a-7141-4f41-9bec-c0f70d829cd6; uev2.ts.session=1633648711936; uev2.loc=^%^7B^%^22address^%^22^%^3A^%^7B^%^22address1^%^22^%^3A^%^22Marqu^%^C3^%^AAs^%^20de^%^20Pombal^%^22^%^2C^%^22address2^%^22^%^3A^%^22Lisboa^%^22^%^2C^%^22aptOrSuite^%^22^%^3A^%^22^%^22^%^2C^%^22eaterFormattedAddress^%^22^%^3A^%^221250-160^%^20Lisboa^%^2C^%^20Portugal^%^22^%^2C^%^22subtitle^%^22^%^3A^%^22Lisboa^%^22^%^2C^%^22title^%^22^%^3A^%^22Marqu^%^C3^%^AAs^%^20de^%^20Pombal^%^22^%^2C^%^22uuid^%^22^%^3A^%^22^%^22^%^7D^%^2C^%^22latitude^%^22^%^3A38.7249414^%^2C^%^22longitude^%^22^%^3A-9.1509968^%^2C^%^22reference^%^22^%^3A^%^22ChIJeXDfLXczGQ0Rj4rXXo3-iL0^%^22^%^2C^%^22referenceType^%^22^%^3A^%^22google_places^%^22^%^2C^%^22type^%^22^%^3A^%^22google_places^%^22^%^2C^%^22source^%^22^%^3A^%^22rev_geo_reference^%^22^%^2C^%^22addressComponents^%^22^%^3A^%^7B^%^22countryCode^%^22^%^3A^%^22PT^%^22^%^2C^%^22firstLevelSubdivisionCode^%^22^%^3A^%^22Lisboa^%^22^%^2C^%^22city^%^22^%^3A^%^22Lisboa^%^22^%^2C^%^22postalCode^%^22^%^3A^%^221250-160^%^22^%^7D^%^2C^%^22originType^%^22^%^3A^%^22user_autocomplete^%^22^%^7D; marketing_vistor_id=78e72918-ff6c-4a37-bb64-be298be77a24; jwt-session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzM2NDg3MTIsImV4cCI6MTYzMzczNTExMn0.-sZDzc__dhesxypJkvNZ0wtHzquiJ7DVuyTHmn7Avew" ^
--data-raw "^{^\^"cacheKey^\^":^\^"JTdCJTIyYWRkcmVzcyUyMiUzQSUyMk1hcnF1JUMzJUFBcyUyMGRlJTIwUG9tYmFsJTIyJTJDJTIycmVmZXJlbmNlJTIyJTNBJTIyQ2hJSmVYRGZMWGN6R1EwUmo0clhYbzMtaUwwJTIyJTJDJTIycmVmZXJlbmNlVHlwZSUyMiUzQSUyMmdvb2dsZV9wbGFjZXMlMjIlMkMlMjJsYXRpdHVkZSUyMiUzQTM4LjcyNDk0MTQlMkMlMjJsb25naXR1ZGUlMjIlM0EtOS4xNTA5OTY4JTdE/DELIVERY///0/0//JTVCJTVE/////^\^",^\^"feedSessionCount^\^":^{^\^"announcementCount^\^":0,^\^"announcementLabel^\^":^\^"^\^"^},^\^"showSearchNoAddress^\^":false,^\^"userQuery^\^":^\^"^\^",^\^"date^\^":^\^"^\^",^\^"startTime^\^":0,^\^"endTime^\^":0,^\^"carouselId^\^":^\^"^\^",^\^"sortAndFilters^\^":^[^],^\^"marketingFeedType^\^":^\^"^\^",^\^"billboardUuid^\^":^\^"^\^",^\^"feedProvider^\^":^\^"^\^",^\^"promotionUuid^\^":^\^"^\^",^\^"targetingStoreTag^\^":^\^"^\^",^\^"venueUuid^\^":^\^"^\^",^\^"favorites^\^":^\^"^\^",^\^"getFeedItemType^\^":^\^"DYNAMIC^\^"^}"
--compressed
Copy-paste it into a .bat file or .sh depending on which OS you’re using. In our case it’s Linux, so we copied from Chrome with the Bash option. After executing the script, the output should be something similar to this.
{"status":"success","data":{"countdowns":[],"diningModes":[{"mode":"DELIVERY","title":"Entrega","isAvailable":true,"isSelected":true},{"mode":"PICKUP","title":"Recolha","isAvailable":true,"isSelected":false}],"sortAndFilters":[{"uuid":"1c7cf7ef-730f-431f-9072-26bc39f7c021","type":"sort","label":"Ordenar","maxPermitted":1,"minPermitted":1,"options":[{"uuid":"3c7cf7ef-730f-431f-9072-26bc39f7c022","value":"Recommended","isDefault":true,"label":" Escolhido para si (predefinido)","iconUrl":"https://duyt4h9nfnj50.cloudfront.net/sort_and_filters/filter-ic-recommended-v2.png"},{"uuid":"4c7cf7ef-730f-431f-9072-26bc39f7c023","value":"Most popular","isDefault":false,"label":" Mais populares","iconUrl":"https://duyt4h9nfnj50.cloudfront.net/sort_and_filters/filter-ic-mostpop.png"},{"uuid":"5991d63c-4d42-46bc-8301-ce224557e615","value":"Rating","isDefault":false,"label":" Classificação","iconUrl":"http://duyt4h9nfnj50.cloudfront.net/sort_and_filters/rating_icn.png"},{"uuid":"5c7cf7ef-730f-431f-9072-26bc39f7c024","value":"Delivery time","isDefault":false,"label":" Tempo de entrega","iconUrl":"https://duyt4h9nfnj50.cloudfront.net/sort_and_filters/filter-ic-etd.png"}],"selected":false},{"uuid":"2c7cf7ef-730f-431f-9072-46bc39f7c021","type":"priceRangeFilter","label":"Interval ... }
So far, so good. But we don’t really want to copy/translate all those HTTP headers into the Python code. So by trial and error we’ll remove each header and check if we keep getting the same response until we stick with the bare minimum headers. By doing such we end up with something like this.
curl 'https://www.ubereats.com/api/getFeedV1?localeCode=pt' \
-H 'x-csrf-token: x' \
-H 'content-type: application/json' \
-H 'cookie: uev2.id.xp=a28c52ba-e720-40c1-93b3-3e9d0ee1988a; dId=d9021049-d7af-4ad9-920a-bc84db56d133; uev2.id.session=5127e00a-7141-4f41-9bec-c0f70d829cd6; uev2.ts.session=1633648711936; uev2.loc=%7B%22address%22%3A%7B%22address1%22%3A%22Marqu%C3%AAs%20de%20Pombal%22%2C%22address2%22%3A%22Lisboa%22%2C%22aptOrSuite%22%3A%22%22%2C%22eaterFormattedAddress%22%3A%221250-160%20Lisboa%2C%20Portugal%22%2C%22subtitle%22%3A%22Lisboa%22%2C%22title%22%3A%22Marqu%C3%AAs%20de%20Pombal%22%2C%22uuid%22%3A%22%22%7D%2C%22latitude%22%3A38.7249414%2C%22longitude%22%3A-9.1509968%2C%22reference%22%3A%22ChIJeXDfLXczGQ0Rj4rXXo3-iL0%22%2C%22referenceType%22%3A%22google_places%22%2C%22type%22%3A%22google_places%22%2C%22source%22%3A%22rev_geo_reference%22%2C%22addressComponents%22%3A%7B%22countryCode%22%3A%22PT%22%2C%22firstLevelSubdivisionCode%22%3A%22Lisboa%22%2C%22city%22%3A%22Lisboa%22%2C%22postalCode%22%3A%221250-160%22%7D%2C%22originType%22%3A%22user_autocomplete%22%7D; marketing_vistor_id=78e72918-ff6c-4a37-bb64-be298be77a24; jwt-session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzM2NDg3MTIsImV4cCI6MTYzMzczNTExMn0.-sZDzc__dhesxypJkvNZ0wtHzquiJ7DVuyTHmn7Avew' \
--data-raw '{"cacheKey":"JTdCJTIyYWRkcmVzcyUyMiUzQSUyMk1hcnF1JUMzJUFBcyUyMGRlJTIwUG9tYmFsJTIyJTJDJTIycmVmZXJlbmNlJTIyJTNBJTIyQ2hJSmVYRGZMWGN6R1EwUmo0clhYbzMtaUwwJTIyJTJDJTIycmVmZXJlbmNlVHlwZSUyMiUzQSUyMmdvb2dsZV9wbGFjZXMlMjIlMkMlMjJsYXRpdHVkZSUyMiUzQTM4LjcyNDk0MTQlMkMlMjJsb25naXR1ZGUlMjIlM0EtOS4xNTA5OTY4JTdE/DELIVERY///0/0//JTVCJTVE/////","feedSessionCount":{"announcementCount":0,"announcementLabel":""},"showSearchNoAddress":false,"userQuery":"","date":"","startTime":0,"endTime":0,"carouselId":"","sortAndFilters":[],"marketingFeedType":"","billboardUuid":"","feedProvider":"","promotionUuid":"","targetingStoreTag":"","venueUuid":"","favorites":"","getFeedItemType":"DYNAMIC"}' \
--compressed
And if we strip the cookies even more we’ll end up with this.
curl 'https://www.ubereats.com/api/getFeedV1?localeCode=pt' \
-H 'x-csrf-token: x' \
-H 'content-type: application/json' \
-H 'cookie: uev2.loc=%7B%22address%22%3A%7B%22address1%22%3A%22Marqu%C3%AAs%20de%20Pombal%22%2C%22address2%22%3A%22Lisboa%22%2C%22aptOrSuite%22%3A%22%22%2C%22eaterFormattedAddress%22%3A%221250-160%20Lisboa%2C%20Portugal%22%2C%22subtitle%22%3A%22Lisboa%22%2C%22title%22%3A%22Marqu%C3%AAs%20de%20Pombal%22%2C%22uuid%22%3A%22%22%7D%2C%22latitude%22%3A38.7249414%2C%22longitude%22%3A-9.1509968%2C%22reference%22%3A%22ChIJeXDfLXczGQ0Rj4rXXo3-iL0%22%2C%22referenceType%22%3A%22google_places%22%2C%22type%22%3A%22google_places%22%2C%22source%22%3A%22rev_geo_reference%22%2C%22addressComponents%22%3A%7B%22countryCode%22%3A%22PT%22%2C%22firstLevelSubdivisionCode%22%3A%22Lisboa%22%2C%22city%22%3A%22Lisboa%22%2C%22postalCode%22%3A%221250-160%22%7D%2C%22originType%22%3A%22user_autocomplete%22%7D;' \
--data-raw '{"cacheKey":"JTdCJTIyYWRkcmVzcyUyMiUzQSUyMk1hcnF1JUMzJUFBcyUyMGRlJTIwUG9tYmFsJTIyJTJDJTIycmVmZXJlbmNlJTIyJTNBJTIyQ2hJSmVYRGZMWGN6R1EwUmo0clhYbzMtaUwwJTIyJTJDJTIycmVmZXJlbmNlVHlwZSUyMiUzQSUyMmdvb2dsZV9wbGFjZXMlMjIlMkMlMjJsYXRpdHVkZSUyMiUzQTM4LjcyNDk0MTQlMkMlMjJsb25naXR1ZGUlMjIlM0EtOS4xNTA5OTY4JTdE/DELIVERY///0/0//JTVCJTVE/////","feedSessionCount":{"announcementCount":0,"announcementLabel":""},"showSearchNoAddress":false,"userQuery":"","date":"","startTime":0,"endTime":0,"carouselId":"","sortAndFilters":[],"marketingFeedType":"","billboardUuid":"","feedProvider":"","promotionUuid":"","targetingStoreTag":"","venueUuid":"","favorites":"","getFeedItemType":"DYNAMIC"}' \
--compressed
Notice that we only kept the uev2.loc
cookie, which apparently is being used as input for our exact delivery address. Using a URL decoding helper like this you can see the JSON content of it.
{
"address": {
"address1": "Marquês de Pombal",
"address2": "Lisboa",
"aptOrSuite": "",
"eaterFormattedAddress": "1250-160 Lisboa, Portugal",
"subtitle": "Lisboa",
"title": "Marquês de Pombal",
"uuid": ""
},
"latitude": 38.7249414,
"longitude": -9.1509968,
"reference": "ChIJeXDfLXczGQ0Rj4rXXo3-iL0",
"referenceType": "google_places",
"type": "google_places",
"source": "rev_geo_reference",
"addressComponents": {
"countryCode": "PT",
"firstLevelSubdivisionCode": "Lisboa",
"city": "Lisboa",
"postalCode": "1250-160"
},
"originType": "user_autocomplete"
}
Back to the code
So for the first part of calling the API, we think the excerpt below summarizes what we did with cURL.
import requests
import json
url = "https://www.ubereats.com/api/getFeedV1?localeCode=pt"
data = {
"cacheKey": "JTdCJTIyYWRkcmVzcyUyMiUzQSUyMk1hcnF1JUMzJUFBcyUyMGRlJTIwUG9tYmFsJTIyJTJDJTIycmVmZXJlbmNlJTIyJTNBJTIyQ2hJSmVYRGZMWGN6R1EwUmo0clhYbzMtaUwwJTIyJTJDJTIycmVmZXJlbmNlVHlwZSUyMiUzQSUyMmdvb2dsZV9wbGFjZXMlMjIlMkMlMjJsYXRpdHVkZSUyMiUzQTM4LjcyNDk0JTJDJTIybG9uZ2l0dWRlJTIyJTNBLTkuMTUwOTkyMiU3RA",
"feedSessionCount": {
"announcementCount": 0,
"announcementLabel": ""
},
"showSearchNoAddress": False,
"userQuery": "",
"date": "",
"startTime": 0,
"endTime": 0,
"carouselId": "",
"sortAndFilters": [{
"uuid": "2c7cf7ef-730f-431f-9072-27bc39f7c021",
"options": [{
"uuid": "2c7cf7ef-730f-431f-9072-26bc39f7c025"
}]
}],
"marketingFeedType": "",
"billboardUuid": "",
"feedProvider": "",
"promotionUuid": "",
"targetingStoreTag": "",
"venueUuid": "",
"favorites": "",
"pageInfo": {
"offset": 0,
"pageSize": 10000
}}
r = requests.post(url, data=json.dumps(data), headers={
"content-type": "application/json",
"x-csrf-token": "x",
"cookie": "uev2.loc=%7B%22address%22%3A%7B%22address1%22%3A%22Marqu%C3%AAs%20de%20Pombal%22%2C%22address2%22%3A%22Lisboa%22%2C%22aptOrSuite%22%3A%22%22%2C%22eaterFormattedAddress%22%3A%221250-160%20Lisboa%2C%20Portugal%22%2C%22subtitle%22%3A%22Lisboa%22%2C%22title%22%3A%22Marqu%C3%AAs%20de%20Pombal%22%2C%22uuid%22%3A%22%22%7D%2C%22latitude%22%3A38.72494%2C%22longitude%22%3A-9.1509922%2C%22reference%22%3A%22ChIJeXDfLXczGQ0Rj4rXXo3-iL0%22%2C%22referenceType%22%3A%22google_places%22%2C%22type%22%3A%22google_places%22%2C%22source%22%3A%22manual_auto_complete%22%2C%22addressComponents%22%3A%7B%22countryCode%22%3A%22PT%22%2C%22firstLevelSubdivisionCode%22%3A%22Lisboa%22%2C%22city%22%3A%22Lisboa%22%2C%22postalCode%22%3A%221250-160%22%7D%2C%22originType%22%3A%22user_autocomplete%22%7D;",
})
if r.status_code != 200:
print("Unexpected response code.")
print(r.status_code)
exit()
We then need to filter it according to our pseudo-code and notify in case there are specific deals on going for our favorite restaurants. This is basic array manipulation, and it can be done in multiple ways. The code below just exemplifies how you can achieve it.
feedItems = r.json()['data']['feedItems']
print(len(feedItems), "items")
def buyOneGetOneFreeFilterFunction(feedItem):
if feedItem["type"] == "MINI_STORE" and feedItem["title"]["text"] == "Best restaurant":
for signpost in feedItem["store"]["signposts"]:
if signpost["text"] == "Compre 1, receba 1 grátis":
return True
return False
buyOneGetOne = filter(buyOneGetOneFreeFilterFunction, feedItems)
Finally, we want to be notified via e-mail.
def send_mail(subject, body):
message = MIMEMultipart()
message['From'] = <your-smtp-user>
message['To'] = <your-smtp-user>
message['Subject'] = subject
message.attach(MIMEText(body, 'plain'))
session = smtplib.SMTP('smtp.gmail.com', 587)
session.starttls()
session.login(user, app_key)
text = message.as_string()
session.sendmail(user, user, text)
session.quit()
On the second-part of this article we’ll make the entire code available and guide you through the basic usage of Crontask.io application, set up a Task and schedule it for period checks.