Reference
REST API
29 endpoints over JSON and HTTPS, each with a fixed price in credits. There's no SDK: any HTTP client and a bearer token will do. The OpenAPI spec describes the same API, for generating a client or a set of agent tools.
https://apkcube.com/api/v1On this page
- Endpoints
- Authentication
- Conventions
- Credits
- Errors
- Rate limits
- Workflows
- Account
- GET/me
- Catalogue
- GET/apps
- GET/apps/{pkg}
- GET/apps/{pkg}/versions
- POST/apps/availability
- POST/apps/batch
- GET/developers/{slug}
- GET/categories
- GET/charts
- GET/apps/{pkg}/similar
- Google Play
- GET/apps/{pkg}/play
- GET/apps/{pkg}/reviews
- Integrity
- GET/apps/{pkg}/security
- GET/apps/{pkg}/sdks
- GET/apps/{pkg}/trackers
- GET/apps/{pkg}/protection
- GET/apps/{pkg}/permissions
- GET/apps/{pkg}/manifest
- GET/apps/{pkg}/raw
- GET/apps/{pkg}/signing
- GET/apps/{pkg}/compare
- Lookups
- GET/lookup/hash/{hash}
- GET/lookup/certificate/{fp}
- GET/developers/{slug}/certificates
- Downloads
- POST/apps/{pkg}/fetch
- GET/apps/{pkg}/fetch
- POST/apps/{pkg}/variants
- GET/apps/{pkg}/variants
- POST/apps/{pkg}/download
Endpoints#
- Search appsGET
/apps1 credit - Get an appGET
/apps/{pkg}5 credits - List versionsGET
/apps/{pkg}/versions5 credits - Check availabilityPOST
/apps/availabilityfree - Look up apps in bulkPOST
/apps/batch1 credit - List a publisher's appsGET
/developers/{slug}1 credit - List categoriesGET
/categoriesfree - Get a chartGET
/charts1 credit - List similar appsGET
/apps/{pkg}/similar1 credit
- Get a security reportGET
/apps/{pkg}/security5 credits - List SDKsGET
/apps/{pkg}/sdks5 credits - List trackersGET
/apps/{pkg}/trackers5 credits - Check for a protectorGET
/apps/{pkg}/protection5 credits - List permissionsGET
/apps/{pkg}/permissions5 credits - Read the manifestGET
/apps/{pkg}/manifest5 credits - Get the raw scan recordGET
/apps/{pkg}/raw10 credits - Get the signing historyGET
/apps/{pkg}/signing5 credits - Compare two releasesGET
/apps/{pkg}/compare1 credit
Authentication#
Send your API key as a bearer token with every request. You can create keys at /account/api. Keys look like akc_<prefix>_<secret>. We only store a hash, so we can't recover a lost key. Revoke it and create a new one.
Until you've bought credits, your account can hold 1 active key. Your first top-up raises that to 10.
GET /me is free, so it's the call to test a new key with.
curl -sS "https://apkcube.com/api/v1/me" \
-H "Authorization: Bearer akc_9f3Ka2Lp_xxxxxxxxxxxxxxxxxxxx"const res = await fetch(
"https://apkcube.com/api/v1/me",
{ headers: { Authorization: "Bearer akc_9f3Ka2Lp_xxxxxxxxxxxxxxxxxxxx" } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/me",
headers={"Authorization": "Bearer akc_9f3Ka2Lp_xxxxxxxxxxxxxxxxxxxx"},
)
data = r.json()Conventions#
Rules that hold for every endpoint, so the reference below doesn't repeat them.
- Requests
- JSON in and out, over HTTPS only. Send POST bodies with
Content-Type: application/json. Responses aren't HTTP-cached. Reads come from a cache that each write clears, so a change can take a moment to show. - Package names
- Exactly as Google Play writes them, such as
org.thoughtcrime.securesms, straight into the path. - Build ids
apkIdandvariantIdare strings. Treat them as opaque, and take a fresh one from the versions list rather than storing it for long, since ids can change.- Comparing versions
- Use
versionCode, the build number, which always increases.versionNameis a label: it doesn't sort, and it can be text like "Varies with device". - Timestamps
- ISO 8601 in UTC, such as
2026-10-29T04:18:05.000Z. One exception: the Play listing'sreleasedis Google Play's own display text, in the storefront's language. - Lists
- Numbered pages (search, a publisher's apps, certificate lookup) take
page, starting at 1, and returnpage,perPage,totalandtotalPages. Ranked lists (charts, similar apps) takelimit. Reviews page with acursor. - Unknown vs none
nullmeans we don't know, never "none". An empty array means there really are none. The integrity endpoints rely on this, so keep the two apart in your code.- Availability
- Every app row has
availability:availableorunavailable. Check it before a download so you don't spend 10 credits on a 451. It can't predict a failed security scan, or a takedown that only applies in your country.
Credits#
Each endpoint has a fixed price set in our code, so prices only change with a release and a changelog entry. /me, /categories and availability are always free. Every answer tells you what it cost and what's left, so you never need to poll your balance.
When you run out, you get 402 INSUFFICIENT_CREDITS with the same headers. Requests refused before the charge (400, 401, 429) carry no credit headers and cost nothing, and failures on our side are refunded. The error tables say which is which for every code.
HTTP/2 200
content-type: application/json
x-credits-cost: 5
x-credits-remaining: 475Response headers
| Name | Type | Description |
|---|---|---|
| X-Credits-Cost | integer | Credits this call used. 0 for free endpoints and refunded calls. On a 402 it is the price you couldn't pay. |
| X-Credits-Remaining | integer | Your balance after this call. On free calls other than /me it can be up to 30 seconds old. |
| X-Credits-Low | "1" | Sent when your balance is at or below the low-balance threshold. Check for this header instead of polling /me. |
Most calls charge the same whatever they find. These are free in one case each:
| Call | Free when |
|---|---|
| GET /apps/{pkg}/versions | we hold no builds for the package (an empty list) |
| GET /lookup/hash/{hash} | nothing matches (found: false) |
| GET /lookup/certificate/{fp} | we hold nothing signed by the key (total: 0) |
| GET /developers/{slug}/certificates | we hold no signing record for any of the publisher's apps (total: 0) |
| POST /apps/{pkg}/fetch | a fetch for the app is already running (FETCHING) |
| GET /apps/{pkg}/fetch | the fetch is still running (FETCHING) |
| GET /apps/{pkg}/similar | there is nothing to list (an empty items) |
| POST /apps/{pkg}/variants | a build of that variant is already running (FETCHING) |
| GET /apps/{pkg}/variants | the build is still running (FETCHING) |
| GET /apps/{pkg}/reviews | the page is empty (an empty items) |
Every other answer is charged, including a search or chart with no results, packages in a batch's notFound, a package we don't have and a 451 (except APP_PAID, which is refunded). Some answers that look empty are charged too:
| Call | Charged even when |
|---|---|
| POST /apps/{pkg}/fetch | we already have builds (READY) or Google Play doesn't list the app |
| GET /apps/{pkg}/fetch | no fetch was requested (FETCH_NOT_FOUND) |
| POST /apps/{pkg}/variants | the variant is already built (READY) |
| GET /apps/{pkg}/variants | nobody has requested that variant (NEEDS_FETCH) |
| GET /lookup/certificate/{fp} | every package the key signed has been removed (an empty items) |
| GET /apps/{pkg}/compare | from and to resolve to the same release |
| GET /apps/{pkg}/play | Google Play doesn't list the app (APP_NOT_FOUND) |
Errors#
All errors use the same format. Branch on code, which won't change, rather than on the HTTP status, which several codes share. message is written for people and its wording may change, so show it but don't parse it.
The codes below are grouped by what they cost, which is also roughly what to do next: fix and resend, accept the answer, or retry later.
{
"error": {
"code": "APP_NOT_FOUND",
"message": "No app in the catalogue for `com.example`."
}
}Refused before the charge
Free. Only the 402 carries credit headers. Fix the request, or wait, and send it again.
| Code | Status | What it means |
|---|---|---|
| VALIDATION_ERROR | 400 | A bad package name, a missing q, a malformed apkId or variantId, or a package list that is empty or over 100. One case is charged: compare when from and to resolve to the same release, because that is only known after the read. |
| API_UNAUTHORIZED | 401 | The key is missing, malformed, unknown, revoked or expired, or the account is banned or has no API access. Every case gets the same message, so the response doesn't reveal which keys exist. |
| KEY_SCOPE | 403 | A Developers Console publishing key was sent to a catalogue endpoint. Use a key from your account's API page. |
| INSUFFICIENT_CREDITS | 402 | Your balance is below the call's price. X-Credits-Cost is the price you couldn't pay. Top up and retry. |
| RATE_LIMITED | 429 | Over the per-key rate limit. Wait the number of seconds in Retry-After, then retry. |
Answers about the app
These answer what you asked, so they're charged like any other answer. Sending the same request again gets the same answer.
| Code | Status | What it means |
|---|---|---|
| APP_NOT_FOUND | 404 | We don't have that package; request a fetch to get it. From fetch and the Google Play reads, it means Google Play doesn't list the package at all. |
| DEVELOPER_NOT_FOUND | 404 | No publisher with that slug. Use the developerSlug field from an app instead of building the slug yourself. |
| APK_NOT_FOUND | 404 | That build doesn't exist for this package. List the versions again to get a current id, since ids can change. |
| VERSION_NOT_FOUND | 404 | Compare only: from or to doesn't match a release we hold. The message lists the versions we do have. |
| FETCH_NOT_FOUND | 404 | Fetch status only: nobody has asked us to fetch this package. |
| APP_UNAVAILABLE | 404 | Fetch only: Google Play won't deliver this app to the regions or devices we can reach. |
| APP_BLACKLISTED | 451 | The app was taken down everywhere or in your country. |
| APP_LOW_RATED | 451 | The app's rating is below our minimum. |
| SECURITY_BLOCKED | 451 | The file failed a security scan or is signed with a certificate we block, or its developer withdrew that version. The message says which. |
Refunded
Failures on our side, and answers a free call would have given you. X-Credits-Cost comes back as 0.
| Code | Status | What it means |
|---|---|---|
| NO_VERSIONS | 404 | We know the app but hold no files for it. Returned by the per-build, signing and compare endpoints, and by download without an apkId. Request a fetch. |
| APP_PAID | 451 | A paid app on Google Play that we don't have. Play only delivers paid apps to the buyer. |
| VARIANT_NOT_FOUND | 404 | Download only: the variant build expired or belongs to another app. Request it again. |
| NOT_READY | 409 | Variant request only: we don't have the ordinary build yet. Fetch the app first. |
| UNLIKELY_VERSION | 422 | Variant request only: the @vcN build number isn't written like this app's builds. Compare it with the versions list. |
| DOWNLOADS_DISABLED | 403 | Downloads are turned off for the whole site for now. It isn't your key. Try again later. |
| FETCH_DISABLED | 403 | Fetching new apps is turned off for the whole site for now. Try again later. |
| UPSTREAM_BUSY | 503 | Google Play reads and searches: too many live reads at once. Retry in a minute. |
| UPSTREAM_UNAVAILABLE | 503 | Something failed on our side, or Google Play didn't answer in time. Retry shortly. |
Rate limits#
120 requests per minute per API key. The limit is per key, not per IP address, so other users on a shared network won't affect you. Going over returns 429 RATE_LIMITED with a Retry-After header, and you aren't charged.
Workflows#
The reference below covers one call at a time. These are the sequences most integrations are built from.
Download an app, fetching it first if needed
- GET /apps/{pkg}/versions5 credits
List the builds we hold. An empty list is free and means we don't have the app yet.
- POST /apps/{pkg}/fetch10 credits
If the list is empty, ask us to get the app from Google Play. Skip this when there are builds: a fetch is charged even then.
- GET /apps/{pkg}/fetch1 credit
Poll every 10 to 30 seconds. It's free while the answer is
FETCHING; only the finalREADYorFAILEDis charged. - POST /apps/{pkg}/download10 credits
Mint a signed URL with an empty body for the newest build, or pass an
apkIdfrom step 1. Download the file straight away, without your API key, since the URL carries its own signature. Then check it against the
sha256the mint returned.
15 credits for an app we already hold, 21 credits for one we fetch first.
set -euo pipefail
API="https://apkcube.com/api/v1"
PKG="org.thoughtcrime.securesms"
api() { curl -sS --fail-with-body -H "Authorization: Bearer $APKCUBE_API_KEY" "$@"; }
# 1. Do we hold a build? Free when the list is empty.
count=$(api "$API/apps/$PKG/versions" | jq '.versions | length')
# 2. If not, ask for one and poll. Polling is free while FETCHING.
if [ "$count" -eq 0 ]; then
status=$(api -X POST "$API/apps/$PKG/fetch" | jq -r .status)
while [ "$status" = FETCHING ]; do
sleep 20
status=$(api "$API/apps/$PKG/fetch" | jq -r .status)
done
[ "$status" = READY ] || { echo "Fetch failed" >&2; exit 1; }
fi
# 3. Mint a URL for the newest build, download it, check it.
mint=$(api -X POST "$API/apps/$PKG/download" \
-H "Content-Type: application/json" -d '{}')
file="$PKG.$(jq -r .format <<<"$mint")"
curl -sS --fail -o "$file" "$(jq -r .url <<<"$mint")"
echo "$(jq -r .sha256 <<<"$mint") $file" | shasum -a 256 -c -import { createHash } from "node:crypto";
import { createReadStream, createWriteStream } from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
const API = "https://apkcube.com/api/v1";
const pkg = "org.thoughtcrime.securesms";
async function call(path, init = {}) {
const res = await fetch(API + path, {
...init,
headers: {
Authorization: `Bearer ${process.env.APKCUBE_API_KEY}`,
...init.headers,
},
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body;
}
// 1. Do we hold a build? Free when the list is empty.
const { versions } = await call(`/apps/${pkg}/versions`);
// 2. If not, ask for one and poll. Polling is free while FETCHING.
if (versions.length === 0) {
let { status } = await call(`/apps/${pkg}/fetch`, { method: "POST" });
while (status === "FETCHING") {
await new Promise((r) => setTimeout(r, 20_000));
({ status } = await call(`/apps/${pkg}/fetch`));
}
if (status !== "READY") throw new Error("Fetch failed");
}
// 3. Mint a URL for the newest build, download it, check it.
const mint = await call(`/apps/${pkg}/download`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
});
const file = `${pkg}.${mint.format}`;
const res = await fetch(mint.url); // no API key: the URL is signed
await pipeline(Readable.fromWeb(res.body), createWriteStream(file));
const hash = createHash("sha256");
for await (const chunk of createReadStream(file)) hash.update(chunk);
if (hash.digest("hex") !== mint.sha256) throw new Error("Checksum mismatch");import hashlib, os, time, requests
API = "https://apkcube.com/api/v1"
PKG = "org.thoughtcrime.securesms"
api = requests.Session()
api.headers["Authorization"] = f"Bearer {os.environ['APKCUBE_API_KEY']}"
def call(method, path, **kwargs):
r = api.request(method, API + path, **kwargs)
if not r.ok:
err = r.json()["error"]
raise RuntimeError(f"{err['code']}: {err['message']}")
return r.json()
# 1. Do we hold a build? Free when the list is empty.
if not call("GET", f"/apps/{PKG}/versions")["versions"]:
# 2. Ask for one and poll. Polling is free while FETCHING.
status = call("POST", f"/apps/{PKG}/fetch")["status"]
while status == "FETCHING":
time.sleep(20)
status = call("GET", f"/apps/{PKG}/fetch")["status"]
if status != "READY":
raise RuntimeError("Fetch failed")
# 3. Mint a URL for the newest build, download it, check it.
mint = call("POST", f"/apps/{PKG}/download", json={})
path = f"{PKG}.{mint['format']}"
digest = hashlib.sha256()
# Plain requests.get, not the session: the URL is signed, so no API key.
with requests.get(mint["url"], stream=True) as r, open(path, "wb") as f:
r.raise_for_status()
for chunk in r.iter_content(1 << 20):
f.write(chunk)
digest.update(chunk)
if digest.hexdigest() != mint["sha256"]:
raise RuntimeError("Checksum mismatch")Vet an APK you already have
- GET /lookup/hash/{hash}5 credits
Hash the file (
shasum -a 256 app.apk) and look the checksum up. A match names the package and itsapkId; a miss is free. - GET /apps/{pkg}/security5 credits
Pass that
apkIdfor the scan verdict, signing certificates, permissions and findings of that exact build. - GET /apps/{pkg}/signing5 credits
Check whether the app's signing key has ever been replaced. A file signed by a key the official releases never used is the clearest sign of a repackaged app.
Account#
Check a key and its balance#
/api/v1/mefreeReturns your key id and credit balance. It's free, so use it to check a key before making paid calls.
curl -sS "https://apkcube.com/api/v1/me" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/me",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"keyId": "key:3f6c2a9e-...",
"credits": 480,
"memberSince": "2026-03-04T09:12:00.000Z"
}Catalogue#
Search apps#
/api/v1/apps1 creditSearch by name, publisher or package id. Returns 24 results per page. Searching for an exact package id returns that app.
facets counts the same 60-match pool by type and by category, before your filters, so you can see which filter would narrow a broad search without paying for each filtered search to find out.
A search nobody has run before is sent to Google Play live, so it can take up to 20 seconds. If Google Play doesn't answer in time and the catalogue has no match, you get 503 UPSTREAM_UNAVAILABLE (or UPSTREAM_BUSY) and the credit is refunded, not an empty page. Retry after a few seconds: the search keeps running and saves its results.
curl -sS "https://apkcube.com/api/v1/apps?q=signal&type=app" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps?q=signal&type=app",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps",
params={"q": "signal", "type": "app"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"items": [
{
"packageName": "org.thoughtcrime.securesms",
"name": "Signal Private Messenger",
"developer": "Signal Foundation",
"developerSlug": "signal-foundation",
"categoryId": "communication",
"categoryName": "Communication",
"type": "app",
"version": "8.33.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-10-27T00:00:00.000Z",
"iconUrl": "https://...",
"onGooglePlay": true,
"hasFiles": true,
"availability": "available"
}
],
"matched": 31,
"facets": {
"type": { "game": 0, "app": 31 },
"categories": [
{ "id": "communication", "name": "Communication", "count": 24 },
{ "id": "tools", "name": "Tools", "count": 7 }
]
},
"total": 31,
"page": 1,
"perPage": 24,
"totalPages": 2
}Query parameters
| Name | Type | Description |
|---|---|---|
| qrequired | string | Search text, publisher name or exact package id. |
| type | "game" | "app" | Only return games or only apps. |
| category | string | A category id from /categories. |
| sort | "relevance" | "installs" | "name" | Defaults to relevance. |
| page | integer | Starts at 1. Defaults to 1. |
Get an app#
/api/v1/apps/{packageName}5 creditsThe full listing for an app: description, screenshots, tags, rating and requirements. It doesn't include downloadable files. Use the versions endpoint for those, which is priced separately.
developerVerified is true when the publisher has verified itself through our Developers Console. Its summary, description, what's new and video may then be the publisher's own text rather than Google Play's.
An app that isn't in the catalogue yet is looked up on Google Play live, which can take up to 20 seconds. If Google Play doesn't answer in time you get 503 UPSTREAM_UNAVAILABLE (or UPSTREAM_BUSY), not 404 APP_NOT_FOUND, and the credits are refunded. Retry after a few seconds.
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"name": "Signal Private Messenger",
"developer": "Signal Foundation",
"developerSlug": "signal-foundation",
"categoryId": "communication",
"categoryName": "Communication",
"type": "app",
"version": "8.33.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-10-27T00:00:00.000Z",
"iconUrl": "https://...",
"onGooglePlay": true,
"hasFiles": true,
"availability": "available",
"summary": "A different messaging experience, built on privacy.",
"description": ["Signal is a messaging app ...", "..."],
"descriptionHtml": "<p>Signal is a messaging app ...</p>",
"recentChanges": "Bug fixes and performance improvements.",
"screenshots": ["https://...", "https://..."],
"headerImage": "https://...",
"videoUrl": null,
"requiresAndroid": "5.0 and up",
"tags": ["messaging", "privacy"],
"paid": false,
"price": null,
"editorsChoice": false,
"developerVerified": false
}List versions#
/api/v1/apps/{packageName}/versions5 creditsAll builds we have for a package, newest first. To download a build other than the newest, pass its apkId from here.
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/versions" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/versions",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/versions",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"appName": "Signal Private Messenger",
"versions": [
{
"apkId": "92937",
"versionName": "8.33.2",
"versionCode": 177201,
"arch": "arm64-v8a",
"minSdk": 21,
"fileSize": 92341104,
"sha256": "9c1185a5c5e9fc54612808977ee8f548b2258d31ddcd1a2d5e6a3b9f0e4c7a10",
"format": "apk",
"isLatest": true,
"uploadedAt": "2026-10-29T04:18:05.000Z",
"changelog": null
}
]
}Response fields
| Name | Type | Description |
|---|---|---|
| appName | string | null | The app's name. Null only when the list is empty and we don't know the package at all. |
| versionCode | integer | The build number from Play, which always increases. Compare versions using this. versionName is just a label and doesn't sort correctly. |
| fileSize | integer | Size in bytes. Some files are several gigabytes. |
| sha256 | string | Checksum of the file you'll download. Check your download against it, because the URL signature doesn't cover the file contents. |
| format | "apk" | "xapk" | "apks" | xapk and apks files are ZIPs of split APKs plus assets. adb install can't install them directly. Unzip and use adb install-multiple, or use an installer that supports split APKs. |
| uploadedAt | date-time | When we added the build, not when the developer released it. |
| isLatest | boolean | The newest build we have. Google Play may have a newer one. |
Check availability#
/api/v1/apps/availabilityfreeWhether we have a build of each of up to 100 packages, which version is newest, and the code and name of every version we have. It's free and needs a key like every other call. It's meant for update checkers that poll on a schedule: send every package you track in one call, store each latestVersionCode, and call list versions or get a download URL only when it changes. One call for all your packages counts once against your rate limit.
curl -sS -X POST \
"https://apkcube.com/api/v1/apps/availability" \
-H "Authorization: Bearer $APKCUBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"packageNames":["org.thoughtcrime.securesms","com.example.nope","com.example.paid"]}'const res = await fetch(
"https://apkcube.com/api/v1/apps/availability",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APKCUBE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ packageNames: ["org.thoughtcrime.securesms","com.example.nope","com.example.paid"] }),
},
);
const data = await res.json();import os, requests
r = requests.post(
"https://apkcube.com/api/v1/apps/availability",
json={"packageNames": ["org.thoughtcrime.securesms","com.example.nope","com.example.paid"]},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"items": [
{
"packageName": "org.thoughtcrime.securesms",
"appName": "Signal Private Messenger",
"available": true,
"latestVersionName": "8.33.2",
"latestVersionCode": 177201,
"latestUploadedAt": "2026-10-29T04:18:05.000Z",
"url": "https://apkcube.com/go?q=org.thoughtcrime.securesms",
"versions": [
{ "versionCode": 177201, "versionName": "8.33.2" },
{ "versionCode": 176902, "versionName": "8.32.3" },
{ "versionCode": 176601, "versionName": "8.31.2" }
]
}
],
"notFound": ["com.example.nope"],
"refused": [
{ "packageName": "com.example.paid", "code": "APP_PAID", "message": "This is a paid app on Google Play and we hold no file for it. ..." }
]
}Request body
| Name | Type | Description |
|---|---|---|
| packageNamesrequired | string[] | 1 to 100 package names. One malformed name fails the whole call with a 400. |
Fields on each item
| Name | Type | Description |
|---|---|---|
| packageName | string | The package, as you sent it. |
| appName | string | The app's name. |
| available | boolean | True when we have at least one build you can download. False means we know the app but don't have a build yet. |
| latestVersionCode | integer | null | The newest build's version code. Compare against this to detect an update, not against the name. |
| latestVersionName | string | null | The newest build's version name, for display. |
| latestUploadedAt | date-time | null | When we added that build, not when the developer released it. |
| url | string | The app's page on this site. |
| versions | { versionCode, versionName }[] | Every version we have, highest version code first, one entry per code. Empty when available is false. To download a specific one, take its apkId from list versions. |
Look up apps in bulk#
/api/v1/apps/batch1 creditLook up to 100 packages in one call. Each result is the same summary you get from search. Use this instead of calling /apps/{pkg} in a loop, for example when checking all the apps on a phone.
curl -sS -X POST \
"https://apkcube.com/api/v1/apps/batch" \
-H "Authorization: Bearer $APKCUBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"packageNames":["org.thoughtcrime.securesms","com.whatsapp","com.example.nope"]}'const res = await fetch(
"https://apkcube.com/api/v1/apps/batch",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APKCUBE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ packageNames: ["org.thoughtcrime.securesms","com.whatsapp","com.example.nope"] }),
},
);
const data = await res.json();import os, requests
r = requests.post(
"https://apkcube.com/api/v1/apps/batch",
json={"packageNames": ["org.thoughtcrime.securesms","com.whatsapp","com.example.nope"]},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"items": [
{ "packageName": "org.thoughtcrime.securesms", "name": "Signal Private Messenger", "...": "..." },
{ "packageName": "com.whatsapp", "name": "WhatsApp Messenger", "...": "..." }
],
"notFound": ["com.example.nope"]
}List a publisher's apps#
/api/v1/developers/{slug}1 creditAll apps from one publisher, most installed first. Get the slug from the developerSlug field on any app; it's null for the few apps with no publisher name.
curl -sS "https://apkcube.com/api/v1/developers/signal-foundation" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/developers/signal-foundation",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/developers/signal-foundation",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"developer": {
"slug": "signal-foundation",
"name": "Signal Foundation",
"total": 2
},
"items": [
{
"packageName": "org.thoughtcrime.securesms",
"name": "Signal Private Messenger",
"developer": "Signal Foundation",
"developerSlug": "signal-foundation",
"categoryId": "communication",
"categoryName": "Communication",
"type": "app",
"version": "8.33.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-10-27T00:00:00.000Z",
"iconUrl": "https://...",
"onGooglePlay": true,
"hasFiles": true,
"availability": "available"
}
],
"page": 1,
"perPage": 24,
"total": 2,
"totalPages": 1
}Query parameters
| Name | Type | Description |
|---|---|---|
| page | integer | Starts at 1. Defaults to 1. |
| per | integer | Results per page, 1 to 96. Defaults to 24. |
List categories#
/api/v1/categoriesfreeThe list of categories you can filter searches by. This call is free.
curl -sS "https://apkcube.com/api/v1/categories" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/categories",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/categories",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"categories": [
{ "id": "adventure", "name": "Adventure", "type": "game" },
{ "id": "puzzle", "name": "Puzzle", "type": "game" },
{ "id": "communication", "name": "Communication", "type": "app" },
{ "id": "tools", "name": "Tools", "type": "app" }
]
}Get a chart#
/api/v1/charts1 creditA ranked chart. hot, grossing, latest and editors-choice are Google Play's charts, and new is the newest releases in the catalogue. Two come from this site's own traffic: popular is what people downloaded here in about the last day, and updated is the newest builds we stored.
curl -sS "https://apkcube.com/api/v1/charts?section=hot&type=app" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/charts?section=hot&type=app",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/charts",
params={"section": "hot", "type": "app"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"section": "hot",
"items": [
{
"packageName": "org.thoughtcrime.securesms",
"name": "Signal Private Messenger",
"developer": "Signal Foundation",
"developerSlug": "signal-foundation",
"categoryId": "communication",
"categoryName": "Communication",
"type": "app",
"version": "8.33.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-10-27T00:00:00.000Z",
"iconUrl": "https://...",
"onGooglePlay": true,
"hasFiles": true,
"availability": "available"
}
]
}Query parameters
| Name | Type | Description |
|---|---|---|
| section | string | One of hot, grossing, latest, new, editors-choice, popular, updated. Defaults to hot, which is also what an unknown section returns. |
| type | "game" | "app" | Only games or only apps. Ignored by popular, which mixes both. |
| limit | integer | Rows to return, 1 to 50. Defaults to 24. |
List similar apps#
/api/v1/apps/{packageName}/similar1 creditApps related to one package: alternatives and neighbours, as the app's page on the site shows them. It's free when there is nothing to list (an empty items). A package that isn't in the catalogue returns 404 APP_NOT_FOUND, and one that was taken down returns 451 APP_BLACKLISTED.
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/similar?limit=8" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/similar?limit=8",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/similar",
params={"limit": "8"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"items": [
{
"packageName": "org.thoughtcrime.securesms",
"name": "Signal Private Messenger",
"developer": "Signal Foundation",
"developerSlug": "signal-foundation",
"categoryId": "communication",
"categoryName": "Communication",
"type": "app",
"version": "8.33.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-10-27T00:00:00.000Z",
"iconUrl": "https://...",
"onGooglePlay": true,
"hasFiles": true,
"availability": "available"
}
]
}Query parameters
| Name | Type | Description |
|---|---|---|
| limit | integer | Rows to return, 1 to 24. Defaults to 8. |
Google Play#
Google Play's own data, read live: the full store listing in any country and language, and user reviews. Both work for any package Google Play lists, including ones that aren't in the catalogue. If Google Play is busy or doesn't answer, you get 503 UPSTREAM_BUSY or 503 UPSTREAM_UNAVAILABLE (502 when its answer can't be read) and the credits are refunded. A taken-down app returns 451 APP_BLACKLISTED.
Get the Play listing#
/api/v1/apps/{packageName}/play5 creditsThe store data the app record leaves out: rating histogram, install estimate, in-app purchase price range, release date, declared permissions, content rating details, and the publisher's legal name and contact details. Pass lang and country to read another storefront; prices, text and availability differ between them.
A few fields only Google Play's web page publishes are usually null: the written-review count, sale pricing and the Play Pass and pre-registration flags.
For en/us we return our own copy while it's less than a day old (source: "catalog"). Otherwise we read Google Play, save the result, and return it (source: "live"). Other storefronts are always read live and cached for a few hours. It's charged even when Google Play doesn't list the app (APP_NOT_FOUND).
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/play?country=de&lang=de" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/play?country=de&lang=de",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/play",
params={"country": "de", "lang": "de"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"lang": "de",
"country": "de",
"source": "live",
"fetchedAt": "2026-10-04T08:12:31.000Z",
"title": "Signal – Sicherer Messenger",
"developer": { "name": "Signal Foundation", "legalName": "Signal Technology Foundation", ... },
"contentRating": "USK ab 0 Jahren",
"released": "17.05.2010",
"updatedAt": "2026-10-27T00:00:00.000Z",
"rating": {
"score": 4.48,
"scoreText": "4.5",
"count": 2214509,
"reviews": null,
"histogram": { "1": 140221, "2": 41870, "3": 82044, "4": 206118, "5": 1744256 }
},
"installs": { "text": "100.000.000+", "min": 100000000, "max": null },
"pricing": {
"free": true, "price": 0, "currency": "EUR",
"inAppPurchases": false, "iapRange": null, "adSupported": null, ...
},
"permissions": ["android.permission.CAMERA", "android.permission.RECORD_AUDIO", ...],
...
}Query parameters
| Name | Type | Description |
|---|---|---|
| lang | string | Language code, e.g. en, de, pt-BR. Defaults to en. |
| country | string | Two-letter storefront, e.g. us, de, jp. Defaults to us. |
List Play reviews#
/api/v1/apps/{packageName}/reviews5 creditsOne page of user reviews, read live from Google Play. We don't store reviews; a page is cached for 15 minutes. Each review has its stars, text, helpful votes, the app version the reviewer had, and the developer's reply.
To read more, pass nextCursor back as cursor with the same sort, num and storefront. nextCursor is null on the last page. Every page is charged, so only ask for what you need. It's free when the page is empty (an empty items).
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/reviews?sort=newest&num=20" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/reviews?sort=newest&num=20",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/reviews",
params={"sort": "newest", "num": "20"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"lang": "en",
"country": "us",
"sort": "newest",
"items": [
{
"id": "5f1c2a…",
"author": "A Google user",
"authorImage": "https://...",
"postedAt": "2026-10-03T21:44:09.000Z",
"score": 5,
"text": "Fast, private, and the new backups work well.",
"thumbsUp": 12,
"appVersion": "8.32.3",
"reply": null,
"url": "https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&reviewId=5f1c2a…"
}
],
"nextCursor": "Q2tJSUJCSUNDQkVR…",
"fetchedAt": "2026-10-04T08:12:31.000Z"
}Query parameters
| Name | Type | Description |
|---|---|---|
| sort | string | newest (default), rating, or helpfulness. |
| num | integer | Reviews per page, 1 to 100. Default 40. |
| cursor | string | nextCursor from the previous page. Omit it for the first page. |
| lang | string | Language code, e.g. en, ja, pt-BR. Default en. |
| country | string | Two-letter storefront, e.g. us, jp. Default us. |
Integrity#
These endpoints tell you whether a file is what it claims to be and what's inside it. Check them before recommending an install. There are nine, and they use the same scan and signature data as our download pages, so the API and the website always agree.
Get a security report#
/api/v1/apps/{packageName}/security5 creditsThe key facts about one build: its checksum, antivirus result, signing certificates, and every permission it declares with a plain description of what it allows. Sensitive permissions are listed first.
findings lists issues we flag from the manifest and natives fields, so you don't need to know Android internals to spot them: debuggable, version-mismatch, cleartext-traffic, backup-allowed, shared-user-id, exported-components, legacy-permission-model and packed. Use the id in your code, not the text. The raw fields are included too if you want to apply your own rules.
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/security" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/security",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/security",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"apkId": "92937",
"versionName": "8.33.2",
"versionCode": 177201,
"verdict": "verified",
"scanned": true,
"scannedAt": "2026-10-29T05:40:05.000Z",
"sha256": "9c1185a5c5e9fc54612808977ee8f548b2258d31ddcd1a2d5e6a3b9f0e4c7a10",
"sha1": "b3d4...",
"md5": "0c7e...",
"virusTotal": { "vendors": 68, "flagged": 0, "flaggedBy": [], "meta": null },
"manifest": { "targetSdk": 35, "allowBackup": true, "...": "..." },
"natives": { "abis": ["arm64-v8a"], "libs": [...], "protectors": [] },
"signature": {
"scheme": "v3",
"status": "verified",
"match": "known",
"certs": [
{
"sha256": "29f34e5f...",
"sha1": "45989dc9...",
"subject": "CN=Signal, O=Signal Foundation",
"issuer": "CN=Signal, O=Signal Foundation",
"scheme": "v3",
"match": "known",
"validFrom": "2011-03-25T17:11:30.000Z",
"validUntil": "2038-08-10T17:11:30.000Z",
"keyAlgorithm": "RSA",
"keySize": 2048,
"debugKey": false,
"findings": []
}
],
"blocked": false,
"knownSignatures": 1
},
"permissions": [
{
"name": "android.permission.CAMERA",
"label": "Camera",
"sensitive": true,
"category": "Camera & microphone",
"description": "Takes photos and records video with the device cameras.",
"standard": true
}
],
"permissionSummary": "31 declared · 8 sensitive",
"findings": [
{
"id": "backup-allowed",
"severity": "notice",
"title": "Backup allowed",
"detail": "The manifest allows the platform to back the app's private data up, which on a device with USB debugging on can be pulled off with adb. …"
}
],
"pairip": false
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have. |
List SDKs#
/api/v1/apps/{packageName}/sdks5 creditsEvery third-party library we recognise in a build, with its vendor. Advertising, analytics, attribution, crash reporting, push and sign-in libraries get a category; the rest (HTTP clients, UI toolkits and so on) have category: null. evidence shows where we found it, such as a file in lib/, a class in the manifest or a package name in the app's code. A library found in two places is a stronger match than one found in one.
This is about privacy, not security. Most apps include ad or analytics SDKs, and that's normal. This list just shows what else is bundled in the app, which app stores don't tell you.
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/sdks" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/com.example.game/sdks",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/com.example.game/sdks",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.example.game",
"apkId": "51904",
"versionName": "4.8.1",
"versionCode": 40801,
"analyzed": true,
"summary": "20 third-party SDKs recognised; the app's own code was read in full.",
"sdks": [
{
"id": "a5f1c...",
"name": "Google AdMob",
"vendor": "Google",
"category": "advertising",
"categoryLabel": "Advertising",
"evidence": ["activity", "dex"]
},
{
"id": "7c0be...",
"name": "Firebase Analytics",
"vendor": "Google",
"category": "analytics",
"categoryLabel": "Analytics",
"evidence": ["dex"]
}
],
"byCategory": [
{ "category": "advertising", "label": "Advertising", "count": 3 },
{ "category": "analytics", "label": "Analytics", "count": 2 },
{ "category": "crash", "label": "Crash reporting", "count": 1 }
],
"sources": ["manifest", "natives", "dex"],
"codeCoverage": "full",
"dex": { "total": 4, "read": 4, "bytes": 21504312, "truncated": false },
"capped": [],
"protectors": [],
"complete": true,
"rules": "libchecker-48@8dd903f"
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have. |
List trackers#
/api/v1/apps/{packageName}/trackers5 creditsThe same data as /sdks, but only libraries that normally send data about the device or user somewhere: advertising, analytics, install attribution, crash reporting, push messaging and social sign-in. Use this to answer “what does this app send about me?” Use /sdks for the full list.
unclassified counts the other libraries we found, like HTTP clients, UI toolkits and JSON parsers. That way the totals still add up to what /sdks reports. Unclassified doesn't mean harmless. We just don't put those libraries in a category.
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/trackers" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/com.example.game/trackers",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/com.example.game/trackers",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.example.game",
"apkId": "51904",
"versionName": "4.8.1",
"versionCode": 40801,
"analyzed": true,
"summary": "6 advertising, analytics or tracking SDKs recognised; the app's own code was read in full.",
"trackers": [
{
"id": "a5f1c...",
"name": "Google AdMob",
"vendor": "Google",
"category": "advertising",
"categoryLabel": "Advertising",
"evidence": ["activity", "dex"]
}
],
"byCategory": [
{ "category": "advertising", "label": "Advertising", "count": 3 },
{ "category": "analytics", "label": "Analytics", "count": 2 },
{ "category": "crash", "label": "Crash reporting", "count": 1 }
],
"total": 6,
"unclassified": 14,
"codeCoverage": "full",
"dex": { "total": 4, "read": 4, "bytes": 21504312, "truncated": false },
"capped": [],
"protectors": [],
"complete": true,
"rules": "libchecker-48@8dd903f"
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have. |
Check for a protector#
/api/v1/apps/{packageName}/protection5 creditsWhether the build uses a commercial protector such as DexProtector, Jiagu, Bangcle, Legu, AppSealing or Google's PairIP, and how that limits the other checks. If a build is protected, an empty SDK list doesn't tell you much.
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/protection" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/com.example.game/protection",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/com.example.game/protection",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.example.game",
"apkId": "51904",
"versionName": "4.8.1",
"versionCode": 40801,
"protected": true,
"protectors": [{ "id": "jiagu", "name": "Jiagu", "vendor": "Qihoo 360" }],
"pairip": false,
"analyzed": true,
"codeCoverage": "none",
"dex": null,
"capped": [],
"summary": "This build is protected with Jiagu (Qihoo 360). That is anti-piracy and anti-tamper tooling, not a sign of harm..."
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have. |
List permissions#
/api/v1/apps/{packageName}/permissions5 creditsThe permissions a build requests, sensitive ones first. Each has a readable name, a category and a short description of what it lets the app do. The labels match /compare and our download pages.
defines is easy to misread. It lists permissions this app creates for other apps to use, not permissions it requests.
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/permissions" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/com.example.game/permissions",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/com.example.game/permissions",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.example.game",
"apkId": "51904",
"versionName": "4.8.1",
"versionCode": 40801,
"permissions": [
{
"name": "android.permission.ACCESS_FINE_LOCATION",
"label": "Precise location",
"sensitive": true,
"category": "Location",
"description": "Reads your exact position from GPS and nearby networks, accurate to a few metres.",
"standard": true
}
],
"summary": "24 declared · 5 sensitive",
"declared": 24,
"sensitive": 5,
"byCategory": [
{ "category": "Network", "count": 6, "sensitive": 0 },
{ "category": "Location", "count": 3, "sensitive": 2 }
],
"defines": ["com.example.game.permission.C2D_MESSAGE"]
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have. |
Read the manifest#
/api/v1/apps/{packageName}/manifest5 creditsOther manifest settings besides permissions, the native libraries in the build, and the findings we flag from both. If a key is missing inside manifest, the manifest doesn't set it. Don't assume that means the Android default.
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/manifest" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/com.example.game/manifest",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/com.example.game/manifest",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.example.game",
"apkId": "51904",
"versionName": "4.8.1",
"versionCode": 40801,
"manifest": {
"versionCode": 40801,
"versionName": "4.8.1",
"targetSdk": 34,
"compileSdk": 34,
"usesCleartextTraffic": true,
"allowBackup": true,
"definesPermissions": ["com.example.game.permission.C2D_MESSAGE"],
"usesFeatures": ["android.hardware.touchscreen"],
"usesLibraries": [],
"exportedUnguarded": [{ "kind": "activity", "name": ".MainActivity" }]
},
"natives": {
"abis": ["arm64-v8a"],
"libs": [{ "name": "libunity.so", "size": 18452992, "abis": ["arm64-v8a"] }],
"protectors": []
},
"findings": [
{
"id": "cleartext-traffic",
"severity": "notice",
"title": "Cleartext traffic allowed",
"detail": "The manifest opts in to unencrypted HTTP..."
},
{ "id": "backup-allowed", "severity": "notice", "...": "..." },
{ "id": "exported-components", "severity": "notice", "...": "..." }
],
"versionMismatch": null
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have. |
Get the raw scan record#
/api/v1/apps/{packageName}/raw10 creditsThe other endpoints in this section apply our rules to the scan data. This one gives you the stored record so you can apply your own. Nothing is interpreted: field names are camelCased, the VirusTotal and signature columns are grouped into virusTotal and signature, and the checksums come from the build itself (sha1 falls back to VirusTotal's for older builds). A null means the scanner left it empty.
It also includes two things you won't find elsewhere. The first is the evidence digest our SDK detection uses (dex package prefixes, manifest component names, intent actions and how much code was read), so you can run your own detection rules. The second is each certificate's serial and sigAlgorithm.
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/raw" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/com.example.game/raw",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/com.example.game/raw",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.example.game",
"apkId": "51904",
"versionName": "4.8.1",
"versionCode": 40801,
"scanned": true,
"status": "COMPLETED",
"verdict": "secure",
"scannedAt": "2026-09-02T11:20:41.000Z",
"sha256": "9f2b...",
"sha1": "3ac1...",
"md5": "0b41...",
"virusTotal": {
"found": true, "malicious": 0, "suspicious": 0, "harmless": 41,
"undetected": 26, "total": 67, "scanDate": "2026-09-01T22:02:00.000Z",
"flagged": [],
"meta": { "firstSeen": "2026-08-30T09:11:00.000Z", "timesSubmitted": 3 }
},
"certs": [
{
"scheme": "v3", "sha256": "ab12...", "sha1": "7d4f...",
"subject": "CN=Example, O=Example Ltd", "issuer": "CN=Example, O=Example Ltd",
"serial": "01f3c9", "keyAlgorithm": "RSA", "keySize": 2048,
"sigAlgorithm": "SHA256withRSA", "match": "known"
}
],
"signature": { "scheme": "v3", "status": "verified", "certSha256": "ab12...", "certSubject": "CN=Example, O=Example Ltd", "certMatch": "known" },
"permissions": ["android.permission.INTERNET"],
"manifest": { "targetSdk": 34 },
"natives": { "abis": ["arm64-v8a"], "libs": [], "protectors": [] },
"pairip": false,
"evidence": { "v": 1, "depth": 3, "sources": ["manifest", "natives", "dex"], "dex": ["com.google.android.gms"], "dexCoverage": { "total": 4, "read": 4, "bytes": 21504312, "truncated": false } },
"evidenceVersion": 1
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have. |
Get the signing history#
/api/v1/apps/{packageName}/signing5 creditsWhich certificate signed each release, and when the key changed. Android only installs an update if it's signed with the same key as the installed app, so this tells you whether a build is a real update or a different app using the same package name. App stores don't publish this. We build it from the certificates we have on record and the signatures in each file.
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/signing" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/signing",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/signing",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"summary": "One signing key across 3 archived releases.",
"hasKeyReplacement": false,
"eras": [
{
"certShas": ["29f34e5f..."],
"versions": ["8.33.2", "8.32.3", "8.31.2"],
"firstSeen": "2026-10-17T08:39:30.000Z",
"lastSeen": "2026-10-29T04:18:05.000Z",
"transition": null
}
],
"unreadVersions": [],
"signers": [
{
"sha256": "29f34e5f...",
"subject": "CN=Signal, O=Signal Foundation",
"trusted": true,
"addedVia": "google-play",
"firstSeenAt": "2025-11-02T08:11:00.000Z",
"inArchive": true
}
]
}Compare two releases#
/api/v1/apps/{packageName}/compare1 creditWhat changed between two releases: added and removed permissions, signing key changes, size difference and minimum Android version. Size is compared using one architecture that both releases have, so differences in which files we store don't skew the result.
It's charged even when from and to resolve to the same release: that comes back as 400 VALIDATION_ERROR, because it's only known after the read.
curl -sS "https://apkcube.com/api/v1/apps/com.spotify.music/compare?from=8.9.72.492&to=8.9.80.518" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/com.spotify.music/compare?from=8.9.72.492&to=8.9.80.518",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/com.spotify.music/compare",
params={"from": "8.9.72.492", "to": "8.9.80.518"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.spotify.music",
"version": "8.9.80.518",
"previousVersion": "8.9.72.492",
"permissionsAdded": [
{
"name": "android.permission.POST_NOTIFICATIONS",
"label": "Show notifications",
"sensitive": false,
"category": "System",
"description": "Sends notifications, once you allow them.",
"standard": true
}
],
"permissionsRemoved": [],
"permissionsKnown": true,
"signerChange": "same",
"signerChangeLabel": "Same signing key",
"signerChangeDetail": "Signed by the same certificate as the previous release.",
"newCertShas": [],
"size": { "arch": "arm64-v8a", "bytes": 4118272 },
"minSdk": { "from": 21, "to": 23 },
"noteworthy": false
}Query
| Name | Type | Description |
|---|---|---|
| from | string | One release, by version name or version code. Required. We try the version code first, because version names are free text and can be things like 'Varies with device'. |
| to | string | The other release. Required. Order doesn't matter. The higher build number is always treated as the newer one. |
Lookups#
The endpoints above all need a package name. The first two work from a file checksum or a signing key fingerprint instead, for example one taken from a device, a threat feed or a security report. The third lists every key behind one publisher's apps.
Find a build by checksum#
/api/v1/lookup/hash/{hash}5 creditsFind the app and build that match a file checksum. Accepts MD5 (32 hex characters), SHA-1 (40) or SHA-256 (64), and detects the type from the length. Colons, spaces and uppercase letters are fine.
A match gives you the apkId, which you can pass to any of the integrity endpoints above.
curl -sS "https://apkcube.com/api/v1/lookup/hash/9f2b8c1d5e6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4ce4a7" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/lookup/hash/9f2b8c1d5e6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4ce4a7",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/lookup/hash/9f2b8c1d5e6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4ce4a7",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"hash": "9f2b8c1d5e6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4ce4a7",
"algorithm": "sha256",
"found": true,
"match": {
"packageName": "com.example.game",
"appName": "Example Game",
"apkId": "51904",
"versionName": "4.8.1",
"versionCode": 40801,
"arch": "arm64-v8a",
"format": "apk",
"fileSize": 48213344,
"sha256": "9f2b8c1d5e6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4ce4a7",
"isLatest": true,
"uploadedAt": "2026-08-30T09:02:11.000Z"
},
"note": "SHA-256 is the digest every stored build carries, so a miss here means we hold no such file."
}Find apps by signing key#
/api/v1/lookup/certificate/{fingerprint}5 creditsAll packages we have that were signed with a given certificate. Since Android requires updates to use the same signing key, the key works like an identity for the developer. If you find a key on one repackaged app, it has often signed others too.
Send the SHA-256 of the DER certificate, with or without colons. SHA-1 is not supported and is rejected without charge, because we only store SHA-256 fingerprints. keytool prints both.
curl -sS "https://apkcube.com/api/v1/lookup/certificate/ab12cd34ef567890ab12cd34ef567890ab12cd34ef567890ab12cd34ef56ef90" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/lookup/certificate/ab12cd34ef567890ab12cd34ef567890ab12cd34ef567890ab12cd34ef56ef90",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/lookup/certificate/ab12cd34ef567890ab12cd34ef567890ab12cd34ef567890ab12cd34ef56ef90",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"fingerprint": "ab12cd34ef567890ab12cd34ef567890ab12cd34ef567890ab12cd34ef56ef90",
"algorithm": "sha256",
"subject": "CN=Example, O=Example Ltd",
"blocked": false,
"blockedCount": 1,
"items": [
{
"packageName": "com.example.game",
"appName": "Example Game",
"firstSeenAt": "2026-07-14T08:33:02.000Z",
"trusted": true,
"addedVia": "google-play"
},
...
],
"page": 1,
"perPage": 24,
"total": 9,
"totalPages": 1,
"note": "Drawn from the signing record we keep per package..."
}Query
| Name | Type | Description |
|---|---|---|
| page | integer | Starts at 1. Default 1. |
| per | integer | Results per page, 1 to 96. Default 24. Larger values are capped at 96. |
List a publisher's signing keys#
/api/v1/developers/{slug}/certificates5 creditsThe certificate lookup in reverse: every signing key behind one publisher's apps. The slug comes from developerSlug on any app. Most publishers have one key. A second key, or a key on only one of their apps, is worth a closer look.
elsewhere counts packages outside this publisher that the same key also signs. Zero is normal. Above zero means the key is shared between the publisher's accounts, or other people's builds were signed with it. Pass its certSha256 to the certificate lookup to see which.
curl -sS "https://apkcube.com/api/v1/developers/example-studio/certificates" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/developers/example-studio/certificates",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/developers/example-studio/certificates",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"developer": { "slug": "example-studio", "name": "Example Studio" },
"total": 2,
"items": [
{
"certSha256": "ab12cd34ef567890ab12cd34ef567890ab12cd34ef567890ab12cd34ef56ef90",
"subject": "CN=Example, O=Example Ltd",
"packages": 14,
"elsewhere": 0,
"firstSeenAt": "2026-03-02T11:20:45.000Z",
"trusted": true,
"blocked": false
},
{
"certSha256": "77e0a1b2...3c4d",
"subject": "CN=Android Debug",
"packages": 1,
"elsewhere": 212,
"firstSeenAt": "2026-09-30T06:12:09.000Z",
"trusted": false,
"blocked": false
}
],
"note": "Drawn from the signing record we keep per package..."
}Downloads#
Request a fetch#
/api/v1/apps/{packageName}/fetch10 creditsAsk us to download an app from Google Play when the versions list comes back empty. There's no request body. A fetch usually takes a few minutes.
We check the app on Google Play first, so a package name that doesn't exist returns 404 APP_NOT_FOUND instead of starting a fetch that can't succeed. Paid apps and apps we won't serve get the same 451 codes as the versions endpoint, and 404 APP_UNAVAILABLE means Google Play won't deliver the app to us. All of those are charged. If Google Play doesn't answer that check in time, you get the app endpoint's refunded 503 instead, and can retry.
curl -sS -X POST \
"https://apkcube.com/api/v1/apps/com.example.newapp/fetch" \
-H "Authorization: Bearer $APKCUBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'const res = await fetch(
"https://apkcube.com/api/v1/apps/com.example.newapp/fetch",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APKCUBE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
const data = await res.json();import os, requests
r = requests.post(
"https://apkcube.com/api/v1/apps/com.example.newapp/fetch",
json={},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "com.example.newapp",
"status": "FETCHING"
}Response fields
| Name | Type | Description |
|---|---|---|
| status | "READY" | "FETCHING" | READY means we already have builds, so list the versions. FETCHING means a fetch is queued or running. |
Check a fetch#
/api/v1/apps/{packageName}/fetch1 creditCheck on a fetch you started. It's free when the fetch is still running (FETCHING), so poll this every 10 to 30 seconds instead of the POST. The final answer (READY or FAILED) costs 1 credit, and so does a check when no fetch was requested (FETCH_NOT_FOUND). READY can also mean Google Play confirmed the build we have is already the newest, so list the versions to see what you got.
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/fetch" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/fetch",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/fetch",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"status": "READY",
"updatedAt": "2026-10-01T09:14:27.000Z"
}Response fields
| Name | Type | Description |
|---|---|---|
| status | "FETCHING" | "READY" | "FAILED" | FETCHING: wait and ask again. READY: list the versions. FAILED: see error. |
| error | string | FAILED only. Why the fetch failed, worded for people. |
| terminal | true | FAILED only. Present when Google Play won't deliver the app, so another fetch won't help. When it's missing, you can POST again to retry. |
| updatedAt | date-time | When the fetch last changed state. |
Request a variant build#
/api/v1/apps/{packageName}/variants10 creditsThe ordinary build is 64-bit ARM with the 24 most common languages. This asks for a build it doesn't cover, of the newest release unless a suffix below picks another:
| Variant | What you get |
|---|---|
armeabi-v7a | The 32-bit ARM build, for older phones. |
universal | Both ARM ABIs in one bundle. |
+all-langs | Suffix: every language Google Play has, not only the standard 24. |
@sdk28 | Suffix: the newest build that runs on that Android API level (24 to 37). |
@vc1234 | Suffix: one exact older build, by its build number (versionCode). It must be written like the builds we know (the ones in the versions list, and the one Google Play lists now): no more digits than the newest, at most one fewer than the oldest. |
device:pixel-9a | Exactly what Google Play serves that device. |
curl -sS -X POST \
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/variants" \
-H "Authorization: Bearer $APKCUBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"variant":"universal+all-langs"}'const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/variants",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APKCUBE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ variant: "universal+all-langs" }),
},
);
const data = await res.json();import os, requests
r = requests.post(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/variants",
json={"variant": "universal+all-langs"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"variant": "universal+all-langs",
"status": "FETCHING"
}Request body
| Name | Type | Description |
|---|---|---|
| variantrequired | string | armeabi-v7a, universal or arm64-v8a, optionally followed by +all-langs and then one of @sdkNN or @vcN (arm64-v8a needs at least one suffix, since alone it is the ordinary build); or device:<slug> for a device profile we know, optionally followed by +all-langs. |
Check a variant build#
/api/v1/apps/{packageName}/variants1 creditCheck on a variant build. It's free when the build is still running (FETCHING), so poll this every 10 to 30 seconds instead of the POST. Every other answer costs 1 credit, including when nobody has requested that variant (NEEDS_FETCH). In a query string, write the + as %2B. A bare + also works. Pass build.variantId to the download call.
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/variants?variant=universal+all-langs" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/variants?variant=universal+all-langs",
{ headers: { Authorization: `Bearer ${process.env.APKCUBE_API_KEY}` } },
);
const data = await res.json();import os, requests
r = requests.get(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/variants",
params={"variant": "universal+all-langs"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"packageName": "org.thoughtcrime.securesms",
"variant": "universal+all-langs",
"status": "READY",
"build": {
"variantId": "9174",
"variant": "universal+all-langs",
"arch": "arm64-v8a+armeabi-v7a",
"versionName": "8.33.2",
"versionCode": 177201,
"minSdk": 24,
"fileSize": 118204331,
"sha256": "9a1f03e2...",
"format": "apks",
"expiresAt": "2026-11-05T05:53:05.000Z"
}
}Response fields
| Name | Type | Description |
|---|---|---|
| status | "READY" | "FETCHING" | "FAILED" | "NEEDS_FETCH" | READY: build is live, mint it. FETCHING: wait and ask again. FAILED: see error, and POST to retry. NEEDS_FETCH: nobody has asked for it, or it expired. |
| build | object | READY only. variantId, variant, arch, versionName, versionCode, minSdk, fileSize, sha256, format and expiresAt. |
| error | string | FAILED only. Why the build failed, worded for people. |
Get a download URL#
/api/v1/apps/{packageName}/download10 creditsGet a short-lived signed URL for one build. This is the only call that gives you a file. The URL's host depends on where the file is stored, so don't pin it.
Unlike the website, there's no captcha, since every download costs credits. The other checks still apply: takedowns, paid apps, minimum rating, blocked signing keys, the malware scan, and versions their developer withdrew.
curl -sS -X POST \
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/download" \
-H "Authorization: Bearer $APKCUBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'const res = await fetch(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/download",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APKCUBE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
},
);
const data = await res.json();import os, requests
r = requests.post(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/download",
json={},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"url": "https://<account>.r2.cloudflarestorage.com/apk-files/...&X-Amz-Signature=...",
"expiresAt": "2026-10-29T10:18:05.000Z",
"packageName": "org.thoughtcrime.securesms",
"appName": "Signal Private Messenger",
"apkId": "92937",
"fileSize": 92341104,
"sha256": "4e0b7c1d...",
"versionName": "8.33.2",
"versionCode": 177201,
"format": "apk"
}Request body
| Name | Type | Description |
|---|---|---|
| apkId | string | Optional. A specific build from the versions endpoint. Leave it out for the newest build. |
| variantId | string | Optional. A variant build of that release, from the variant status call. The same checks apply as for the ordinary build. The response then names it in variantId, and its file fields describe the variant. |