What an APK actually exposes
An APK is a ZIP container, and each kind of file inside it answers a different question. The manifest names the process and the entry point. Compiled resources hold strings and references. DEX files hold the Java and Kotlin bytecode. Native libraries hold the code that runs after a JNI call. Bundled assets can hold certificates, environment files and configuration. When you are looking for a network story you want all of them, because each one can hold a piece that the others do not.
The first step is to split the package and produce a readable view that you can search.
unzip -o app.apk -d apk apktool d app.apk -o out jadx -d out app.apk
Jadx gives you decompiled source you can read and follow. Apktool gives you a resource and Smali view that preserves things the decompiler hides. You usually want both.
Search every layer for the endpoint
Run the same search over the resources, the DEX strings and the native libraries. A base URL, a path fragment, a scheme name or a certificate filename are each a hit. The point is to find where each one is referenced so you can go read the code that uses it.
grep -aohE 'https?://[A-Za-z0-9._/:?&=%-]+' apk/resources.arsc 2>/dev/null | sort -u grep -rnoE 'https?://[^[:space:]"']+' out/sources 2>/dev/null | sort -u strings lib/arm64-v8a/libapp.so 2>/dev/null | grep -oE 'https?://[^[:space:]]+' | sort -u
A hit is evidence that a string exists, not that a request uses it. A route in the DEX may be built from parts, a host from remote configuration, a path from a variable. Treat every hit as a lead and confirm it against the code that builds the request.
Read a hit as a lead
| Where you found it | What it proves | What to do next |
|---|---|---|
| resources.arsc or strings.xml | A base URL is shipped as a resource. | Find the code that loads it, then the request that consumes it. |
| A DEX string | The app builds the path in Java or Kotlin. | Read the request builder and the values interpolated into the path. |
| A native library string | The path or host is built in native code. | Trace the JNI call that returns it, and the encoding it uses. |
| A GraphQL operation name | The app uses a GraphQL endpoint. | Match the operation to a query or a persisted-query hash. |
| A WebSocket scheme or certificate file | The app uses a live connection or a pinned cert. | Plan for a persistent socket or a pinning bypass at capture time. |
Find the request builder, then the interceptor
Retrofit interfaces are the easiest to read because the annotations preserve the shape: the path, the HTTP method and the header names are all in the signature. What they do not preserve is the work done per call. An OkHttp interceptor adds a token, a timestamp, a nonce, a signature or a device header to every request, and that is usually the hard half of the reconstruction.
# which classes build requests grep -rloE '@(GET|POST|PUT|DELETE|PATCH)' out/sources | sort -u # which classes change the request per call grep -rloE 'okhttp3.Interceptor|okhttp3.OkHttpClient|Interceptor.Chain' out/sources | sort -u # the Ktor, Volley or raw names, if the app does not use OkHttp grep -rloE 'KtorClient|Volley|HttpURLConnection' out/sources | sort -u
Follow each candidate to the method that builds the Request. That method is where you see the base URL, the path, the body and the headers actually assembled. The interceptor that sits on the client is where you see the values added at send time.
Record the flow, not a list of URLs
An endpoint list is not a usable API. For each call record the method, the exact path, the query order, the body encoding, the required headers, the cookie jar, the token source and the request order. Request order is not cosmetic. A refresh interceptor can retry a call after replacing the token, which means the same business action sends two different requests. A configuration call can supply a second hostname. A timestamp and a nonce are often generated seconds before the request and included in a signature.
Missing any one of those dependencies makes a copied request fail, and the failure usually points at the wrong thing because it is an HTTP error rather than a missing-value error.
Confirm outside the app
Static analysis shows what the app can do; runtime observation shows what it did on one tested path. Use both. To confirm, rebuild the sequence with a cookie jar, serialise the body exactly as the app does and refresh through the same call the app uses. When the server returns 401 or 403, compare the generated request byte by byte before changing headers at random.
The output you want is a repeatable call with known inputs and stable error handling, not a longer list of captured requests.
Reviewed 30 August 2026 · SReverse research desk