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.

Base URL
https://apkcube.com/api/v1
On this page

Endpoints#

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.

Request
curl -sS "https://apkcube.com/api/v1/me" \
  -H "Authorization: Bearer akc_9f3Ka2Lp_xxxxxxxxxxxxxxxxxxxx"

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
apkId and variantId are 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. versionName is 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's released is 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 return page, perPage, total and totalPages. Ranked lists (charts, similar apps) take limit. Reviews page with a cursor.
Unknown vs none
null means 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: available or unavailable. 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.

Response headers
HTTP
HTTP/2 200
content-type: application/json
x-credits-cost: 5
x-credits-remaining: 475

Response headers

NameTypeDescription
X-Credits-CostintegerCredits this call used. 0 for free endpoints and refunded calls. On a 402 it is the price you couldn't pay.
X-Credits-RemainingintegerYour 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:

CallFree when
GET /apps/{pkg}/versionswe 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}/certificateswe hold no signing record for any of the publisher's apps (total: 0)
POST /apps/{pkg}/fetcha fetch for the app is already running (FETCHING)
GET /apps/{pkg}/fetchthe fetch is still running (FETCHING)
GET /apps/{pkg}/similarthere is nothing to list (an empty items)
POST /apps/{pkg}/variantsa build of that variant is already running (FETCHING)
GET /apps/{pkg}/variantsthe build is still running (FETCHING)
GET /apps/{pkg}/reviewsthe 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:

CallCharged even when
POST /apps/{pkg}/fetchwe already have builds (READY) or Google Play doesn't list the app
GET /apps/{pkg}/fetchno fetch was requested (FETCH_NOT_FOUND)
POST /apps/{pkg}/variantsthe variant is already built (READY)
GET /apps/{pkg}/variantsnobody has requested that variant (NEEDS_FETCH)
GET /lookup/certificate/{fp}every package the key signed has been removed (an empty items)
GET /apps/{pkg}/comparefrom and to resolve to the same release
GET /apps/{pkg}/playGoogle 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 envelope
JSON
{
  "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.

CodeStatusWhat it means
VALIDATION_ERROR400A 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_UNAUTHORIZED401The 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_SCOPE403A Developers Console publishing key was sent to a catalogue endpoint. Use a key from your account's API page.
INSUFFICIENT_CREDITS402Your balance is below the call's price. X-Credits-Cost is the price you couldn't pay. Top up and retry.
RATE_LIMITED429Over 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.

CodeStatusWhat it means
APP_NOT_FOUND404We 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_FOUND404No publisher with that slug. Use the developerSlug field from an app instead of building the slug yourself.
APK_NOT_FOUND404That build doesn't exist for this package. List the versions again to get a current id, since ids can change.
VERSION_NOT_FOUND404Compare only: from or to doesn't match a release we hold. The message lists the versions we do have.
FETCH_NOT_FOUND404Fetch status only: nobody has asked us to fetch this package.
APP_UNAVAILABLE404Fetch only: Google Play won't deliver this app to the regions or devices we can reach.
APP_BLACKLISTED451The app was taken down everywhere or in your country.
APP_LOW_RATED451The app's rating is below our minimum.
SECURITY_BLOCKED451The 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.

CodeStatusWhat it means
NO_VERSIONS404We 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_PAID451A paid app on Google Play that we don't have. Play only delivers paid apps to the buyer.
VARIANT_NOT_FOUND404Download only: the variant build expired or belongs to another app. Request it again.
NOT_READY409Variant request only: we don't have the ordinary build yet. Fetch the app first.
UNLIKELY_VERSION422Variant request only: the @vcN build number isn't written like this app's builds. Compare it with the versions list.
DOWNLOADS_DISABLED403Downloads are turned off for the whole site for now. It isn't your key. Try again later.
FETCH_DISABLED403Fetching new apps is turned off for the whole site for now. Try again later.
UPSTREAM_BUSY503Google Play reads and searches: too many live reads at once. Retry in a minute.
UPSTREAM_UNAVAILABLE503Something 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

  1. GET /apps/{pkg}/versions5 credits

    List the builds we hold. An empty list is free and means we don't have the app yet.

  2. 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.

  3. GET /apps/{pkg}/fetch1 credit

    Poll every 10 to 30 seconds. It's free while the answer is FETCHING; only the final READY or FAILED is charged.

  4. POST /apps/{pkg}/download10 credits

    Mint a signed URL with an empty body for the newest build, or pass an apkId from step 1.

  5. Download the file straight away, without your API key, since the URL carries its own signature. Then check it against the sha256 the mint returned.

15 credits for an app we already hold, 21 credits for one we fetch first.

List, fetch, download, verify
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 -

Vet an APK you already have

  1. 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 its apkId; a miss is free.

  2. GET /apps/{pkg}/security5 credits

    Pass that apkId for the scan verdict, signing certificates, permissions and findings of that exact build.

  3. 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#

GET/api/v1/mefree

Returns your key id and credit balance. It's free, so use it to check a key before making paid calls.

Request
curl -sS "https://apkcube.com/api/v1/me" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
JSON
{
  "keyId": "key:3f6c2a9e-...",
  "credits": 480,
  "memberSince": "2026-03-04T09:12:00.000Z"
}

Catalogue#

Get an app#

GET/api/v1/apps/{packageName}5 credits

The 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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#

GET/api/v1/apps/{packageName}/versions5 credits

All builds we have for a package, newest first. To download a build other than the newest, pass its apkId from here.

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/versions" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
appNamestring | nullThe app's name. Null only when the list is empty and we don't know the package at all.
versionCodeintegerThe build number from Play, which always increases. Compare versions using this. versionName is just a label and doesn't sort correctly.
fileSizeintegerSize in bytes. Some files are several gigabytes.
sha256stringChecksum 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.
uploadedAtdate-timeWhen we added the build, not when the developer released it.
isLatestbooleanThe newest build we have. Google Play may have a newer one.

Check availability#

POST/api/v1/apps/availabilityfree

Whether 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.

Request
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"]}'
Response
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

NameTypeDescription
packageNamesrequiredstring[]1 to 100 package names. One malformed name fails the whole call with a 400.

Fields on each item

NameTypeDescription
packageNamestringThe package, as you sent it.
appNamestringThe app's name.
availablebooleanTrue when we have at least one build you can download. False means we know the app but don't have a build yet.
latestVersionCodeinteger | nullThe newest build's version code. Compare against this to detect an update, not against the name.
latestVersionNamestring | nullThe newest build's version name, for display.
latestUploadedAtdate-time | nullWhen we added that build, not when the developer released it.
urlstringThe 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#

POST/api/v1/apps/batch1 credit

Look 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.

Request
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"]}'
Response
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#

GET/api/v1/developers/{slug}1 credit

All 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.

Request
curl -sS "https://apkcube.com/api/v1/developers/signal-foundation" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
pageintegerStarts at 1. Defaults to 1.
perintegerResults per page, 1 to 96. Defaults to 24.

List categories#

GET/api/v1/categoriesfree

The list of categories you can filter searches by. This call is free.

Request
curl -sS "https://apkcube.com/api/v1/categories" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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#

GET/api/v1/charts1 credit

A 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.

Request
curl -sS "https://apkcube.com/api/v1/charts?section=hot&type=app" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
sectionstringOne 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.
limitintegerRows to return, 1 to 50. Defaults to 24.

List similar apps#

GET/api/v1/apps/{packageName}/similar1 credit

Apps 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/similar?limit=8" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
limitintegerRows 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#

GET/api/v1/apps/{packageName}/play5 credits

The 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).

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/play?country=de&lang=de" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
langstringLanguage code, e.g. en, de, pt-BR. Defaults to en.
countrystringTwo-letter storefront, e.g. us, de, jp. Defaults to us.

List Play reviews#

GET/api/v1/apps/{packageName}/reviews5 credits

One 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).

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/reviews?sort=newest&num=20" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
sortstringnewest (default), rating, or helpfulness.
numintegerReviews per page, 1 to 100. Default 40.
cursorstringnextCursor from the previous page. Omit it for the first page.
langstringLanguage code, e.g. en, ja, pt-BR. Default en.
countrystringTwo-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#

GET/api/v1/apps/{packageName}/security5 credits

The 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/security" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
apkIdstringA specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have.

List SDKs#

GET/api/v1/apps/{packageName}/sdks5 credits

Every 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/sdks" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
apkIdstringA specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have.

List trackers#

GET/api/v1/apps/{packageName}/trackers5 credits

The 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/trackers" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
apkIdstringA specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have.

Check for a protector#

GET/api/v1/apps/{packageName}/protection5 credits

Whether 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/protection" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
apkIdstringA specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have.

List permissions#

GET/api/v1/apps/{packageName}/permissions5 credits

The 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/permissions" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
apkIdstringA specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have.

Read the manifest#

GET/api/v1/apps/{packageName}/manifest5 credits

Other 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/manifest" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
apkIdstringA specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have.

Get the raw scan record#

GET/api/v1/apps/{packageName}/raw10 credits

The 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/com.example.game/raw" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
apkIdstringA specific build from /apps/{pkg}/versions. Leave it out to get the newest build we have.

Get the signing history#

GET/api/v1/apps/{packageName}/signing5 credits

Which 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/signing" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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#

GET/api/v1/apps/{packageName}/compare1 credit

What 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.

Request
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"
Response
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

NameTypeDescription
fromstringOne 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'.
tostringThe 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#

GET/api/v1/lookup/hash/{hash}5 credits

Find 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.

Request
curl -sS "https://apkcube.com/api/v1/lookup/hash/9f2b8c1d5e6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4ce4a7" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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#

GET/api/v1/lookup/certificate/{fingerprint}5 credits

All 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.

Request
curl -sS "https://apkcube.com/api/v1/lookup/certificate/ab12cd34ef567890ab12cd34ef567890ab12cd34ef567890ab12cd34ef56ef90" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
pageintegerStarts at 1. Default 1.
perintegerResults per page, 1 to 96. Default 24. Larger values are capped at 96.

List a publisher's signing keys#

GET/api/v1/developers/{slug}/certificates5 credits

The 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.

Request
curl -sS "https://apkcube.com/api/v1/developers/example-studio/certificates" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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#

POST/api/v1/apps/{packageName}/fetch10 credits

Ask 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.

Request
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 '{}'
Response
JSON
{
  "packageName": "com.example.newapp",
  "status": "FETCHING"
}

Response fields

NameTypeDescription
status"READY" | "FETCHING"READY means we already have builds, so list the versions. FETCHING means a fetch is queued or running.

Check a fetch#

GET/api/v1/apps/{packageName}/fetch1 credit

Check 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/fetch" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
JSON
{
  "packageName": "org.thoughtcrime.securesms",
  "status": "READY",
  "updatedAt": "2026-10-01T09:14:27.000Z"
}

Response fields

NameTypeDescription
status"FETCHING" | "READY" | "FAILED"FETCHING: wait and ask again. READY: list the versions. FAILED: see error.
errorstringFAILED only. Why the fetch failed, worded for people.
terminaltrueFAILED 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.
updatedAtdate-timeWhen the fetch last changed state.

Request a variant build#

POST/api/v1/apps/{packageName}/variants10 credits

The 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:

VariantWhat you get
armeabi-v7aThe 32-bit ARM build, for older phones.
universalBoth ARM ABIs in one bundle.
+all-langsSuffix: every language Google Play has, not only the standard 24.
@sdk28Suffix: the newest build that runs on that Android API level (24 to 37).
@vc1234Suffix: 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-9aExactly what Google Play serves that device.
Request
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"}'
Response
JSON
{
  "packageName": "org.thoughtcrime.securesms",
  "variant": "universal+all-langs",
  "status": "FETCHING"
}

Request body

NameTypeDescription
variantrequiredstringarmeabi-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#

GET/api/v1/apps/{packageName}/variants1 credit

Check 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.

Request
curl -sS "https://apkcube.com/api/v1/apps/org.thoughtcrime.securesms/variants?variant=universal+all-langs" \
  -H "Authorization: Bearer $APKCUBE_API_KEY"
Response
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

NameTypeDescription
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.
buildobjectREADY only. variantId, variant, arch, versionName, versionCode, minSdk, fileSize, sha256, format and expiresAt.
errorstringFAILED only. Why the build failed, worded for people.

Get a download URL#

POST/api/v1/apps/{packageName}/download10 credits

Get 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.

Request
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 '{}'
Response
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

NameTypeDescription
apkIdstringOptional. A specific build from the versions endpoint. Leave it out for the newest build.
variantIdstringOptional. 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.