---
name: apkcube
description: Search the APKCube Android catalogue, inspect an app's builds, and mint download URLs for APK/XAPK files. Use when asked to find an Android app, compare versions, check what an app requires, or fetch an APK.
---

# APKCube

A catalogue of Android apps and their downloadable builds. You can search it,
read an app's metadata, list every build held for a package, and mint a
short-lived signed URL for one of those builds.

## Credentials

Read the key from the environment; never ask the user to paste it into a file:

```bash
: "${APKCUBE_API_KEY:?set APKCUBE_API_KEY (create a key at https://apkcube.com/account/keys)}"
```

Every request sends it as a bearer token:

```bash
curl -sS -H "Authorization: Bearer $APKCUBE_API_KEY" "https://apkcube.com/api/v1/me"
```

This file is safe to share. It holds no secrets.

## Cost, and why you should care

Calls are metered in credits and the balance is finite. Every response carries
`X-Credits-Remaining`; a 402 means the balance ran out.

| Call | Credits |
|---|---|
| `/me`, `/categories` | 0 |
| `/apps` (search), `/developers/{slug}`, `/charts`, `/apps/{pkg}/compare` | 1 |
| `/apps/{pkg}`, `/apps/{pkg}/versions`, `/apps/{pkg}/security`, `/apps/{pkg}/sdks`, `/apps/{pkg}/signing`, `/apps/{pkg}/protection`, `/apps/{pkg}/trackers`, `/apps/{pkg}/permissions`, `/apps/{pkg}/manifest`, `/lookup/hash/{hash}`, `/lookup/certificate/{fingerprint}` | 5 |
| `/apps/{pkg}/raw` | 10 |
| `/apps/batch` | 1 **per package** |
| `/apps/{pkg}/download` | **10** |

Three habits worth keeping: check `availability` and `hasFiles` on an app row
before spending a download; do not re-search for something you already have the
package name for; and when you need several apps, send them to `/apps/batch` in
one call rather than looping — it is 1 per package against
5 for `/apps/{pkg}`, because it returns the summary row and not
the full record. Reach for the single-app call when you actually need the
description or the screenshots.

## The normal path

Search → pick a package → list its builds → check what the build is → mint a URL
→ verify what you got.

```bash
BASE="https://apkcube.com/api/v1"
AUTH="Authorization: Bearer $APKCUBE_API_KEY"

# 1. Find the package.
curl -sS -H "$AUTH" "$BASE/apps?q=whatsapp" |
  jq '.items[] | {packageName, name, version, availability, hasFiles}'

# 2. List the builds held for it.
curl -sS -H "$AUTH" "$BASE/apps/com.whatsapp/versions" |
  jq '.versions[] | {apkId, versionName, versionCode, arch, fileSize, sha256, format}'

# 3. Check what that build actually is, before you recommend installing it.
curl -sS -H "$AUTH" "$BASE/apps/com.whatsapp/security?apkId=12345" |
  jq '{verdict, scanned, virusTotal, permissionSummary, signature: .signature.match,
       findings: [.findings[] | {severity, title}]}'

# 4. Mint a URL for one. POST, and the body names the build.
curl -sS -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"apkId":"12345"}' "$BASE/apps/com.whatsapp/download" | jq

# 5. Fetch it. The URL expires in about five minutes.
curl -L -o app.apk "<url from step 4>"

# 6. Verify it against the sha256 the mint returned.
shasum -a 256 app.apk
```

## Every endpoint

| Method | Path | What it does |
|---|---|---|
| GET | `/me` | What the key is, and the credit balance. No owner identity. |
| GET | `/apps?q=` | Search. Also takes `type`, `category`, `sort`, `page`. 24 a page. |
| GET | `/apps/{packageName}` | Full record: description, screenshots, rating, tags. |
| POST | `/apps/batch` | Up to 25 packages at once. Body: `{"packageNames":[…]}`. Priced per package. |
| GET | `/apps/{packageName}/versions` | Every build held, each with its `sha256`. Gives you `apkId`. |
| GET | `/apps/{packageName}/security` | Checksum, scan verdict, signers, permissions, findings. `?apkId=` or newest. |
| GET | `/apps/{packageName}/sdks` | Third-party SDKs in a build, with what bounds the answer. `?apkId=` or newest. |
| GET | `/apps/{packageName}/trackers` | The advertising/analytics subset of that list, counted by category. |
| GET | `/apps/{packageName}/protection` | Is the build packed, and how far did anything else get into it. |
| GET | `/apps/{packageName}/permissions` | Declared permissions, described, sensitive first. |
| GET | `/apps/{packageName}/manifest` | What the manifest and the native libraries declare. |
| GET | `/apps/{packageName}/raw` | The stored scan record verbatim, evidence digest included. |
| GET | `/lookup/hash/{hash}` | Which build an MD5/SHA-1/SHA-256 names. No package name needed. |
| GET | `/lookup/certificate/{fingerprint}` | Every package we hold signed by one key. SHA-256 only. |
| GET | `/apps/{packageName}/signing` | Which key signed which releases, and where it moved. |
| GET | `/apps/{packageName}/compare` | Diff two builds. `?from=&to=`, by version name or code. |
| POST | `/apps/{packageName}/download` | Mint a signed URL. Body: `{"apkId":"…"}`. |
| GET | `/developers/{slug}` | One publisher's catalogue. Slug from `developerSlug`. Takes `page` and `per` (1–96). |
| GET | `/categories` | The taxonomy, for filtering a search. Free. |
| GET | `/charts?section=hot` | `hot`, `grossing`, `latest`, `new`, `editors-choice`. |

Full reference: https://apkcube.com/docs/api · OpenAPI: https://apkcube.com/api/v1/openapi.json

## Things that will trip you up

- **A search sees at most 60 matches.** `total` is what survived your
  filters within that pool and `matched` is the pool before them — neither counts the
  catalogue, and paging stops at page 3. If the app is not in the first
  page or two, narrow the phrase; do not walk the pager.
- **`installs` is a string, not a number.** Google Play publishes a bucket —
  `"10,000,000+"` — and there is no exact count behind it. Do not parse it into
  an integer and do not present it as one.
- **Compare `versionCode`, never `versionName`.** The name is a marketing
  string and sorts wrong; the code is Play's monotonic build number.
- **`format` matters before you install.** An `xapk` or `apks` is a ZIP of
  split APKs plus assets. `adb install` will not take it directly — it needs
  `adb install-multiple` after unpacking, or a split-aware installer.
- **`fileSize` is bytes.** Some of these are gigabytes; check before downloading.
- **Verify the download.** Every version row and every mint carries `sha256`. The
  signature is on the URL, not on the bytes that came back through it, so check
  the file before you hand it to anyone.
- **A changed signing key means it is not the same app.** Android only installs
  an update signed by the key that signed what is already on the device, so
  `hasKeyReplacement: true` from `/signing` means the newer build will not go
  over the older one — which is what a repackaged app looks like. `"added"` is
  ordinary key rotation and is not a finding.
- **`findings` is derived, and it is not a verdict.** On `/security` it applies
  our rules to the manifest and native facts on the same response —
  `debuggable`, `version-mismatch`, `cleartext-traffic`, `backup-allowed`,
  `shared-user-id`, `exported-components`, `legacy-permission-model`,
  `packed`. Branch on `id`, not on the prose. `severity: "warning"` covers
  only the two that mean the file is not what a release build should look like;
  everything else is a `notice` an ordinary app may well have chosen on
  purpose, so repeat the whole `detail` rather than the title alone. An empty
  array is not an all-clear — a manifest nobody read produces no findings
  either.
- **An absent scan is not a clean scan.** On `/security`, `scanned: false` and
  `virusTotal: null` mean nobody analysed the file; only a non-null
  `virusTotal` with `flagged: 0` is an all-clear. `permissions: null` means the
  manifest was never read, while `[]` means the app really declares none. Never
  report any of these as reassurance.
- **Start from the hash when you have one.** `/lookup/hash/{hash}` takes an
  MD5, SHA-1 or SHA-256 and works the algorithm out from the length — do not
  label it, and do not strip colons or lower-case it yourself. `found: false`
  is a real answer; read `note` before concluding anything from it, because
  only SHA-256 is carried by every stored build.
- **A signing key is an identity; a SHA-1 of one is not a lookup key.**
  `/lookup/certificate/{fingerprint}` takes the SHA-256 of the DER certificate
  and refuses a SHA-1 with the reason, free. What it returns is bounded by what
  we hold — an empty result means we have nothing signed by that key, never that
  the key signs nothing.
- **Packed is not malicious, and it is not nothing either.** A non-empty
  `protectors` on `/protection` (or on `/sdks`) means anti-piracy or
  anti-tamper tooling, which is ordinary and is what Google's own PairIP is —
  but the app's real code was never readable, so every other analysis of that
  build is a floor. Call it before reporting that a build carries no trackers.
- **An empty SDK list is usually not "none".** On `/apps/{pkg}/sdks`, read
  `analyzed`, `codeCoverage` and `protectors` before `sdks`: recognising a
  library is strong evidence it is there, but failing to is weak evidence it is
  not, because a packed or obfuscated build hides the code we would match on.
  Only `analyzed: true` with `codeCoverage: "full"` makes an empty list mean
  much. Repeat `summary` rather than the count alone — it carries the bound.
  These libraries are also ordinary parts of shipping an app: report them as
  what the app carries, never as a finding of harm.
- **Metadata is scraped from Google Play** and can trail the store by hours. If
  a version looks old, it may genuinely be the newest build held here.
- **A download can be refused with 451** for a taken-down app, a paid app we hold
  no file for (Play only releases a purchased app to the account that bought it),
  a listing under the operator's rating threshold, or a file that failed a
  security scan. `availability: "unavailable"` on the app row predicts all four —
  check it rather than paying 10 credits to be told no.
- **Do not store a minted URL.** It is signed and expires in about five minutes.
  Mint a fresh one instead of caching it.
- **Rate limit is 120 requests a minute** per key. A 429 carries `Retry-After`.

## Errors

Every failure is `{"error":{"code":"…","message":"…"}}`. The codes worth
branching on: `API_UNAUTHORIZED` (401, a key that is missing, malformed, revoked, expired, or
attached to an account that cannot use the API — one message for all of them),
`INSUFFICIENT_CREDITS` (402, top up), `RATE_LIMITED` (429, back off),
`VALIDATION_ERROR` (400, a malformed package name or a bad body — fix the request,
it cost nothing), `APP_NOT_FOUND` / `APK_NOT_FOUND` / `DEVELOPER_NOT_FOUND` /
`VERSION_NOT_FOUND` (404 — the last one names the releases actually held, so
read the message rather than re-listing) / `NO_VERSIONS` (404, we hold no files
for the package at all), `DOWNLOADS_DISABLED` (403, ours and temporary — retry later),
`APP_BLACKLISTED` / `APP_PAID` / `APP_LOW_RATED` / `SECURITY_BLOCKED` (451,
and none of them will change on retry), and `UPSTREAM_UNAVAILABLE` (503, ours —
you were refunded, so retry shortly).

## If your agent speaks MCP

Skip this file and use the server instead — same catalogue, same credits, no
shell required. It exposes every endpoint above as a tool, plus prompts for the
workflows worth doing in order (vetting an app before installing it, finding and
downloading one, diffing two releases).

It needs no key. The URL is the whole configuration: your client registers
itself, opens a browser once so the account owner can approve it, and holds its
own token from then on.

```bash
claude mcp add --transport http apkcube https://apkcube.com/api/mcp
```
