Reverse-Engineering Zomato Food Rescue: MQTT, Server-Driven UI, and a Headless Monitor

Written by jatin-banga | Published 2026/03/09
Tech Story Tags: reverse-engineering | android | python | mqtt | security | api | mobile-security | hacking

TLDRHow I intercepted Zomato's Android traffic, found MQTT credentials in plain JSON, and built a real-time monitor to win Food Rescue before anyone else.via the TL;DR App

Zomato’s “Food Rescue” is essentially a race condition. Here’s how I built a headless monitor to win it.

Every so often, Zomato throws a pop-up on your screen: a cancelled nearby order offered at 50% off. It’s gone in seconds — claimed by whoever is fast enough, or lucky enough to be staring at the right screen at the right time.

I missed it one too many times. Not because I was slow — because I wasn’t even looking. And that bothered me enough to do something about it.

What started as “I want a notification before the flyer appears” became a weeks-long deep dive into Android traffic interception, MQTT protocol internals, server-driven UI architecture, and Zomato’s real-time event pipeline. This is that story.

The Problem: A Race Condition You Can’t Win Passively

Food Rescue events trigger when a real human cancels an order that is already out for delivery. The deal appears for users within a ~3 km radius of where the delivery person currently is. The moment it appears in your app, it’s also appearing for dozens of other users. Miss the flyer — or accidentally swipe back — and it’s gone.

The official app requires you to be actively watching it. I wanted a solution that didn’t.

My goal: intercept the exact same real-time signals the Zomato app listens to, and send myself a push notification the moment a cancellation event fires — before the app even opens.

Step 1: Intercepting the Traffic

Reverse-engineering a web app is trivial. You open DevTools, go to the Network tab, right-click a request, and copy it as cURL. Done.
Mobile apps are a different problem entirely. Modern Android apps operate across three levels of certificate trust, each progressively harder to break:

  • Level 1: User Trust: The app trusts any certificate you install in your phone’s settings. This method has been dead since Android 7, where apps stopped honoring user-installed certificates by default.
  • Level 2 : System Trust: The app trusts certificates pre-installed on the OS (the System Store). To intercept here, you need a rooted device or emulator so you can inject your own certificate into the system store.
  • Level 3: SSL Pinning: The app hardcodes a specific fingerprint of its server certificate directly in its own code. Even if your certificate is in the system store, the app checks: “Does this certificate’s hash match the one I have stored?” If it doesn’t, the connection is killed immediately.

Interestingly, Zomato’s basic HTTP traffic was interceptable using HTTPToolkit, which injects a certificate into the system store. SSL pinning — if it exists in Zomato’s stack — either wasn’t enforced for these endpoints or HTTPToolkit was handling more under the hood than I initially understood.

I searched the intercepted traffic for keywords like food_rescue and rescue. An hour in, I found what I was looking for.

Step 2: The Home Feed Endpoint

When you open Zomato to a saved address, the app calls:

GET /gw/tabbed-home?cell_id=412037575450XXXXXX&address_id=123456 HTTP/1.1
Host: api.zomato.com
X-Zomato-Access-Token: ...
Accept: application/json
User-Agent: OkHttp/4.12.0

Both cell_id and address_id come from your saved addresses — they’re returned when you list your saved locations in the app.

The response is enormous — thousands of lines of JSON. The reason: Zomato uses server-driven UI. Every button, every action, every layout in the app is defined in this JSON response. The client is essentially just an interpreter. You see things like layout_snippet_v3, timer_snippet_type_4, and so on — the server tells the app what to render and what to do when you tap it.

Buried inside this response, under subscription_channels, was this:

{
  "subscription_channels": [
    {
      "type": "food_rescue_cell_412037575450XXXXXX",
      "time": 174XXXXXXXX,
      "client": {
        "username": "zemqtt",
        "password": "secure_password_here",
        "keepalive": 900
      }
    }
  ]
}

I had no idea what MQTT was. A quick ask to an LLM confirmed: this was an MQTT subscription configuration. The app was subscribing to a real-time event channel. The MQTT host, however, wasn’t in the payload — it was hardcoded somewhere in the application binary.

Step 3: Finding the MQTT Host

This is where things got harder for me.

HTTPToolkit only intercepts HTTP. The garbled traffic I had been seeing in its UI wasn’t a bug — it was MQTT traffic being intercepted and misrepresented. No useful clues there.

I tried decompiling the APK using JADX and searching for strings like *.zomato.com and mqtt. I could see where the MQTT connection was being established in the code, but I couldn’t isolate the actual host — too many search results, too much layering.

So I did something unconventional: I asked Grok to deep-search for anything publicly available about Zomato’s MQTT infrastructure. It surfaced the host: “consumermqtt.zomato.com”.

I still don’t know exactly how it found it. But after trying different protocols and common MQTT port numbers, I was able to connect. Later I discovered the official app actually uses “hedwig.zomato.com” — the other endpoint appears to be an alias.

Step 4: The First Real MQTT Message

Using MQTT Explorer, I subscribed to the topic food_rescue_cell_412037575450XXXXXX. The first message I ever received looked like this:

{
  "id": "unique_hash_here",
  "event_type": "order_claimed",
  "timestamp": 177XXXXX
  <other_fields>
}

A few things I learned immediately:

  • Zomato’s MQTT broker retains messages. If the last event was order_claimed, reconnecting 30 minutes later would still show you that message. Same for order_cancelled — if the order wasn't claimed, the cancellation message persists for a while.
  • There are two event types that matter: order_cancelled (the trigger) and order_claimed (the resolution).
  • Subscribing to # (all topics) was blocked — Zomato has access control configured per client.

Step 5: How the App Reacts to a Cancellation

At this point I needed to understand exactly what the app does when it receives an order_cancelled event. HTTPToolkit couldn’t help here since it was blocking MQTT traffic.

I found apk-mitm, which patches the APK, removes the System Trust restriction from the manifest, and recompiles it — so the modified app explicitly trusts user-installed certificates. Combined with mitmproxy, I could now see both HTTP and MQTT traffic simultaneously.

After some waiting, a real cancellation event occurred nearby. The moment it did, the app fired:

POST /gw/gamification/food-rescue/create-cart HTTP/1.1
Host: api.zomato.com
X-Zomato-Access-Token: your_token_here
X-User-Defined-Lat: 28.5355
X-User-Defined-Long: 77.3910
...

With a body built entirely from data already available in the address object. The response — when an active rescue exists — is a deeply nested JSON structure containing the cart ID, parent order ID, restaurant details, number of people watching, and a countdown timer.

{
  "response": {
    "floating_timer_view_1": {
      "click_action": {
        "open_food_rescue_bottom_sheet": {
          "results": [
            {
              "timer_snippet_type_4": {
                "button": {
                  "click_action": {
                    "deeplink": {
                      "url": "zomato://order?res_id=1912345",
                      "post_body": "{\"cart_id\":\"cart_888\",\"context\":{\"cart_modification\":{\"ParentOrderID\":\"order_999\",\"ParentCartID\":\"cart_777\",\"CartModificationType\":\"replace\"},\"cart_analytics_data\":{\"number_of_people_watching\":\"15\",\"cart_expiry_timestamp\":\"1740000000\"}}}"
                    }
                  }
                }
              }
            }
          ]
        }
      }
    }
  }
...
}

Step 6: The “Pitch Once” Constraint

Here’s the painful part I discovered after wasting a day or two: the /gw/gamification/food-rescue/create-cart endpoint is pitch-once only.

The moment you call it, Zomato marks that you have been shown the offer. Call it a second time — even a second later — and you get a failure response. The server has either flagged your session as already notified, or someone else claimed it faster.

This means: if I called the endpoint programmatically when the MQTT event fired, and then the user opened the official Zomato app hoping to see the flyer — nothing would be there. The app had already “shown” it to my script.

I tried deep linking into the Zomato app from a notification to hand off the session. None of it worked reliably — too time-sensitive, too many variables.

So I accepted a simpler outcome: listen for the MQTT event, send a push notification, let the user open Zomato themselves. The notification would at least give them a head start versus waiting to stumble upon the flyer organically.

Step 7: Deduplication and Cooldown Logic

One issue I didn’t expect: Zomato sometimes fires the same order_cancelled event 3–5 times in rapid succession. Whether this is intentional retry logic or a side effect of their message retention setup, I'm not sure.

The fix was straightforward:

  • Store the unique id from each event.
  • If the script has already processed that ID, skip it.
  • Implement a cooldown window so that even if multiple events arrive for the same cancellation, only one notification goes out.

The first notification is all a user needs. Sending five is just noise.

Step 8: Building It Out

At this point the core logic was working. The remaining questions were about packaging.

Login flow: Rather than asking users to manually intercept and hardcode their access token, I reverse-engineered Zomato’s login flow — phone number, OTP, session establishment — and built it into the tool. This took around 10–15 hours but made the tool self-contained.

Distribution: I packaged the Python logic into a CLI app: github.com/jatin-dot-py/jomato. But asking people to run a Python script on Termux or a personal server felt like the wrong UX.

I tried building a mobile UI with Flet. Eight hours later, nothing usable.

So I ported the entire thing to Kotlin. The prompt I gave the LLM was precise: “Copy this Python script exactly. Same headers, same business logic. At the network level, no one should be able to tell whether it’s Python or Kotlin.” The Kotlin app came together in under 24 hours: github.com/jatin-dot-py/jomato-mobile.

What This Reveals About Zomato’s Architecture

A few things stood out from this investigation worth noting independently:

  • Server-driven UI at scale: Zomato’s entire frontend is orchestrated from the server. The client app is a generic renderer. This lets them ship UI changes without app updates, but it also means the entire payload — including internal action endpoints, deeplinks, and analytics — is exposed to anyone who can read the JSON.
  • MQTT for real-time events: Using MQTT for features like Food Rescue is a sensible architectural choice. It’s lightweight, persistent, and works well for broadcast scenarios. The credential exposure in the home feed response (username and password for the MQTT client) is worth noting — anyone who can read that API response can subscribe to the same topics.
  • The pitch-once constraint as abuse prevention: The single-call limit on /gw/gamification/food-rescue/create-cart is almost certainly intentional — it prevents one session from camping on the endpoint and blocking other users. But it has the side effect of making the flow incompatible with programmatic interception, which may also be intentional.

What’s Next

A few ideas I haven’t fully explored:

  • Deep linking completion: There may be a way to start the cart programmatically and hand off to the official app mid-flow. I haven’t found it yet, but I haven’t fully given up.
  • Shadow account approach: Run a secondary Zomato account that mirrors your address and location. The shadow account handles the MQTT monitoring and the initial cart call. When it fires, your real account opens the app fresh — no “already pitched” state, full flyer experience.
  • Good enough as-is: The notification-first approach already removes the need to stare at the app. For most users, that’s sufficient.



Written by jatin-banga | Python Engineer, Security Researcher
Published by HackerNoon on 2026/03/09