Reverse-Engineering Google's Private batchexecute API

How Photo Cleaner actually deletes photos: the same undocumented rpcid-based RPC endpoint photos.google.com's own front-end uses, driven from a real browser session.

Reverse-Engineering Google's Private batchexecute API

If there's no documented delete API, how does an app actually delete a photo from your Google Photos library and have it land in the real trash? The answer is batchexecute — a private RPC transport that photos.google.com itself uses for essentially everything the web UI does, and that Photo Cleaner drives directly.

Reverse-engineered from the front-end, not from documentation
Reverse-engineered from the front-end, not from documentation

The same rpcids and positional response arrays Google's own web client sends, issued from a real, signed-in session.

What batchexecute is

batchexecute is a generic RPC dispatcher Google uses across several products (Docs, Photos, and others), not something Photos invented. Every call bundles one or more requests, each identified by an opaque rpcid — a short alphanumeric string with no public meaning attached to it. The response comes back as a deeply nested JSON-like array, addressed by position rather than by field name. There's no schema, no OpenAPI spec, and no versioning guarantee.

Working out which rpcid does what — "list a page of the timeline," "move an item to trash," "add to an album" — is not something you get from Google. It comes from watching the network tab while using the real site and matching requests to actions. Photo Cleaner didn't do that work from scratch; it builds on rpcid mappings worked out by prior open-source reverse-engineering projects (credited properly in a later post in this series).

Why positional parsing is fragile by design

Because responses are arrays indexed by position, not objects keyed by name, a Google-side change to field order or an inserted field shifts every index after it. There's no way to request "just give me the fields I know about" — you get the whole positional blob or nothing.

The parser in Photo Cleaner is written around that fragility on purpose: a shape change should degrade to a dropped field or a skipped item, never to a misread identifier. That distinction matters a lot here, because the identifier at stake — dedupKey — is literally what the trash operation acts on. A parser that silently reads the wrong array index into the dedupKey field wouldn't fail loudly; it would trash the wrong photo. Better to skip an item you can't parse cleanly than to guess.

How the calls are actually issued

The critical design point — covered in depth in the next post — is that Photo Cleaner does not construct and sign these batchexecute requests itself the way a bot or scraper would. It loads the real photos.google.com inside an Android WebView, the user logs in on Google's actual page, and the JavaScript running in that page issues the calls:

fetch(url, {
  method: 'POST',
  credentials: 'include',
  body: batchExecuteEnvelope,
})

That's not a simulated browser request. It's a real one, from a real authenticated origin, carrying the same rpcids and payload shapes Google's own front-end sends, because it is Google's own front-end.

What this buys, and what it doesn't

Reverse-engineering batchexecute gets you deletion, trash, and album membership — the operations the public API refuses to expose. It does not get you a stable contract. Google can change an rpcid, reshape a response, or require a new parameter at any time, with no deprecation notice, because nothing here is a published interface. The app has no recourse but to notice the failure and degrade gracefully rather than crash or, worse, act incorrectly.

That instability is the price of doing something the vendor didn't design for third parties to do. Whether that price is worth paying — and how the app is built to fail safely when it inevitably breaks — is a running theme for the rest of this series.

Series: Google Photos Cleaner.