Reference
20 endpoints over JSON and HTTPS. Base URL https://apkcube.com/api/v1. There is no SDK and no client library — a bearer token and any HTTP client is the whole integration.
Machine-readable contract: OpenAPI 3.1 spec. Import it to generate a client or an agent tool list.
Send an API key as a bearer token on every request. Create one at /account/keys. Keys look like akc_<prefix>_<secret>; only a hash is stored, so a lost key cannot be recovered — revoke it and mint another.
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()Every call is metered. The cost is fixed per endpoint and lives in code, so it cannot be changed under a running script without a deploy and a changelog entry.
Response headers
| Name | Type | Description |
|---|---|---|
| X-Credits-Cost | integer | What this call charged. 0 on a free endpoint. |
| X-Credits-Remaining | integer | Balance after the charge — read back from the debit itself, not a cached figure. |
| X-Credits-Low | "1" | Present when the balance has reached the low-balance threshold. Watch for it rather than polling /me. |
Running out returns 402 INSUFFICIENT_CREDITS, still carrying those headers. A request refused by the rate limit is never charged, and a failure on our side is refunded — a malformed request from you is not, because the work was done.
Every failure has the same envelope. Branch on code, which is stable; message is for humans and may be reworded.
{
"error": {
"code": "APP_NOT_FOUND",
"message": "No app in the catalogue for `com.example`."
}
}Codes worth handling
| Name | Type | Description |
|---|---|---|
| API_UNAUTHORIZED | 401 | Any reason the key does not authorise the call — absent, malformed, unknown, revoked, expired, or an account that is banned or not entitled to the API. One message for all of them, deliberately, so the response is not an oracle for which keys exist. |
| INSUFFICIENT_CREDITS | 402 | Top up and retry. |
| RATE_LIMITED | 429 | Carries Retry-After. Back off; you were not charged. |
| VALIDATION_ERROR | 400 | A malformed package name, an absent q, or a body without a usable apkId. Refused before the debit, so it costs nothing. |
| APP_NOT_FOUND | 404 | No such package in the catalogue. |
| DEVELOPER_NOT_FOUND | 404 | No publisher with that slug. Take the slug from developerSlug on an app row rather than slugifying the name yourself. |
| APK_NOT_FOUND | 404 | No such build for that package. List versions again — ids are not guessable and do not persist across a re-ingest. |
| APP_BLACKLISTED | 451 | Taken down, globally or in your country. Will not change on retry. |
| APP_PAID | 451 | Paid on Google Play and we hold no file. Play only releases a purchased app to the buyer. |
| APP_LOW_RATED | 451 | Below the rating threshold this deployment serves. |
| SECURITY_BLOCKED | 451 | The file failed a security scan or is signed by a certificate we no longer accept. |
| DOWNLOADS_DISABLED | 403 | Every download is switched off site-wide, ours not yours. Temporary — retry later rather than re-minting a key. |
| UPSTREAM_UNAVAILABLE | 503 | Ours. Credits refunded; retry shortly. |
What the key is and what it can spend. Free, and the right way to verify a key before spending anything on a real call.
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_...",
"credits": 480,
"memberSince": "2026-03-04T09:12:00.000Z"
}Search by name, publisher or exact package id. Fixed at 24 results a page. Searching a package id works as a lookup.
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": "7.21.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-08-19T00:00:00.000Z",
"iconUrl": "https://...",
"hasFiles": true,
"availability": "available"
}
],
"matched": 31,
"total": 12,
"page": 1,
"perPage": 24,
"totalPages": 1
}Query parameters
| Name | Type | Description |
|---|---|---|
| qrequired | string | Search phrase, publisher, or exact package id. |
| type | "game" | "app" | Restrict to one side of the catalogue. |
| category | string | Category id from /categories. |
| sort | "relevance" | "installs" | "name" | Defaults to relevance. |
| page | integer | 1-based. Defaults to 1. |
The full record: description, screenshots, tags, rating, requirements. It does not list downloadable files — that is a separate, separately priced call, because plenty of callers want metadata and never touch an artifact.
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": "7.21.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-08-19T00:00:00.000Z",
"iconUrl": "https://...",
"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
}Every build held for a package, newest first. This is where apkId comes from, and the step before a download.
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",
"versions": [
{
"apkId": "48211",
"versionName": "7.21.2",
"versionCode": 1533,
"arch": "arm64-v8a",
"minSdk": 21,
"fileSize": 92341104,
"sha256": "9c1185a5c5e9fc54612808977ee8f548b2258d31ddcd1a2d5e6a3b9f0e4c7a10",
"format": "apk",
"isLatest": true,
"uploadedAt": "2026-08-19T14:02:11.000Z",
"changelog": null
}
]
}Fields that get misread
| Name | Type | Description |
|---|---|---|
| versionCode | integer | Play's monotonic build number. Compare on this — versionName is a marketing string and sorts wrong. |
| fileSize | integer | BYTES, not a formatted size. Some of these are gigabytes. |
| sha256 | string | Checksum of the file this build's download URL serves. Verify what you fetched against it — the signature is on the URL, not on the bytes. |
| format | "apk" | "xapk" | "apks" | An xapk/apks is a ZIP of split APKs plus assets. `adb install` will not take it directly — it needs install-multiple after unpacking, or a split-aware installer. |
| uploadedAt | date-time | When WE ingested the build, not when the developer published it. |
| isLatest | boolean | Newest build we hold — not necessarily the newest on Play. |
Up to 25 packages in one call, returning the same summary row a search does. Use it instead of looping over /apps/{pkg}: an agent checking the apps on a device makes one request rather than twenty.
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"]
}One publisher's catalogue, most-installed first. The slug is the developerSlug carried on any app row. Takes page and per (1–96).
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": "7.21.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-08-19T00:00:00.000Z",
"iconUrl": "https://...",
"hasFiles": true,
"availability": "available"
}
],
"page": 1,
"perPage": 24,
"total": 2,
"totalPages": 1
}The taxonomy you filter a search by. Free — you have to read it before you can ask a real question, and charging for that makes an API tiresome to explore.
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" }
]
}A ranked section: hot, grossing, latest, new or editors-choice. Also takes type and limit (1–50). An unknown section falls back to hot.
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": "7.21.2",
"installs": "100,000,000+",
"rating": 4.6,
"ratings": 2140553,
"updatedAt": "2026-08-19T00:00:00.000Z",
"iconUrl": "https://...",
"hasFiles": true,
"availability": "available"
}
]
}Every other section answers what apps exist. This one answers the question you have to answer before you tell anyone to install something: is this file what it claims to be, and what else is inside it? Nine calls, all derived from the same scan and signature records the download page itself reads — so the API and the page cannot come to different conclusions about one build.
What one build actually is: its checksum, the anti-malware verdict, the certificates that signed it, and every Android permission it declares with a plain-English gloss of what that permission lets the app do. Permissions come back sensitive-first, which is the order worth reading them in.
findings is our rules read off the raw manifest and natives on the same response, so you do not have to know Android to notice what they imply: debuggable, version-mismatch, cleartext-traffic, backup-allowed, shared-user-id, exported-components, legacy-permission-model, packed. Branch on id, never on the prose. The raw fields stay beside it — running your own rules instead costs you nothing.
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": "48211",
"versionName": "7.21.2",
"versionCode": 1533,
"verdict": "verified",
"scanned": true,
"scannedAt": "2026-08-19T15:40:02.000Z",
"sha256": "9c1185a5c5e9fc54612808977ee8f548b2258d31ddcd1a2d5e6a3b9f0e4c7a10",
"sha1": "b3d4...",
"virusTotal": { "vendors": 68, "flagged": 0, "flaggedBy": [] },
"signature": {
"scheme": "v3",
"status": "valid",
"match": "known",
"certs": [
{
"sha256": "29f34e5f...",
"subject": "CN=Signal, O=Signal Foundation",
"scheme": "v3",
"match": "known",
"debugKey": false
}
],
"blocked": false,
"knownSignatures": 1
},
"permissions": [
{
"name": "android.permission.CAMERA",
"label": "Camera",
"sensitive": true,
"category": "Camera and microphone",
"description": "Takes photos and records video.",
"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. Omit it and you get the newest build we hold — which is the point, since the usual question is about whatever you would install today. |
What other companies' code an app carries: the advertising, analytics, attribution, crash-reporting, push and sign-in libraries matched in the build, each with its vendor and where it was recognised. evidence names the artifact a library was found in — a name in lib/, a class in the manifest, a package prefix in the app's own code — and two entries is stronger than one.
This is the privacy question, not the safety one. Ad and analytics SDKs are ordinary parts of shipping an app; what the list answers is what no store publishes — what else is in the file besides the app.
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": "6 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-44@ded2b38"
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build, from /apps/{pkg}/versions. Omit it for the newest build we hold. |
The same evidence as /sdks, filtered to the libraries whose ordinary job involves sending something about the device or the person somewhere else: advertising, analytics, install attribution, crash reporting, push messaging, social sign-in. Ask this when somebody asked what an app sends about them; ask /sdks when you want the whole inventory, which is a superset of this one.
unclassified counts the recognised libraries that are neither — an HTTP client, a UI toolkit, a JSON parser. They are counted rather than listed so the difference from the SDK inventory's total is visible instead of looking like two endpoints disagreeing about one build. Unclassified means we take no position, never that a library is harmless.
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-44@ded2b38"
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build, from /apps/{pkg}/versions. Omit it for the newest build we hold. |
Whether the build is wrapped in a commercial protector — DexProtector, Jiagu, Bangcle, Legu, AppSealing, Google's own PairIP — and what that does to everything else you can learn about it. The second half is why this is its own call rather than a field: it is the fact that decides what an empty SDK list is worth.
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 — but the app's real code is not in classes.dex for anyone to read, so any SDK or code analysis of this artifact is a floor rather than a census."
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build, from /apps/{pkg}/versions. Omit it for the newest build we hold. |
What the build asks the device for, sensitive first, each with a readable label, its capability family and a sentence saying what the app can actually do with it. The same labelling /compare diffs and the download page renders, so no two surfaces can name one permission differently.
defines is the other direction and is easy to misread: those are permissions this app declares for other apps to hold, not things it is asking for.
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": "Read your exact position from GPS and nearby networks.",
"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. Omit it for the newest build we hold. |
What the artifact declares about itself beyond the permissions it requests, plus the native libraries it ships and our rules read off the pair. A key absent inside manifest means the manifest is silent about that attribute — never that it declares the platform default, which is a different claim.
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 permits unencrypted HTTP..."
}
],
"versionMismatch": null
}Query
| Name | Type | Description |
|---|---|---|
| apkId | string | A specific build, from /apps/{pkg}/versions. Omit it for the newest build we hold. |
Every other call in this section is a projection with our rules applied. This is the record they are projected from, for a caller who would rather apply their own: nothing derived, nothing renamed beyond camel case, and nothing filled in — a null is a null the engine left.
It carries the two things the projections have no place for: the evidence digest every SDK match is drawn from — the dex package prefixes, the manifest component names, the intent actions and how much code was read — so you can run your own corpus against it instead of ours, and 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": "ok", "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. Omit it for the newest build we hold. |
Which certificate signed which releases, and where the key moved. Android's own install rule is that an update must be signed by the key that signed what is already on the device, so this is the fact that decides whether a build is an update at all or a different app wearing the same package name. No store publishes it; we hold both halves — the certificates on record for the package, and the signer set extracted from each artifact — so the timeline is derivable.
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 14 archived releases.",
"hasKeyReplacement": false,
"eras": [
{
"certShas": ["29f34e5f..."],
"versions": ["7.21.2", "7.20.1", "7.19.4"],
"firstSeen": "2025-11-02T08:11:00.000Z",
"lastSeen": "2026-08-19T14:02:11.000Z",
"transition": null
}
],
"unreadVersions": ["6.44.0"],
"signers": [
{
"sha256": "29f34e5f...",
"subject": "CN=Signal, O=Signal Foundation",
"trusted": true,
"addedVia": "google-play",
"firstSeenAt": "2025-11-02T08:11:00.000Z",
"inArchive": true
}
]
}What changed between two releases: permissions gained and lost, whether the signing key moved, the size delta and the minimum Android. Size is compared within one architecture present in both releases — comparing whole version groups across differing split sets would report a change that is only a change in what we happen to store.
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": "Send notifications",
"sensitive": true,
"category": "System",
"description": "Shows notifications on your device.",
"standard": true
}
],
"permissionsRemoved": [],
"permissionsKnown": true,
"signerChange": "same",
"signerChangeLabel": "Same signing key",
"signerChangeDetail": "Signed by the same certificate as the release before it.",
"newCertShas": [],
"size": { "arch": "arm64-v8a", "bytes": 4118272 },
"minSdk": { "from": 21, "to": 23 },
"noteworthy": true
}Query
| Name | Type | Description |
|---|---|---|
| from | string | One release, by version NAME or version CODE. Required. The code is tried first — a version name is free text the developer sets, and 'Varies with device' is a real value. |
| to | string | The other release. Required. Order does not matter: the higher build number is treated as the newer, so reversing the pair reports the same change rather than an inverted one. |
Every call above starts from a package name. These two start from the artifact — a checksum off a device or out of a threat feed, a signing-key fingerprint out of somebody's writeup — which are the questions a catalogue keyed on package names cannot otherwise be asked.
Which app and which build a file checksum names. Takes an MD5 (32 hex), SHA-1 (40) or SHA-256 (64) and works out which from the length — the algorithm is not a parameter, because a caller asked to label their hash is a caller who can mislabel it. Colons, spaces and upper case are accepted and normalised: those are the spellings the tools people paste from produce.
A hit gives you the apkId every per-build call in the section above takes, so this is the step that turns a file you are holding into everything we know about it.
curl -sS "https://apkcube.com/api/v1/lookup/hash/9f2b8c1d...e4a7" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/lookup/hash/9f2b8c1d...e4a7",
{ 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/9f2b8c1d...e4a7",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"hash": "9f2b8c1d...e4a7",
"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": "9f2b8c1d...e4a7",
"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."
}Every package we hold that one signing certificate is on record for. Android installs an update only when it is signed by the key that signed what is already on the device, which makes the signing key the closest thing an app has to an identity across package names — so a key found on one repackaged app is worth asking about, because it has usually signed others.
SHA-256 of the DER certificate, with or without colons. A SHA-1 is refused, before the debit, with the reason: the record stores one fingerprint per certificate and a SHA-1 cannot be resolved to it. keytool prints both — send the SHA-256.
curl -sS "https://apkcube.com/api/v1/lookup/certificate/ab12cd34...ef90" \
-H "Authorization: Bearer $APKCUBE_API_KEY"const res = await fetch(
"https://apkcube.com/api/v1/lookup/certificate/ab12cd34...ef90",
{ 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/ab12cd34...ef90",
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"fingerprint": "ab12cd34...ef90",
"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 | 1-based. Default 1. |
| per | integer | Rows per page, 1–96. Default 24. Over the cap clamps. |
Mint a short-lived signed URL for one build. The only call that hands over a file, and the only expensive one.
No captcha stands in front of this, unlike the browser download page. The credit charge is what replaces it: an anonymous flood is expensive in a way a captcha never made it. Every check that decides whether the file may be served — takedowns, the paid rule, the rating bar, the signature blocklist and the malware scan — applies here exactly as it does in the browser.
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 '{"apkId":"48211"}'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({ apkId: "48211" }),
},
);
const data = await res.json();import os, requests
r = requests.post(
"https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/download",
json={"apkId": "48211"},
headers={"Authorization": f"Bearer {os.environ['APKCUBE_API_KEY']}"},
)
data = r.json(){
"url": "https://files.apkcube.com/...&X-Amz-Signature=...",
"expiresAt": "2026-08-27T10:35:00.000Z",
"packageName": "org.thoughtcrime.securesms",
"apkId": "48211",
"fileSize": 92341104,
"versionName": "7.21.2",
"versionCode": 1533
}Request body
| Name | Type | Description |
|---|---|---|
| apkIdrequired | string | The build to mint, from the versions endpoint. |
120 requests a minute, counted per key rather than per IP — so an agent behind a shared address is not throttled by its neighbours, and an anonymous flood cannot lock a paying customer out of their own API. Exceeding it returns 429 RATE_LIMITED with Retry-After, and costs no credits.