Koskamo

APKCube MCP

An official MCP server provided by APKCube that allows AI agents to search the Android APK catalogue, list app versions and builds, check security scans and signing keys, and retrieve verified download links for Android applications.

Endpoint and access

No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.

APKCube MCP logo, by APKCubeVendor evidenceStreamable HTTPNo sign-inEndpoint checked 2026-11-06Operated by APKCube
You need:Programmatic access is metered in credits against an API keyCreate a key on your account and send it as Authorization: Bearer akc_… on every callSign in, check the client's name, and approve
MCP endpoint (Streamable HTTP)
https://apkcube.com/api/mcp
Vendor documentation

What its tools do

38 tools, grouped by the hints the server advertises. Hints are self-declared, not verified behavior.

38 of 38 have a description · 38 of 38 declare behavior hints · 36 of 36 with parameters document them all

Checks and signals

Each line is separate evidence, not a trust score.

  • Passed Endpoint answered the live check ·
  • Passed Official MCP Registry namespace is tied to apkcube.com ·
  • Passed Endpoint is on the registry namespace domain ·
  • Availability 1 of 1 checks passed in 30 days

What you can do with it

  • Search for Android apps by name or package ID
  • Check APK security scans, permissions, and signing keys
  • List available builds and architectures for an app
  • Get verified download URLs for APK files

Source and maintenance

Vendor evidenceRegistry 1.0.0

Protocol and capabilities

Server
apkcube 1.0.0
MCP protocol negotiated in check
2025-11-25
Tools
38 advertised
Resources
Not advertised
Resource templates
Not advertised
Prompts
8 advertised
Server-advertised capabilities
{
  "tools": {
    "listChanged": false
  },
  "prompts": {
    "listChanged": false
  }
}

Server-provided instructions

Untrusted server output shown for inspection. It is not Koskamo guidance and is excluded from knowledge search.

APKCube's Android APK catalogue. The normal path is: find a package (search_apps, or get_apps for several at once), list the builds held for it (list_versions), check what a build actually is (get_version_security), then mint a short-lived download URL for one (get_download_url). Every tool that takes an apkId treats it as optional and uses the newest build held without it. When list_versions is empty, request_fetch asks us to download the app from Google Play; poll get_fetch_status until it says READY, then list again. Before telling anyone to install something, read get_version_security and get_signing_history: an absent scan is not a clean scan, `permissions: null` means nobody read the manifest rather than that the app asks for nothing, and a replaced signing key means the build is not an update of the one already installed. Verify every download against the `sha256` the mint returns. Calls are charged in credits against the account that authorized this client: check_availability, list_categories and get_account are free; get_apps bills 1 per package; search_apps, list_versions, get_fetch_status, get_variant_status, list_developer_apps, list_chart, list_similar_apps, compare_versions cost 1 each; get_app, get_play_details, list_reviews, get_version_security, list_sdks, get_protection, list_trackers, list_permissions, get_manifest, lookup_hash, lookup_certificate, list_developer_certificates, get_signing_history cost 5 each; get_download_url, request_fetch, request_variant, get_raw_scan cost 10 each. Each result reports what it cost and what is left in _meta under 'com.apkcube/credits'; get_account reports the balance on its own when you need it before spending.

Tools

38 advertised

search_apps — Search apps
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Search the APK catalogue by name, publisher or package id. Start here when you have a phrase and need a package name. Returns 24 results a page out of at most 60 matches, so paging stops at page 3 — narrow the phrase rather than paging for more. `facets` counts the matches by type and category before filtering, so it tells you which filter would narrow a broad phrase. Searching an exact package id works as a lookup. A phrase nobody has searched before asks Google Play live and can take up to 20 seconds; UPSTREAM_UNAVAILABLE / UPSTREAM_BUSY there is refunded and means Play did not answer in time, not that nothing matches — retry the same phrase after a few seconds.

search_apps input schema
{
  "type": "object",
  "required": [
    "q"
  ],
  "properties": {
    "q": {
      "type": "string",
      "maxLength": 200,
      "minLength": 1,
      "description": "Search phrase, publisher, or exact package id."
    },
    "page": {
      "type": "integer",
      "maximum": 3,
      "minimum": 1,
      "description": "1-based page number, 1–3. Defaults to 1."
    },
    "sort": {
      "enum": [
        "relevance",
        "installs",
        "name"
      ],
      "type": "string",
      "description": "Result ordering. Defaults to relevance."
    },
    "type": {
      "enum": [
        "game",
        "app"
      ],
      "type": "string",
      "description": "Restrict to games or apps."
    },
    "category": {
      "type": "string",
      "description": "Category id from list_categories."
    }
  },
  "additionalProperties": false
}
search_apps output schema
{
  "type": "object",
  "required": [
    "items",
    "total",
    "page"
  ],
  "properties": {
    "page": {
      "type": "integer",
      "description": "1-based page number of this result."
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "name",
          "developer",
          "type",
          "availability",
          "hasFiles"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "App title."
          },
          "type": {
            "enum": [
              "game",
              "app"
            ],
            "type": "string",
            "description": "Game or app."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "description": "Play star rating, 0–5. Null when the app is not on Google Play, or when Play gave it no rating."
          },
          "iconUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store artwork URL."
          },
          "ratings": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Vote count behind `rating`. Null on rows seeded from a list scrape."
          },
          "version": {
            "type": "string",
            "description": "Latest version NAME known ('2.24.9'). May be 'Varies with device'."
          },
          "hasFiles": {
            "type": "boolean",
            "description": "We hold at least one downloadable file. Check before spending a download."
          },
          "installs": {
            "type": [
              "string",
              "null"
            ],
            "description": "Google Play's install BUCKET, verbatim ('10,000,000+'). A string, not a number — Play publishes no exact count. Null when the app is not on Google Play."
          },
          "developer": {
            "type": "string",
            "description": "Publisher name as scraped."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When Play last published an update, ISO 8601."
          },
          "categoryId": {
            "type": "string",
            "description": "Category id, as listed by the categories call."
          },
          "packageName": {
            "type": "string",
            "description": "Android package id — the identifier every app call takes."
          },
          "availability": {
            "enum": [
              "available",
              "unavailable"
            ],
            "type": "string",
            "description": "'unavailable' means a download call will be refused — a takedown, a paid app we cannot obtain, or a listing under the operator's rating bar."
          },
          "categoryName": {
            "type": "string",
            "description": "Human-readable category."
          },
          "onGooglePlay": {
            "type": "boolean",
            "description": "False for an app its developer published on this site directly, with no Google Play listing: the Play fields are then null and the Play listing call answers 404."
          },
          "developerSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Key for the developer lookup. Null when the row has no publisher."
          }
        }
      }
    },
    "total": {
      "type": "integer",
      "description": "Total matching rows across all pages."
    },
    "facets": {
      "type": "object",
      "required": [
        "type",
        "categories"
      ],
      "properties": {
        "type": {
          "type": "object",
          "required": [
            "game",
            "app"
          ],
          "properties": {
            "app": {
              "type": "integer",
              "description": "Apps in the pool."
            },
            "game": {
              "type": "integer",
              "description": "Games in the pool."
            }
          }
        },
        "categories": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "id",
              "name",
              "count"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "Category id — what the category filter takes."
              },
              "name": {
                "type": "string",
                "description": "Display name."
              },
              "count": {
                "type": "integer",
                "description": "Matches in this category."
              }
            }
          },
          "description": "Most matches first."
        }
      },
      "description": "How the match pool divides before `type`/`category` were applied — which filter would narrow the phrase, and by how much."
    },
    "matched": {
      "type": "integer",
      "description": "Matches before type/category filtering were applied."
    },
    "perPage": {
      "type": "integer",
      "description": "Rows per page."
    },
    "totalPages": {
      "type": "integer",
      "description": "Number of pages available."
    }
  }
}
search_apps annotations
{
  "title": "Search apps",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_app — Get app details
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Full record for one package: description, screenshots, rating, tags, and whether a download would be refused. Does NOT list downloadable files — use list_versions for those. An app not in the catalogue yet is looked up on Google Play live, for up to 20 seconds; UPSTREAM_UNAVAILABLE / UPSTREAM_BUSY then is refunded and means Play did not answer in time, not that the app does not exist — retry after a few seconds.

get_app input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Android package id, e.g. com.whatsapp."
    }
  },
  "additionalProperties": false
}
get_app output schema
{
  "type": "object",
  "required": [
    "packageName",
    "name",
    "developer",
    "type",
    "availability",
    "hasFiles",
    "summary"
  ],
  "properties": {
    "name": {
      "type": "string",
      "description": "App title."
    },
    "paid": {
      "type": "boolean",
      "description": "Paid on Google Play."
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "type": {
      "enum": [
        "game",
        "app"
      ],
      "type": "string",
      "description": "Game or app."
    },
    "price": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "amount": {
          "type": "number"
        },
        "currency": {
          "type": "string"
        }
      },
      "description": "Play's asking price. Null for a free listing."
    },
    "rating": {
      "type": [
        "number",
        "null"
      ],
      "description": "Play star rating, 0–5. Null when the app is not on Google Play, or when Play gave it no rating."
    },
    "iconUrl": {
      "type": [
        "string",
        "null"
      ],
      "description": "Store artwork URL."
    },
    "ratings": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Vote count behind `rating`. Null on rows seeded from a list scrape."
    },
    "summary": {
      "type": "string",
      "description": "One-line store summary."
    },
    "version": {
      "type": "string",
      "description": "Latest version NAME known ('2.24.9'). May be 'Varies with device'."
    },
    "hasFiles": {
      "type": "boolean",
      "description": "We hold at least one downloadable file. Check before spending a download."
    },
    "installs": {
      "type": [
        "string",
        "null"
      ],
      "description": "Google Play's install BUCKET, verbatim ('10,000,000+'). A string, not a number — Play publishes no exact count. Null when the app is not on Google Play."
    },
    "videoUrl": {
      "type": [
        "string",
        "null"
      ],
      "description": "Promo video, when Play lists one."
    },
    "developer": {
      "type": "string",
      "description": "Publisher name as scraped."
    },
    "updatedAt": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "When Play last published an update, ISO 8601."
    },
    "categoryId": {
      "type": "string",
      "description": "Category id, as listed by the categories call."
    },
    "description": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Plain-text paragraphs."
    },
    "headerImage": {
      "type": [
        "string",
        "null"
      ],
      "description": "Wide feature graphic."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id — the identifier every app call takes."
    },
    "screenshots": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "availability": {
      "enum": [
        "available",
        "unavailable"
      ],
      "type": "string",
      "description": "'unavailable' means a download call will be refused — a takedown, a paid app we cannot obtain, or a listing under the operator's rating bar."
    },
    "categoryName": {
      "type": "string",
      "description": "Human-readable category."
    },
    "onGooglePlay": {
      "type": "boolean",
      "description": "False for an app its developer published on this site directly, with no Google Play listing: the Play fields are then null and the Play listing call answers 404."
    },
    "developerSlug": {
      "type": [
        "string",
        "null"
      ],
      "description": "Key for the developer lookup. Null when the row has no publisher."
    },
    "editorsChoice": {
      "type": "boolean",
      "description": "Carries Play's Editors' Choice badge."
    },
    "recentChanges": {
      "type": [
        "string",
        "null"
      ],
      "description": "Play's 'What's new'."
    },
    "descriptionHtml": {
      "type": [
        "string",
        "null"
      ],
      "description": "Sanitised store markup."
    },
    "requiresAndroid": {
      "type": "string",
      "description": "Minimum Android, as Play states it ('5.0 and up')."
    },
    "developerVerified": {
      "type": "boolean",
      "description": "The publisher is verified on this site through the Developers Console (it proved it owns the Play listing, or an operator approved a publisher that is not on Google Play); the text fields may then be its own rather than Play's."
    }
  }
}
get_app annotations
{
  "title": "Get app details",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
check_availability — Check app availability
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Free. Whether we hold a downloadable build of each of up to 100 packages, the newest one's versionName and versionCode, and `versions`: every build we hold by versionCode and versionName, highest code first — the cheap way to check for updates before calling list_versions or get_download_url. Send every package you are checking in one call rather than looping. Each lands in one of three lists: `items` for packages we know (`available` false when we hold no build), `notFound` for ones we have never seen, and `refused` for ones we will not serve at all, with the reason a download would give.

check_availability input schema
{
  "type": "object",
  "required": [
    "packageNames"
  ],
  "properties": {
    "packageNames": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 100,
      "minItems": 1,
      "description": "Android package ids, 1–100."
    }
  },
  "additionalProperties": false
}
check_availability output schema
{
  "type": "object",
  "required": [
    "items",
    "notFound",
    "refused"
  ],
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "appName",
          "available",
          "latestVersionName",
          "latestVersionCode",
          "latestUploadedAt",
          "url",
          "versions"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "The app's page on this site."
          },
          "appName": {
            "type": "string",
            "description": "The app's display name."
          },
          "versions": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "versionCode",
                "versionName"
              ],
              "properties": {
                "versionCode": {
                  "type": "integer",
                  "description": "Play's monotonic build number."
                },
                "versionName": {
                  "type": "string",
                  "description": "Human-readable version string."
                }
              }
            },
            "description": "Every release we hold, newest `versionCode` first, one entry per code. Empty when `available` is false."
          },
          "available": {
            "type": "boolean",
            "description": "True when we hold at least one build you can download."
          },
          "packageName": {
            "type": "string",
            "description": "The package asked about."
          },
          "latestUploadedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When we ingested that build, not when it was published. Null when `available` is false."
          },
          "latestVersionCode": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Play's monotonic build number of the newest build we hold — compare on this, not on the name. Null when `available` is false."
          },
          "latestVersionName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version string of the newest build we hold. Null when `available` is false."
          }
        }
      },
      "description": "Packages we know, in the order sent — `available` false when we hold no build."
    },
    "refused": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "What a download would be refused with: APP_BLACKLISTED, APP_PAID or APP_LOW_RATED."
          },
          "message": {
            "type": "string",
            "description": "Why, in words."
          },
          "packageName": {
            "type": "string",
            "description": "The package refused."
          }
        }
      },
      "description": "Packages we will not serve at all, with the reason a download would give."
    },
    "notFound": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Ids with no row in the catalogue."
    }
  }
}
check_availability annotations
{
  "title": "Check app availability",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_versions — List downloadable versions
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Every APK/XAPK build held for a package, newest first, each with its SHA-256. Call this before get_download_url when you want a specific build — the apkId it returns is what that tool takes; without one it serves the newest. Free when we hold no builds for the package (an empty list); request_fetch is how to get the app. A package we will not serve at all is refused here with the same reason a download would give, so you learn it without spending a download.

list_versions input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
list_versions output schema
{
  "type": "object",
  "required": [
    "packageName",
    "appName",
    "versions"
  ],
  "properties": {
    "appName": {
      "type": [
        "string",
        "null"
      ],
      "description": "The app's display name. Null only when we have no row for the package."
    },
    "versions": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "apkId",
          "versionName",
          "versionCode",
          "fileSize",
          "sha256",
          "format"
        ],
        "properties": {
          "arch": {
            "type": "string",
            "description": "'universal', 'arm64-v8a', …"
          },
          "apkId": {
            "type": "string",
            "description": "Pass this to the download call."
          },
          "format": {
            "type": "string",
            "description": "'apk', 'xapk' or 'apks'. An xapk/apks is a ZIP of split APKs plus assets — not directly `adb install`-able."
          },
          "minSdk": {
            "type": "integer",
            "description": "Minimum Android API level."
          },
          "sha256": {
            "type": "string",
            "description": "SHA-256 of the file as the ingesting engine reported it, lowercase hex. Check your download against it to catch truncation or corruption in transit; it is our record of the upload rather than a digest recomputed over the stored object."
          },
          "fileSize": {
            "type": "integer",
            "description": "Bytes."
          },
          "isLatest": {
            "type": "boolean",
            "description": "Newest build we hold for this package."
          },
          "changelog": {
            "type": [
              "string",
              "null"
            ],
            "description": "Release notes for this build."
          },
          "uploadedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When we ingested it, not when it was published."
          },
          "versionCode": {
            "type": "integer",
            "description": "Play's monotonic build number — compare on this, not on the name."
          },
          "versionName": {
            "type": "string",
            "description": "Version string as the developer set it."
          }
        }
      }
    },
    "packageName": {
      "type": "string",
      "description": "The package these builds belong to."
    }
  }
}
list_versions annotations
{
  "title": "List downloadable versions",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_download_url — Get a download URL
Advertised by the server during the last successful anonymous check.
Open-world

Mint a short-lived signed URL for one build — the newest held unless you pass an apkId from list_versions. Costs 10 credits — the most expensive call here, and the only one that hands over a file. Check get_version_security before handing anything to a person. The URL expires in about 5 minutes; fetch it promptly and do not store it. The response carries the file's `sha256` — verify what you downloaded against it, because the signature is on the URL and not on the bytes.

get_download_url input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "variantId": {
      "type": "string",
      "description": "Optional: an on-demand variant build of that release, from get_variant_status. It is served under the ordinary build's checks; omit it for the ordinary build."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_download_url output schema
{
  "type": "object",
  "required": [
    "url",
    "expiresAt",
    "packageName",
    "appName",
    "apkId",
    "sha256"
  ],
  "properties": {
    "url": {
      "type": "string",
      "description": "Signed URL, valid about 5 minutes. Fetch it before it expires."
    },
    "apkId": {
      "type": "string",
      "description": "The artifact that was minted."
    },
    "format": {
      "type": "string",
      "description": "'apk', 'xapk' or 'apks' — the container `url` serves."
    },
    "sha256": {
      "type": "string",
      "description": "SHA-256 of the file this URL serves, lowercase hex. Verify the download against it."
    },
    "appName": {
      "type": "string",
      "description": "The app's display name."
    },
    "fileSize": {
      "type": "integer",
      "description": "Bytes."
    },
    "expiresAt": {
      "type": "string",
      "format": "date-time",
      "description": "When `url` stops working."
    },
    "variantId": {
      "type": "string",
      "description": "Present when an on-demand variant build was minted; `fileSize` and `sha256` are then the variant's."
    },
    "packageName": {
      "type": "string",
      "description": "Package this file belongs to."
    },
    "versionCode": {
      "type": "integer",
      "description": "Build number of the minted build."
    },
    "versionName": {
      "type": "string",
      "description": "Version string of the minted build."
    }
  }
}
get_download_url annotations
{
  "title": "Get a download URL",
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
request_fetch — Fetch a missing app
Advertised by the server during the last successful anonymous check.
Open-world

Ask us to download an app from Google Play when list_versions came back empty. Costs 10 credits, even when we already have builds (`READY`) or Google Play doesn't list the app — so call list_versions first. Free only when a fetch for the app is already running (`FETCHING`). A fetch usually takes a few minutes. Poll get_fetch_status every 10–30 seconds — not this tool — and call list_versions once it says READY. Paid apps and apps Google Play will not deliver to us are refused with the reason. An app not in the catalogue yet is looked up on Google Play live, for up to 20 seconds; UPSTREAM_UNAVAILABLE / UPSTREAM_BUSY then is refunded and means Play did not answer in time, not that the app does not exist — retry after a few seconds.

request_fetch input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
request_fetch output schema
{
  "type": "object",
  "required": [
    "packageName",
    "status"
  ],
  "properties": {
    "error": {
      "type": "string",
      "description": "FAILED only: why, in words safe to show a person."
    },
    "status": {
      "enum": [
        "READY",
        "FETCHING",
        "FAILED"
      ],
      "type": "string",
      "description": "READY: we hold builds — list the versions. FETCHING: a job is queued or running; poll again in a few seconds. FAILED: the last job failed; see `error`."
    },
    "terminal": {
      "type": "boolean",
      "description": "FAILED only, and present only when retrying cannot help: Google Play will not deliver this app to us. Absent means another fetch request may succeed."
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "description": "Status read only: when the job last moved."
    },
    "packageName": {
      "type": "string",
      "description": "The package this fetch is for."
    }
  }
}
request_fetch annotations
{
  "title": "Fetch a missing app",
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
get_fetch_status — Check a fetch
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Where the fetch request_fetch started stands now: FETCHING (keep waiting), READY (call list_versions), or FAILED with a reason. Free when the fetch is still running (`FETCHING`); the final answer costs 1 credit, and so does a check when no fetch was requested (`FETCH_NOT_FOUND`). When FAILED carries `terminal: true`, Google Play will not deliver this app to us and asking again will not help; otherwise request_fetch may be retried.

get_fetch_status input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_fetch_status output schema
{
  "type": "object",
  "required": [
    "packageName",
    "status"
  ],
  "properties": {
    "error": {
      "type": "string",
      "description": "FAILED only: why, in words safe to show a person."
    },
    "status": {
      "enum": [
        "READY",
        "FETCHING",
        "FAILED"
      ],
      "type": "string",
      "description": "READY: we hold builds — list the versions. FETCHING: a job is queued or running; poll again in a few seconds. FAILED: the last job failed; see `error`."
    },
    "terminal": {
      "type": "boolean",
      "description": "FAILED only, and present only when retrying cannot help: Google Play will not deliver this app to us. Absent means another fetch request may succeed."
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "description": "Status read only: when the job last moved."
    },
    "packageName": {
      "type": "string",
      "description": "The package this fetch is for."
    }
  }
}
get_fetch_status annotations
{
  "title": "Check a fetch",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": false
}
request_variant — Request a variant build
Advertised by the server during the last successful anonymous check.
Open-world

Ask for an on-demand build of an app's newest release that the ordinary download does not carry: every language (`+all-langs`), the 32-bit ARM build (`armeabi-v7a`), both ARM ABIs in one bundle (`universal`), the newest build for an older Android version (`@sdkNN`), or exactly what one real device gets (`device:<slug>`). We must already hold the ordinary build — call list_versions first, and request_fetch if it is empty. Costs 10 credits, even when the variant is already built (`READY`); free only when a build of that variant is already running (`FETCHING`). A build usually takes a few minutes: poll get_variant_status, not this tool, then pass its `variantId` to get_download_url. Builds are temporary — see `expiresAt`.

request_variant input schema
{
  "type": "object",
  "required": [
    "packageName",
    "variant"
  ],
  "properties": {
    "variant": {
      "type": "string",
      "maxLength": 80,
      "description": "Which build: `<arch>[+all-langs][@sdkNN|@vcN]` with arch one of arm64-v8a, armeabi-v7a, universal, or `device:<slug>[+all-langs]`. Plain `arm64-v8a` is the ordinary build and not a variant. `+all-langs` carries every language Google Play has rather than the standard 24; armeabi-v7a is the 32-bit ARM build; universal is both ARM ABIs in one bundle; `@sdkNN` (NN one of 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24) asks for the newest build that runs on that Android version; `@vcN` asks for one exact build by its versionCode (not combined with `@sdkNN`; refused with UNLIKELY_VERSION when N is not written like this app's build numbers); `device:<slug>` is exactly what Play serves that device and takes neither pin. Device slugs: pixel-9a, galaxy-s25-ultra, nothing-phone-1, samsung-f34-5g, samsung-s20-plus, samsung-a13-5g, redmi-note-12-4g, xiaomi-11-lite-5g-ne, poco-f1, oneplus-8-pro, huawei-mate-20, xperia-5, oppo-r17, xiaomi-mi-a1, redmi-7, realme-5i, nokia-1-3, realme-5-pro, samsung-j5-prime, pixel-9-pro-fold, pixel-tablet, shield-tv, bravia-vu2, hp-googlebook-14, play-games-pc. Examples: `armeabi-v7a`, `universal+all-langs`, `arm64-v8a@sdk28`, `arm64-v8a@vc1234`, `device:pixel-9a`."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
request_variant output schema
{
  "type": "object",
  "required": [
    "packageName",
    "variant",
    "status"
  ],
  "properties": {
    "build": {
      "type": "object",
      "required": [
        "variantId",
        "variant",
        "arch",
        "versionName",
        "versionCode",
        "minSdk",
        "fileSize",
        "sha256",
        "format",
        "expiresAt"
      ],
      "properties": {
        "arch": {
          "type": "string",
          "description": "ABIs the bundle carries code for, `+`-joined for universal."
        },
        "format": {
          "type": "string",
          "description": "'apk', 'xapk' or 'apks'."
        },
        "minSdk": {
          "type": "integer",
          "description": "Minimum Android API level."
        },
        "sha256": {
          "type": "string",
          "description": "SHA-256 of the file, lowercase hex."
        },
        "variant": {
          "type": "string",
          "description": "The variant name this build answers."
        },
        "fileSize": {
          "type": "integer",
          "description": "Bytes."
        },
        "expiresAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the build is deleted. Mint before then; afterwards, request it again."
        },
        "variantId": {
          "type": "string",
          "description": "Pass to the download call as `variantId`, beside the ordinary build's `apkId`."
        },
        "versionCode": {
          "type": "integer",
          "description": "Build number. Usually the newest release; older for an `@sdkNN` or device pin when that is what Play serves such a device."
        },
        "versionName": {
          "type": "string",
          "description": "Version string of the build."
        }
      }
    },
    "error": {
      "type": "string",
      "description": "FAILED only: why, in words safe to show a person."
    },
    "status": {
      "enum": [
        "READY",
        "FETCHING",
        "FAILED",
        "NEEDS_FETCH"
      ],
      "type": "string",
      "description": "READY: `build` is live — mint it. FETCHING: being built; poll again in 10–30 seconds. FAILED: the last build failed, see `error`; requesting again retries. NEEDS_FETCH (status only): nobody has asked for this variant of the current release, or its build expired — request it."
    },
    "variant": {
      "type": "string",
      "description": "The variant asked about."
    },
    "packageName": {
      "type": "string",
      "description": "The package."
    }
  }
}
request_variant annotations
{
  "title": "Request a variant build",
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
get_variant_status — Check a variant build
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Where one variant of an app stands: READY with the build (its `variantId` goes to get_download_url), FETCHING (keep waiting, 10–30 seconds between checks), FAILED with a reason (request_variant retries), or NEEDS_FETCH (nobody has asked for it, or it expired — call request_variant). Free when the build is still running (`FETCHING`); any other answer costs 1 credit, including when nobody has requested that variant (`NEEDS_FETCH`).

get_variant_status input schema
{
  "type": "object",
  "required": [
    "packageName",
    "variant"
  ],
  "properties": {
    "variant": {
      "type": "string",
      "maxLength": 80,
      "description": "Which build: `<arch>[+all-langs][@sdkNN|@vcN]` with arch one of arm64-v8a, armeabi-v7a, universal, or `device:<slug>[+all-langs]`. Plain `arm64-v8a` is the ordinary build and not a variant. `+all-langs` carries every language Google Play has rather than the standard 24; armeabi-v7a is the 32-bit ARM build; universal is both ARM ABIs in one bundle; `@sdkNN` (NN one of 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24) asks for the newest build that runs on that Android version; `@vcN` asks for one exact build by its versionCode (not combined with `@sdkNN`; refused with UNLIKELY_VERSION when N is not written like this app's build numbers); `device:<slug>` is exactly what Play serves that device and takes neither pin. Device slugs: pixel-9a, galaxy-s25-ultra, nothing-phone-1, samsung-f34-5g, samsung-s20-plus, samsung-a13-5g, redmi-note-12-4g, xiaomi-11-lite-5g-ne, poco-f1, oneplus-8-pro, huawei-mate-20, xperia-5, oppo-r17, xiaomi-mi-a1, redmi-7, realme-5i, nokia-1-3, realme-5-pro, samsung-j5-prime, pixel-9-pro-fold, pixel-tablet, shield-tv, bravia-vu2, hp-googlebook-14, play-games-pc. Examples: `armeabi-v7a`, `universal+all-langs`, `arm64-v8a@sdk28`, `arm64-v8a@vc1234`, `device:pixel-9a`."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_variant_status output schema
{
  "type": "object",
  "required": [
    "packageName",
    "variant",
    "status"
  ],
  "properties": {
    "build": {
      "type": "object",
      "required": [
        "variantId",
        "variant",
        "arch",
        "versionName",
        "versionCode",
        "minSdk",
        "fileSize",
        "sha256",
        "format",
        "expiresAt"
      ],
      "properties": {
        "arch": {
          "type": "string",
          "description": "ABIs the bundle carries code for, `+`-joined for universal."
        },
        "format": {
          "type": "string",
          "description": "'apk', 'xapk' or 'apks'."
        },
        "minSdk": {
          "type": "integer",
          "description": "Minimum Android API level."
        },
        "sha256": {
          "type": "string",
          "description": "SHA-256 of the file, lowercase hex."
        },
        "variant": {
          "type": "string",
          "description": "The variant name this build answers."
        },
        "fileSize": {
          "type": "integer",
          "description": "Bytes."
        },
        "expiresAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the build is deleted. Mint before then; afterwards, request it again."
        },
        "variantId": {
          "type": "string",
          "description": "Pass to the download call as `variantId`, beside the ordinary build's `apkId`."
        },
        "versionCode": {
          "type": "integer",
          "description": "Build number. Usually the newest release; older for an `@sdkNN` or device pin when that is what Play serves such a device."
        },
        "versionName": {
          "type": "string",
          "description": "Version string of the build."
        }
      }
    },
    "error": {
      "type": "string",
      "description": "FAILED only: why, in words safe to show a person."
    },
    "status": {
      "enum": [
        "READY",
        "FETCHING",
        "FAILED",
        "NEEDS_FETCH"
      ],
      "type": "string",
      "description": "READY: `build` is live — mint it. FETCHING: being built; poll again in 10–30 seconds. FAILED: the last build failed, see `error`; requesting again retries. NEEDS_FETCH (status only): nobody has asked for this variant of the current release, or its build expired — request it."
    },
    "variant": {
      "type": "string",
      "description": "The variant asked about."
    },
    "packageName": {
      "type": "string",
      "description": "The package."
    }
  }
}
get_variant_status annotations
{
  "title": "Check a variant build",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": false
}
list_developer_apps — List a publisher's apps
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Every app we hold from one publisher, most-installed first. The slug comes from `developerSlug` on any app row.

list_developer_apps input schema
{
  "type": "object",
  "required": [
    "slug"
  ],
  "properties": {
    "per": {
      "type": "integer",
      "maximum": 96,
      "minimum": 1,
      "description": "Rows per page, 1–96. Defaults to 24."
    },
    "page": {
      "type": "integer",
      "minimum": 1,
      "description": "1-based page. Defaults to 1."
    },
    "slug": {
      "type": "string",
      "description": "Publisher slug, from an app's developerSlug field."
    }
  },
  "additionalProperties": false
}
list_developer_apps output schema
{
  "type": "object",
  "required": [
    "developer",
    "items"
  ],
  "properties": {
    "page": {
      "type": "integer",
      "description": "1-based page number of this result."
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "name",
          "developer",
          "type",
          "availability",
          "hasFiles"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "App title."
          },
          "type": {
            "enum": [
              "game",
              "app"
            ],
            "type": "string",
            "description": "Game or app."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "description": "Play star rating, 0–5. Null when the app is not on Google Play, or when Play gave it no rating."
          },
          "iconUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store artwork URL."
          },
          "ratings": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Vote count behind `rating`. Null on rows seeded from a list scrape."
          },
          "version": {
            "type": "string",
            "description": "Latest version NAME known ('2.24.9'). May be 'Varies with device'."
          },
          "hasFiles": {
            "type": "boolean",
            "description": "We hold at least one downloadable file. Check before spending a download."
          },
          "installs": {
            "type": [
              "string",
              "null"
            ],
            "description": "Google Play's install BUCKET, verbatim ('10,000,000+'). A string, not a number — Play publishes no exact count. Null when the app is not on Google Play."
          },
          "developer": {
            "type": "string",
            "description": "Publisher name as scraped."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When Play last published an update, ISO 8601."
          },
          "categoryId": {
            "type": "string",
            "description": "Category id, as listed by the categories call."
          },
          "packageName": {
            "type": "string",
            "description": "Android package id — the identifier every app call takes."
          },
          "availability": {
            "enum": [
              "available",
              "unavailable"
            ],
            "type": "string",
            "description": "'unavailable' means a download call will be refused — a takedown, a paid app we cannot obtain, or a listing under the operator's rating bar."
          },
          "categoryName": {
            "type": "string",
            "description": "Human-readable category."
          },
          "onGooglePlay": {
            "type": "boolean",
            "description": "False for an app its developer published on this site directly, with no Google Play listing: the Play fields are then null and the Play listing call answers 404."
          },
          "developerSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Key for the developer lookup. Null when the row has no publisher."
          }
        }
      }
    },
    "total": {
      "type": "integer",
      "description": "Total matching rows across all pages."
    },
    "perPage": {
      "type": "integer",
      "description": "Rows per page."
    },
    "developer": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "Publisher name."
        },
        "slug": {
          "type": "string",
          "description": "Publisher slug."
        },
        "total": {
          "type": "integer",
          "description": "Apps held."
        }
      }
    },
    "totalPages": {
      "type": "integer",
      "description": "Number of pages available."
    }
  }
}
list_developer_apps annotations
{
  "title": "List a publisher's apps",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_categories — List categories
Advertised by the server during the last successful anonymous check.
Advertised read-only

The category taxonomy, for filtering a search. Free — this call costs no credits.

list_categories input schema
{
  "type": "object",
  "additionalProperties": false
}
list_categories output schema
{
  "type": "object",
  "required": [
    "categories"
  ],
  "properties": {
    "categories": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass as the category filter on a search."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "type": {
            "enum": [
              "game",
              "app"
            ],
            "type": "string",
            "description": "Which tree it belongs to."
          }
        }
      }
    }
  }
}
list_categories annotations
{
  "title": "List categories",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true
}
list_chart — List a chart
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

A ranked section of the catalogue. Google Play's charts (hot, grossing, latest, editors-choice), the catalogue's newest releases (new), what people downloaded here in about the last day (popular), or the newest builds we stored (updated). An unrecognised section falls back to 'hot' rather than failing.

list_chart input schema
{
  "type": "object",
  "properties": {
    "type": {
      "enum": [
        "game",
        "app"
      ],
      "type": "string",
      "description": "Restrict to games or apps. Ignored by `popular`, which is one mixed list."
    },
    "limit": {
      "type": "integer",
      "maximum": 50,
      "minimum": 1,
      "description": "Rows to return, 1–50. Defaults to 24."
    },
    "section": {
      "enum": [
        "hot",
        "grossing",
        "latest",
        "new",
        "editors-choice",
        "popular",
        "updated"
      ],
      "type": "string",
      "default": "hot",
      "description": "Which chart. hot, grossing, latest and editors-choice are Google Play's charts; new is the catalogue's newest releases; popular is what people downloaded here in about the last day; updated is the newest builds we stored. An unrecognised section falls back to 'hot'."
    }
  },
  "additionalProperties": false
}
list_chart output schema
{
  "type": "object",
  "required": [
    "section",
    "items"
  ],
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "name",
          "developer",
          "type",
          "availability",
          "hasFiles"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "App title."
          },
          "type": {
            "enum": [
              "game",
              "app"
            ],
            "type": "string",
            "description": "Game or app."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "description": "Play star rating, 0–5. Null when the app is not on Google Play, or when Play gave it no rating."
          },
          "iconUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store artwork URL."
          },
          "ratings": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Vote count behind `rating`. Null on rows seeded from a list scrape."
          },
          "version": {
            "type": "string",
            "description": "Latest version NAME known ('2.24.9'). May be 'Varies with device'."
          },
          "hasFiles": {
            "type": "boolean",
            "description": "We hold at least one downloadable file. Check before spending a download."
          },
          "installs": {
            "type": [
              "string",
              "null"
            ],
            "description": "Google Play's install BUCKET, verbatim ('10,000,000+'). A string, not a number — Play publishes no exact count. Null when the app is not on Google Play."
          },
          "developer": {
            "type": "string",
            "description": "Publisher name as scraped."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When Play last published an update, ISO 8601."
          },
          "categoryId": {
            "type": "string",
            "description": "Category id, as listed by the categories call."
          },
          "packageName": {
            "type": "string",
            "description": "Android package id — the identifier every app call takes."
          },
          "availability": {
            "enum": [
              "available",
              "unavailable"
            ],
            "type": "string",
            "description": "'unavailable' means a download call will be refused — a takedown, a paid app we cannot obtain, or a listing under the operator's rating bar."
          },
          "categoryName": {
            "type": "string",
            "description": "Human-readable category."
          },
          "onGooglePlay": {
            "type": "boolean",
            "description": "False for an app its developer published on this site directly, with no Google Play listing: the Play fields are then null and the Play listing call answers 404."
          },
          "developerSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Key for the developer lookup. Null when the row has no publisher."
          }
        }
      }
    },
    "section": {
      "type": "string",
      "description": "The chart that was returned."
    }
  }
}
list_chart annotations
{
  "title": "List a chart",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_similar_apps — List similar apps
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Apps related to one package — alternatives and neighbours, as the app's page on the site shows them. Free when there is nothing to list (an empty `items`). A package that is not in the catalogue is a 404 rather than a guess.

list_similar_apps input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "limit": {
      "type": "integer",
      "maximum": 24,
      "minimum": 1,
      "description": "Rows to return, 1–24. Defaults to 8."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
list_similar_apps output schema
{
  "type": "object",
  "required": [
    "packageName",
    "items"
  ],
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "name",
          "developer",
          "type",
          "availability",
          "hasFiles"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "App title."
          },
          "type": {
            "enum": [
              "game",
              "app"
            ],
            "type": "string",
            "description": "Game or app."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "description": "Play star rating, 0–5. Null when the app is not on Google Play, or when Play gave it no rating."
          },
          "iconUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store artwork URL."
          },
          "ratings": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Vote count behind `rating`. Null on rows seeded from a list scrape."
          },
          "version": {
            "type": "string",
            "description": "Latest version NAME known ('2.24.9'). May be 'Varies with device'."
          },
          "hasFiles": {
            "type": "boolean",
            "description": "We hold at least one downloadable file. Check before spending a download."
          },
          "installs": {
            "type": [
              "string",
              "null"
            ],
            "description": "Google Play's install BUCKET, verbatim ('10,000,000+'). A string, not a number — Play publishes no exact count. Null when the app is not on Google Play."
          },
          "developer": {
            "type": "string",
            "description": "Publisher name as scraped."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When Play last published an update, ISO 8601."
          },
          "categoryId": {
            "type": "string",
            "description": "Category id, as listed by the categories call."
          },
          "packageName": {
            "type": "string",
            "description": "Android package id — the identifier every app call takes."
          },
          "availability": {
            "enum": [
              "available",
              "unavailable"
            ],
            "type": "string",
            "description": "'unavailable' means a download call will be refused — a takedown, a paid app we cannot obtain, or a listing under the operator's rating bar."
          },
          "categoryName": {
            "type": "string",
            "description": "Human-readable category."
          },
          "onGooglePlay": {
            "type": "boolean",
            "description": "False for an app its developer published on this site directly, with no Google Play listing: the Play fields are then null and the Play listing call answers 404."
          },
          "developerSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Key for the developer lookup. Null when the row has no publisher."
          }
        }
      }
    },
    "packageName": {
      "type": "string",
      "description": "The package these are related to."
    }
  }
}
list_similar_apps annotations
{
  "title": "List similar apps",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_play_details — Get the Google Play listing
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Google Play's full store listing for one package, in any storefront: the star histogram, install estimate, IAP price range, release date, declared permissions, content-rating descriptors, legal publisher and contact details — the store data get_app leaves out. Usually null: the written-review count, sale pricing and the Play Pass and pre-registration flags. Pass `lang`/`country` for another storefront (prices, text and availability differ); the default en/us listing is served from our own scrape while it is fresh and scraped and stored otherwise. Works for packages outside the catalogue. Charged even when Google Play doesn't list the app (`APP_NOT_FOUND`); a busy or failed Google Play is refunded.

get_play_details input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "lang": {
      "type": "string",
      "description": "Language code, e.g. en, ja, pt-BR. Defaults to en."
    },
    "country": {
      "type": "string",
      "description": "Two-letter storefront country, e.g. us, jp. Defaults to us."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_play_details output schema
{
  "type": "object",
  "required": [
    "packageName",
    "lang",
    "country",
    "source",
    "fetchedAt",
    "title",
    "developer",
    "rating",
    "installs",
    "pricing",
    "permissions"
  ],
  "properties": {
    "url": {
      "type": [
        "string",
        "null"
      ],
      "description": "The store page."
    },
    "lang": {
      "type": "string",
      "description": "Language the listing was read in."
    },
    "genre": {
      "type": [
        "string",
        "null"
      ],
      "description": "Store genre label, e.g. 'Strategy'."
    },
    "title": {
      "type": "string",
      "description": "Store title."
    },
    "rating": {
      "type": "object",
      "required": [
        "score",
        "scoreText",
        "count",
        "reviews",
        "histogram"
      ],
      "properties": {
        "count": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Star ratings in total."
        },
        "score": {
          "type": [
            "number",
            "null"
          ],
          "description": "Average stars, 1–5."
        },
        "reviews": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Written reviews, a subset of `count`. Web store only, so usually null."
        },
        "histogram": {
          "type": [
            "object",
            "null"
          ],
          "required": [
            "1",
            "2",
            "3",
            "4",
            "5"
          ],
          "properties": {
            "1": {
              "type": "integer"
            },
            "2": {
              "type": "integer"
            },
            "3": {
              "type": "integer"
            },
            "4": {
              "type": "integer"
            },
            "5": {
              "type": "integer"
            }
          },
          "description": "Ratings per star."
        },
        "scoreText": {
          "type": [
            "string",
            "null"
          ],
          "description": "The average as the store prints it."
        }
      }
    },
    "source": {
      "enum": [
        "catalog",
        "live"
      ],
      "type": "string",
      "description": "`catalog`: the stored en/us listing, at most a day old. `live`: asked of Google Play for this call or a recent one."
    },
    "country": {
      "type": "string",
      "description": "Storefront country the listing was read from."
    },
    "genreId": {
      "type": [
        "string",
        "null"
      ],
      "description": "Store genre id, e.g. 'GAME_STRATEGY'."
    },
    "pricing": {
      "type": "object",
      "required": [
        "free",
        "price",
        "currency",
        "priceText",
        "originalPrice",
        "saleEndsText",
        "inAppPurchases",
        "iapRange",
        "adSupported",
        "playPass",
        "preregister"
      ],
      "properties": {
        "free": {
          "type": "boolean",
          "description": "Free to install."
        },
        "price": {
          "type": "number",
          "description": "Price, 0 when free."
        },
        "currency": {
          "type": [
            "string",
            "null"
          ],
          "description": "ISO 4217 currency of `price`."
        },
        "iapRange": {
          "type": [
            "string",
            "null"
          ],
          "description": "E.g. '$0.99 - $399.99 per item'."
        },
        "playPass": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Included in Play Pass. Web store only, so usually null."
        },
        "priceText": {
          "type": [
            "string",
            "null"
          ],
          "description": "Price as the store prints it."
        },
        "adSupported": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Contains ads."
        },
        "preregister": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Open for pre-registration, not yet released. Web store only, so usually null."
        },
        "saleEndsText": {
          "type": [
            "string",
            "null"
          ],
          "description": "When the sale ends, as the store prints it. Web store only, so usually null."
        },
        "originalPrice": {
          "type": [
            "number",
            "null"
          ],
          "description": "Pre-sale price, during a sale. Web store only, so usually null."
        },
        "inAppPurchases": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Offers in-app purchases."
        }
      }
    },
    "summary": {
      "type": [
        "string",
        "null"
      ],
      "description": "Short promo text."
    },
    "version": {
      "type": [
        "string",
        "null"
      ],
      "description": "Current version name."
    },
    "installs": {
      "type": "object",
      "required": [
        "text",
        "min",
        "max"
      ],
      "properties": {
        "max": {
          "type": [
            "integer",
            "null"
          ],
          "description": "The store's own unrounded estimate."
        },
        "min": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Lower bound of the bucket."
        },
        "text": {
          "type": [
            "string",
            "null"
          ],
          "description": "The store's bucket, verbatim, e.g. '1,000,000+'."
        }
      }
    },
    "released": {
      "type": [
        "string",
        "null"
      ],
      "description": "First publication, as the store prints it."
    },
    "developer": {
      "type": "object",
      "required": [
        "name",
        "id",
        "legalName",
        "email",
        "website",
        "address"
      ],
      "properties": {
        "id": {
          "type": [
            "string",
            "null"
          ],
          "description": "Google Play's developer id, when published."
        },
        "name": {
          "type": [
            "string",
            "null"
          ],
          "description": "Publisher name."
        },
        "email": {
          "type": [
            "string",
            "null"
          ],
          "description": "Contact email."
        },
        "address": {
          "type": [
            "string",
            "null"
          ],
          "description": "Postal address."
        },
        "website": {
          "type": [
            "string",
            "null"
          ],
          "description": "Website."
        },
        "legalName": {
          "type": [
            "string",
            "null"
          ],
          "description": "Registered legal name."
        }
      }
    },
    "fetchedAt": {
      "type": "string",
      "description": "When Google Play produced this listing, ISO 8601."
    },
    "updatedAt": {
      "type": [
        "string",
        "null"
      ],
      "description": "Last update, ISO 8601."
    },
    "packageName": {
      "type": "string",
      "description": "The package."
    },
    "permissions": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "string"
      },
      "description": "Permissions as the store lists them. Null when this listing's source does not publish them — the per-build manifest has the authoritative list."
    },
    "contentRating": {
      "type": [
        "string",
        "null"
      ],
      "description": "E.g. 'Teen'."
    },
    "recentChanges": {
      "type": [
        "string",
        "null"
      ],
      "description": "What's new."
    },
    "privacyPolicyUrl": {
      "type": [
        "string",
        "null"
      ],
      "description": "Privacy policy link."
    },
    "minAndroidVersion": {
      "type": [
        "string",
        "null"
      ],
      "description": "Machine form, e.g. '5.0' or 'VARY'."
    },
    "minAndroidVersionText": {
      "type": [
        "string",
        "null"
      ],
      "description": "Store sentence, e.g. '5.0 and up'."
    },
    "contentRatingDescription": {
      "type": [
        "string",
        "null"
      ],
      "description": "E.g. 'Mild Violence'."
    }
  }
}
get_play_details annotations
{
  "title": "Get the Google Play listing",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_reviews — List Google Play reviews
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

One page of Google Play user reviews for a package, live: stars, text, helpful votes, the app version the reviewer ran, and the developer's reply. Sort by newest (default), rating or helpfulness; up to 100 a page. Walk further pages by passing `nextCursor` back as `cursor` with the same sort, num and storefront — each page is a separate charge, so ask for what you need. Free when the page is empty (an empty `items`); a busy or failed Google Play is refunded.

list_reviews input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "num": {
      "type": "integer",
      "maximum": 100,
      "minimum": 1,
      "description": "Reviews per page, 1–100. Defaults to 40."
    },
    "lang": {
      "type": "string",
      "description": "Language code, e.g. en, ja, pt-BR. Defaults to en."
    },
    "sort": {
      "enum": [
        "newest",
        "rating",
        "helpfulness"
      ],
      "type": "string",
      "description": "newest (default), rating, or helpfulness."
    },
    "cursor": {
      "type": "string",
      "description": "`nextCursor` from the previous page. Omit for the first page; keep sort, num and the storefront the same while walking."
    },
    "country": {
      "type": "string",
      "description": "Two-letter storefront country, e.g. us, jp. Defaults to us."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
list_reviews output schema
{
  "type": "object",
  "required": [
    "packageName",
    "lang",
    "country",
    "sort",
    "items",
    "nextCursor",
    "fetchedAt"
  ],
  "properties": {
    "lang": {
      "type": "string",
      "description": "Language the reviews were read in."
    },
    "sort": {
      "enum": [
        "newest",
        "rating",
        "helpfulness"
      ],
      "type": "string",
      "description": "Order of `items`."
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id",
          "score",
          "text",
          "thumbsUp",
          "reply"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Review id."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Link to the review on the store."
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "description": "The review."
          },
          "reply": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "text",
              "postedAt"
            ],
            "properties": {
              "text": {
                "type": "string",
                "description": "Reply text."
              },
              "postedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ISO 8601."
              }
            },
            "description": "The developer's public reply."
          },
          "score": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Stars, 1–5."
          },
          "author": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reviewer's display name."
          },
          "postedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601."
          },
          "thumbsUp": {
            "type": "integer",
            "description": "'Helpful' votes."
          },
          "appVersion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Version the reviewer had installed, when recorded."
          },
          "authorImage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reviewer's avatar URL."
          }
        }
      }
    },
    "country": {
      "type": "string",
      "description": "Storefront country."
    },
    "fetchedAt": {
      "type": "string",
      "description": "When Google Play produced this page, ISO 8601."
    },
    "nextCursor": {
      "type": [
        "string",
        "null"
      ],
      "description": "Pass back as `cursor` for the next page, with the same sort, num and storefront. Null at the end."
    },
    "packageName": {
      "type": "string",
      "description": "The package."
    }
  }
}
list_reviews annotations
{
  "title": "List Google Play reviews",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": false
}
get_version_security — Check a build's integrity
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

What one build actually IS: its SHA-256, the anti-malware verdict, the certificates that signed it, and every Android permission it declares with a plain-English gloss. Defaults to the newest build, so you do not need list_versions first. `findings` is the part to read first: it applies our rules to the manifest and native facts on the same response and names what they imply — a debuggable build, a version that disagrees with the listing, cleartext traffic, components any app on the device can start, a packer. Two severities, `warning` and `notice`, and NEITHER is a verdict; an empty `findings` is not an all-clear, because a manifest nobody read produces no findings either. Read this BEFORE recommending an install, and read the nulls carefully — `scanned: false` and `virusTotal: null` mean nobody has looked, which is not the same as clean, and `permissions: null` means the manifest was never read, which is not the same as an app that asks for nothing.

get_version_security input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_version_security output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId",
    "versionName",
    "versionCode",
    "verdict",
    "scanned",
    "sha256"
  ],
  "properties": {
    "md5": {
      "type": [
        "string",
        "null"
      ],
      "description": "MD5 of the artifact, computed at ingest. Null on builds ingested before it was computed — never a claim the file has no MD5."
    },
    "sha1": {
      "type": [
        "string",
        "null"
      ],
      "description": "SHA-1 of the artifact, when one was reported. Computed at ingest where available, else taken from the VirusTotal report. An identifier for matching a published IOC — sha256 is the integrity check."
    },
    "apkId": {
      "type": "string",
      "description": "The build this describes — the one asked for, or the newest held."
    },
    "pairip": {
      "type": "boolean",
      "description": "Google PairIP licence protection, which breaks a plain sideload. The same signal reaches natives.protectors as the id 'pairip' when the native walk ran; this field is the scan's own boolean and answers even when natives is null."
    },
    "sha256": {
      "type": "string",
      "description": "SHA-256 of the artifact. The same value the version list and the download call publish."
    },
    "natives": {
      "type": [
        "object",
        "null"
      ],
      "description": "Native libraries the artifact ships: abis, libs (name, largest uncompressed size, the ABIs carrying it) and protectors. NULL means the archive's directory could not be read; empty lists are the factual 'ships no native libraries', which is ordinary for a split bundle's base APK. A protector is NOT a malware signal — anti-piracy and anti-tamper are what they are sold for — but it does mean the app's own code was not open to inspection, so treat other analysis of the same artifact as partial."
    },
    "scanned": {
      "type": "boolean",
      "description": "Something has analysed this artifact. When false, every field below is null or zero and NONE of it means 'clean'."
    },
    "verdict": {
      "enum": [
        "verified",
        "secure",
        "caution",
        "dangerous",
        "unverified"
      ],
      "type": "string",
      "description": "Our overall reading of the artifact."
    },
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id",
          "severity",
          "title",
          "detail"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable machine-readable id: debuggable | version-mismatch | cleartext-traffic | backup-allowed | shared-user-id | exported-components | legacy-permission-model | packed. Branch on this, not on the prose."
          },
          "title": {
            "type": "string",
            "description": "Short label."
          },
          "detail": {
            "type": "string",
            "description": "One sentence: what it means and what it does not, with the specifics where there are any. Both halves matter — every one of these is ordinary in some app shipping today, and relaying only the first clause turns a fact into an accusation."
          },
          "severity": {
            "enum": [
              "warning",
              "notice"
            ],
            "type": "string",
            "description": "warning — this file is not what a release build of this app should look like (a debuggable build, or a version that disagrees with the listing). notice — true, worth knowing, and routinely a deliberate choice of an ordinary app. Neither is a verdict: the anti-malware verdict is a separate field and a finding never moves it."
          }
        }
      },
      "description": "What manifest and natives already imply, read for a caller who should not have to know Android to notice it. EMPTY IS NOT AN ALL-CLEAR: absence is never a finding, so an empty array also covers the case where the manifest was never read — manifest: null beside it is how you tell those apart. Warnings first."
    },
    "manifest": {
      "type": [
        "object",
        "null"
      ],
      "description": "What the artifact's own AndroidManifest.xml declares beyond the permissions it requests: targetSdk, compileSdk, debuggable, usesCleartextTraffic, allowBackup, sharedUserId, installLocation, the permissions it DEFINES, uses-feature, uses-library, components exported without a permission guard, and its own versionCode/versionName. NULL means nobody read the manifest. A key ABSENT inside the object means the manifest does not declare that attribute — never that it declares the platform default. Its versionCode against the versionCode above is the only check there is on what the store claimed: they disagreeing means the file is not the build the listing describes."
    },
    "scannedAt": {
      "type": [
        "string",
        "null"
      ],
      "description": "When the analysis ran, ISO 8601."
    },
    "signature": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "certs": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "sha256",
              "debugKey",
              "findings"
            ],
            "properties": {
              "sha1": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "SHA-1 of the same certificate, when the scan reported one. Not derivable from the SHA-256."
              },
              "match": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Against our record for this package: 'trusted', 'known', 'first-seen' or 'mismatch'."
              },
              "issuer": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Issuer CN/O. Equal to the subject for a self-signed key, which is every Android signing key."
              },
              "scheme": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Signature scheme it was found under: 'v2', 'v3' or 'v3.1'."
              },
              "sha256": {
                "type": "string",
                "description": "SHA-256 fingerprint of the DER certificate — the value that identifies a signing key."
              },
              "keySize": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Key size in bits."
              },
              "subject": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Subject CN/O."
              },
              "debugKey": {
                "type": "boolean",
                "description": "Signed with the Android SDK's debug key, whose private half is on every machine that built the app. A release signed this way is a leak, a rebuild or a repack, and cannot install over the real app. Equals findings.includes('debug-key')."
              },
              "findings": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "What is wrong with this certificate, from its own facts: 'debug-key', 'weak-key' (RSA/DSA modulus under 2048 bits), 'weak-signature-algorithm' (signed with MD5 or SHA-1). Empty is NOT an all-clear — a certificate we could not fully parse also yields none. Judges the key and its algorithm, never its dates: Android pins the key, so expiry is reported as context in validUntil, not as a finding."
              },
              "validFrom": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ISO 8601. Android pins the key and not its dates, so this is context, never a verdict."
              },
              "validUntil": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ISO 8601."
              },
              "keyAlgorithm": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "'RSA', 'EC P-256', …"
              }
            }
          }
        },
        "match": {
          "type": [
            "string",
            "null"
          ],
          "description": "Best match across every signer certificate found."
        },
        "scheme": {
          "type": [
            "string",
            "null"
          ],
          "description": "APK Signature Scheme found."
        },
        "status": {
          "type": "string",
          "description": "Validation outcome."
        },
        "blocked": {
          "type": "boolean",
          "description": "Signed with a key on the catalogue-wide blocklist. The download is refused whatever the anti-malware result said."
        },
        "knownSignatures": {
          "type": "integer",
          "description": "Certificates on record for this package. More than one means an update only installs over a matching signature."
        }
      },
      "description": "Signature validation. Null when that stage did not run."
    },
    "virusTotal": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "meta": {
          "type": [
            "object",
            "null"
          ],
          "description": "What VirusTotal knows that we cannot derive: threatLabel (a NAME for what the vendors are counting), threatCategories, firstSeen and timesSubmitted (submission history), reputation, yaraRules, sandboxVerdicts. Fields are absent when the report did not carry them; the object is null for a verdict that came from a fresh upload, which sees an analysis with no history. A recent firstSeen with a low timesSubmitted is the repackage shape, and is only meaningful NEXT TO A DETECTION — on release day every legitimate build looks the same."
        },
        "flagged": {
          "type": "integer",
          "description": "Vendors that called the file malicious or suspicious."
        },
        "vendors": {
          "type": "integer",
          "description": "Vendors that returned a result."
        },
        "flaggedBy": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "result": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "vendor": {
                "type": "string"
              },
              "category": {
                "type": "string"
              }
            }
          },
          "description": "Which vendors flagged it, and as what."
        }
      },
      "description": "Anti-malware analysis for this hash. NULL means nobody has looked — only a non-null object with flagged: 0 is an all-clear."
    },
    "packageName": {
      "type": "string",
      "description": "The package asked for."
    },
    "permissions": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "object",
        "required": [
          "name",
          "label",
          "sensitive"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Raw manifest name, e.g. 'android.permission.CAMERA'."
          },
          "label": {
            "type": "string",
            "description": "Readable label ('Precise location')."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Capability family ('Location'); null for unrecognised names."
          },
          "standard": {
            "type": "boolean",
            "description": "Declared under Android's own namespace, not by the app or an SDK."
          },
          "sensitive": {
            "type": "boolean",
            "description": "Android-dangerous or special-access. These are the ones worth reading."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the app can do with it; null when unrecognised."
          }
        }
      },
      "description": "NULL means the manifest was never read — not that the app declares none. An empty array is the genuine 'declares none'."
    },
    "versionCode": {
      "type": "integer",
      "description": "Its build number."
    },
    "versionName": {
      "type": "string",
      "description": "Version name of that build."
    },
    "permissionSummary": {
      "type": [
        "string",
        "null"
      ],
      "description": "'24 declared · 5 sensitive'. Null whenever `permissions` is."
    }
  }
}
get_version_security annotations
{
  "title": "Check a build's integrity",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_sdks — List a build's third-party SDKs
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

What OTHER companies' code is inside an app: the ad, analytics, attribution, crash-reporting, push and sign-in libraries matched in the build, each with its vendor and where it was recognised. Defaults to the newest build. This is the privacy question, not the safety one — these libraries are ordinary parts of shipping an app, so report them as what the app carries and never as evidence of harm. READ THE BOUNDS BEFORE THE LIST: `analyzed: false` means nobody looked; `codeCoverage` says whether the app's own code was read at all, and in full; a non-empty `protectors` means the build is packed and its real code was never open to inspection. An empty `sdks` is only close to 'none' when `analyzed` is true and `codeCoverage` is `full`. Relay `summary` whole — it states the finding and its bound in one sentence, and dropping the second half is the standard way to get this wrong.

list_sdks input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
list_sdks output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId",
    "versionName",
    "versionCode",
    "analyzed",
    "summary",
    "sdks",
    "codeCoverage",
    "complete"
  ],
  "properties": {
    "dex": {
      "type": [
        "object",
        "null"
      ],
      "description": "How much code was read, when any was: read of total dex files, bytes, and truncated (one file ran out of budget part-way). NULL when none was read."
    },
    "sdks": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "evidence"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Corpus id. Stable within a corpus version — see `rules`."
          },
          "name": {
            "type": "string",
            "description": "Library name."
          },
          "vendor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who publishes it. NULL where the corpus records none."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "advertising | analytics | attribution | crash | push | social. NULL means the corpus names this library but we have not classified it — 'unclassified', never 'harmless'. Most of that bucket is a UI toolkit or an HTTP client with nothing to classify."
          },
          "evidence": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Where it was recognised: native (a bundled .so), dex (a package prefix in the app's own code), static (a declared uses-library), action (an intent action), or the manifest element that named it — application, activity, service, receiver, provider. Two entries is stronger evidence than one."
          },
          "categoryLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "English name for `category`. NULL whenever it is."
          }
        }
      },
      "description": "Ordered by category (advertising first, unclassified last), then name."
    },
    "apkId": {
      "type": "string",
      "description": "The build this describes — the one asked for, or the newest held."
    },
    "rules": {
      "type": [
        "string",
        "null"
      ],
      "description": "The corpus version that answered THIS request. Matching runs at read time against stored evidence, so two calls either side of a deploy may legitimately name a library the earlier one could not: the artifact has not changed, our ability to recognise what is in it has."
    },
    "capped": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Evidence tiers that hit their size cap while the artifact was being read, so the analysis worked from a subset of what the file carries. The one bound no later corpus can repair — fixing it needs the bytes back."
    },
    "sources": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Evidence classes actually SEARCHED — manifest, natives, dex — not what was found. Empty when analyzed is false. This is the field that separates 'we looked and found nothing' from 'we never looked'."
    },
    "summary": {
      "type": "string",
      "description": "One sentence stating the finding AND its bound. Relay both halves: 'none found' without 'and the code was not read' is the misreading this whole shape exists to prevent."
    },
    "analyzed": {
      "type": "boolean",
      "description": "Was this build's evidence ever collected? FALSE means nobody looked, and the empty list beside it says nothing about the app. TRUE with an empty list is the factual 'we looked and recognised none' — bounded by codeCoverage."
    },
    "complete": {
      "type": "boolean",
      "description": "The answer is bounded by none of the above: code read through, nothing capped, no protector. FALSE is the normal case and is not an error — it means this list is a floor, not a census."
    },
    "byCategory": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "category",
          "label",
          "count"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Libraries in this category."
          },
          "label": {
            "type": "string",
            "description": "English name."
          },
          "category": {
            "type": "string",
            "description": "Machine name."
          }
        }
      },
      "description": "Counts per category, same order. Empty when sdks is."
    },
    "protectors": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Commercial protectors on the build, by product name. NOT a malware signal — anti-piracy and anti-tamper are what they are sold for, Google's PairIP included — but a packed app's real code is not in classes.dex for anyone to read, so a short list beside a non-empty protectors is explained by it."
    },
    "packageName": {
      "type": "string",
      "description": "The package asked for."
    },
    "versionCode": {
      "type": "integer",
      "description": "Its build number."
    },
    "versionName": {
      "type": "string",
      "description": "Version name of that build."
    },
    "codeCoverage": {
      "enum": [
        "none",
        "partial",
        "full"
      ],
      "type": "string",
      "description": "How far the analysis reached into the app's OWN code: none — only the manifest and bundled libraries were read; partial — the code was read but not through; full — read through. Only 'full' makes an empty result mean anything close to 'none'."
    }
  }
}
list_sdks annotations
{
  "title": "List a build's third-party SDKs",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_protection — Check whether a build is packed
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Whether this build is wrapped in a commercial protector — DexProtector, Jiagu, Bangcle, Legu, AppSealing, Google's own PairIP — and what that means for everything else you can find out about it. Defaults to the newest build. A PROTECTOR IS NOT A MALWARE SIGNAL: anti-piracy, anti-tamper and anti-cheat are what these are sold for, and saying an app is 'suspicious' because it is packed is wrong about a large part of the catalogue. What it does mean is that the app's real code is not in classes.dex for anyone to read, so an empty SDK list, a short permission list or any other analysis of the same artifact is a floor and not a census — call this BEFORE you report that a build carries no trackers. `analyzed: false` means the archive was never opened, which is not the same as an unpacked build. Relay `summary` whole: it carries the finding and the bound in one sentence.

get_protection input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_protection output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId",
    "versionName",
    "versionCode",
    "protected",
    "protectors",
    "pairip",
    "analyzed",
    "codeCoverage",
    "summary"
  ],
  "properties": {
    "dex": {
      "type": [
        "object",
        "null"
      ],
      "description": "How much code was read, when any was: read of total dex files, bytes, and truncated (one file ran out of budget part-way). NULL when none was read."
    },
    "apkId": {
      "type": "string",
      "description": "The build this describes — the one asked for, or the newest held."
    },
    "capped": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Evidence tiers that hit their size cap while the artifact was being read, so the analysis worked from a subset of what the file carries. The one bound no later corpus can repair — fixing it needs the bytes back."
    },
    "pairip": {
      "type": "boolean",
      "description": "Google PairIP licence protection specifically. Beyond packing, it checks the app's licence at run time, so a plain sideload of the file may refuse to run."
    },
    "summary": {
      "type": "string",
      "description": "One sentence carrying both halves — what was found, and what it bounds. Relay it whole: 'packed with Jiagu' on its own turns a build-tool choice into an accusation, and the second clause is the part a reader can act on."
    },
    "analyzed": {
      "type": "boolean",
      "description": "Was the artifact ever opened? FALSE means every field below is a default and none of them is a finding."
    },
    "protected": {
      "type": "boolean",
      "description": "At least one commercial protector was identified. FALSE with analyzed:false above it means nobody looked — not that the build is unpacked."
    },
    "protectors": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "vendor"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Engine id — 'pairip', 'jiagu', 'dexprotector', 'bangcle', 'legu', … Branch on this."
          },
          "name": {
            "type": "string",
            "description": "Product name, or the id itself for one this deployment cannot name yet."
          },
          "vendor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who publishes it. Null rather than guessed for an unknown id."
          }
        }
      },
      "description": "What was identified. NOT a malware signal — anti-piracy and anti-tamper are what these are sold for, Google's own PairIP included. What it does mean is that the app's real code is not in classes.dex for anyone to read."
    },
    "packageName": {
      "type": "string",
      "description": "The package asked for."
    },
    "versionCode": {
      "type": "integer",
      "description": "Its build number."
    },
    "versionName": {
      "type": "string",
      "description": "Version name of that build."
    },
    "codeCoverage": {
      "enum": [
        "none",
        "partial",
        "full"
      ],
      "type": "string",
      "description": "How far the analysis reached into the app's OWN code: none — only the manifest and bundled libraries were read; partial — the code was read but not through; full — read through. Only 'full' makes an empty result mean anything close to 'none'."
    }
  }
}
get_protection annotations
{
  "title": "Check whether a build is packed",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_trackers — List a build's tracking and advertising SDKs
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

The privacy question on its own: which advertising, analytics, install-attribution, crash-reporting, push and social sign-in SDKs are in this build, and nothing else. `list_sdks` is the same evidence unfiltered — every recognised library including HTTP clients and UI toolkits — so use that one when you want the full inventory and this one when somebody asked what an app sends about them. A CATEGORY IS NOT AN ACCUSATION: all six are ordinary parts of shipping an app, and `unclassified` counts recognised libraries that are neither (unclassified means we take no position, never that a library is harmless). READ THE BOUNDS BEFORE THE LIST: `analyzed: false` means nobody looked; `codeCoverage` says whether the app's own code was read at all and in full; a non-empty `protectors` means the real code was never readable, and an empty list under one says nothing about the app. Relay `summary` whole — 'no trackers found' without its second clause is the standard way to get this wrong.

list_trackers input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
list_trackers output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId",
    "versionName",
    "versionCode",
    "analyzed",
    "summary",
    "trackers",
    "byCategory",
    "total",
    "unclassified",
    "codeCoverage",
    "complete"
  ],
  "properties": {
    "dex": {
      "type": [
        "object",
        "null"
      ],
      "description": "How much code was read, when any was: read of total dex files, bytes, and truncated (one file ran out of budget part-way). NULL when none was read."
    },
    "apkId": {
      "type": "string",
      "description": "The build this describes — the one asked for, or the newest held."
    },
    "rules": {
      "type": [
        "string",
        "null"
      ],
      "description": "The corpus version that answered THIS request. Matching runs at read time against stored evidence, so two calls either side of a deploy may legitimately name a library the earlier one could not: the artifact has not changed, our ability to recognise what is in it has."
    },
    "total": {
      "type": "integer",
      "description": "Classified libraries found — the number to quote."
    },
    "capped": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Evidence tiers that hit their size cap while the artifact was being read, so the analysis worked from a subset of what the file carries. The one bound no later corpus can repair — fixing it needs the bytes back."
    },
    "sources": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Evidence classes actually SEARCHED — manifest, natives, dex — not what was found. Empty when analyzed is false. This is the field that separates 'we looked and found nothing' from 'we never looked'."
    },
    "summary": {
      "type": "string",
      "description": "One sentence stating the finding AND its bound. Relay both halves: 'no trackers found' without 'and the code was not read' is the misreading this shape exists to prevent, and on a packed build it is false in the most damaging direction available."
    },
    "analyzed": {
      "type": "boolean",
      "description": "Was this build's evidence ever collected? FALSE means nobody looked, and the empty list beside it says nothing about the app. TRUE with an empty list is the factual 'we looked and recognised none' — bounded by codeCoverage."
    },
    "complete": {
      "type": "boolean",
      "description": "The answer is bounded by none of the above: code read through, nothing capped, no protector. FALSE is the normal case and is not an error — it means this list is a floor, not a census."
    },
    "trackers": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id",
          "name",
          "evidence"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Corpus id. Stable within a corpus version — see `rules`."
          },
          "name": {
            "type": "string",
            "description": "Library name."
          },
          "vendor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who publishes it. NULL where the corpus records none."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "advertising | analytics | attribution | crash | push | social. NULL means the corpus names this library but we have not classified it — 'unclassified', never 'harmless'. Most of that bucket is a UI toolkit or an HTTP client with nothing to classify."
          },
          "evidence": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Where it was recognised: native (a bundled .so), dex (a package prefix in the app's own code), static (a declared uses-library), action (an intent action), or the manifest element that named it — application, activity, service, receiver, provider. Two entries is stronger evidence than one."
          },
          "categoryLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "English name for `category`. NULL whenever it is."
          }
        }
      },
      "description": "Classified libraries only — advertising, analytics, install attribution, crash reporting, push messaging, social sign-in. Ordered by category, then name."
    },
    "byCategory": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "category",
          "label",
          "count"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Libraries in this category."
          },
          "label": {
            "type": "string",
            "description": "English name."
          },
          "category": {
            "type": "string",
            "description": "Machine name."
          }
        }
      },
      "description": "Counts per category, same order. Empty when the list is."
    },
    "protectors": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Commercial protectors on the build, by product name. NOT a malware signal — anti-piracy and anti-tamper are what they are sold for, Google's PairIP included — but a packed app's real code is not in classes.dex for anyone to read, so a short list beside a non-empty protectors is explained by it."
    },
    "packageName": {
      "type": "string",
      "description": "The package asked for."
    },
    "versionCode": {
      "type": "integer",
      "description": "Its build number."
    },
    "versionName": {
      "type": "string",
      "description": "Version name of that build."
    },
    "codeCoverage": {
      "enum": [
        "none",
        "partial",
        "full"
      ],
      "type": "string",
      "description": "How far the analysis reached into the app's OWN code: none — only the manifest and bundled libraries were read; partial — the code was read but not through; full — read through. Only 'full' makes an empty result mean anything close to 'none'."
    },
    "unclassified": {
      "type": "integer",
      "description": "Recognised libraries we do NOT classify as trackers: HTTP clients, UI toolkits, anything with nothing to classify. Counted rather than listed, so the difference from the SDK inventory's total is visible instead of looking like two endpoints disagreeing. Unclassified is never 'clean'."
    }
  }
}
list_trackers annotations
{
  "title": "List a build's tracking and advertising SDKs",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_permissions — List what a build asks the device for
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Every Android permission this build declares, sensitive ones first, each with a readable label, its capability family and a sentence saying what the app can actually do with it. Defaults to the newest build. This is the same labelling compare_versions diffs, so the two cannot name the same permission differently. THE NULL IS THE PART TO READ: `permissions: null` means the manifest was never read and is NOT a claim that the app asks for nothing — an empty array is that claim. `sensitive` is the count worth quoting; a long list of ordinary permissions is what almost every app looks like. `defines` is separate and often misread: those are permissions this app declares for OTHER apps to hold, not things it is asking for.

list_permissions input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
list_permissions output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId",
    "versionName",
    "versionCode",
    "permissions",
    "byCategory",
    "defines"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "The build this describes — the one asked for, or the newest held."
    },
    "defines": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Permissions this app DEFINES for other apps to hold — a different thing from the ones it requests. Empty when it defines none, and also empty when no manifest was read: permissions:null is how to tell."
    },
    "summary": {
      "type": [
        "string",
        "null"
      ],
      "description": "'24 declared · 5 sensitive'. Null whenever permissions is."
    },
    "declared": {
      "type": [
        "integer",
        "null"
      ],
      "description": "How many. Null when permissions is."
    },
    "sensitive": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Of those, how many are Android-dangerous or special-access. This is the number worth reading."
    },
    "byCategory": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "category",
          "count",
          "sensitive"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Names declared in this family."
          },
          "category": {
            "type": "string",
            "description": "Capability family, e.g. 'Location'. 'Other' for names we cannot file."
          },
          "sensitive": {
            "type": "integer",
            "description": "Of those, how many are sensitive."
          }
        }
      },
      "description": "Counts by capability family, most-declared first, each with how many of its names are sensitive — a family of eleven with none sensitive is a different shape of app from two that are both dangerous."
    },
    "packageName": {
      "type": "string",
      "description": "The package asked for."
    },
    "permissions": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "object",
        "required": [
          "name",
          "label",
          "sensitive"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Raw manifest name, e.g. 'android.permission.CAMERA'."
          },
          "label": {
            "type": "string",
            "description": "Readable label ('Precise location')."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Capability family ('Location'); null for unrecognised names."
          },
          "standard": {
            "type": "boolean",
            "description": "Declared under Android's own namespace, not by the app or an SDK."
          },
          "sensitive": {
            "type": "boolean",
            "description": "Android-dangerous or special-access. These are the ones worth reading."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the app can do with it; null when unrecognised."
          }
        }
      },
      "description": "Sensitive first, then by label. NULL means the manifest was never read — NOT that the app declares none. An empty array is the genuine 'declares none', and the two are opposite answers to the only question this call gets asked."
    },
    "versionCode": {
      "type": "integer",
      "description": "Its build number."
    },
    "versionName": {
      "type": "string",
      "description": "Version name of that build."
    }
  }
}
list_permissions annotations
{
  "title": "List what a build asks the device for",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_manifest — Read what a build declares about itself
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

What the artifact's own AndroidManifest.xml says beyond the permissions it requests — targetSdk, compileSdk, debuggable, cleartext traffic, backup, sharedUserId, install location, uses-feature, uses-library, and the components it exports without a permission guard — plus the native libraries it ships, and our rules read off the pair as `findings`. Defaults to the newest build. `formFactors` lists the device types the build declares (Android TV, Wear OS, desktop and so on), each 'requires' or 'supports'; empty is not a claim that the app won't run on them. A KEY ABSENT INSIDE `manifest` means the manifest is silent about that attribute, never that it declares the platform default; `manifest: null` means nobody read it at all, and an empty `findings` beside that is not an all-clear. `versionMismatch` is the one to escalate: non-null means the file's own version disagrees with what the listing claims, which is what a swapped or relabelled artifact looks like. Use get_raw_scan if you want the stored record without our rules applied.

get_manifest input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_manifest output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId",
    "versionName",
    "versionCode",
    "manifest",
    "formFactors",
    "natives",
    "findings",
    "versionMismatch"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "The build this describes — the one asked for, or the newest held."
    },
    "natives": {
      "type": [
        "object",
        "null"
      ],
      "description": "Native libraries the artifact ships: abis, libs (name, largest uncompressed size, the abis carrying it) and protectors. NULL = the archive directory could not be read; empty lists are the factual 'ships no native libraries', ordinary for a split bundle's base APK."
    },
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "id",
          "severity",
          "title",
          "detail"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable machine-readable id: debuggable | version-mismatch | cleartext-traffic | backup-allowed | shared-user-id | exported-components | legacy-permission-model | packed. Branch on this, not on the prose."
          },
          "title": {
            "type": "string",
            "description": "Short label."
          },
          "detail": {
            "type": "string",
            "description": "One sentence: what it means and what it does not, with the specifics where there are any. Both halves matter — every one of these is ordinary in some app shipping today, and relaying only the first clause turns a fact into an accusation."
          },
          "severity": {
            "enum": [
              "warning",
              "notice"
            ],
            "type": "string",
            "description": "warning — this file is not what a release build of this app should look like (a debuggable build, or a version that disagrees with the listing). notice — true, worth knowing, and routinely a deliberate choice of an ordinary app. Neither is a verdict: the anti-malware verdict is a separate field and a finding never moves it."
          }
        }
      },
      "description": "Our rules read off the two fields above, warnings first. Empty is NOT an all-clear: absence is never a finding, so a manifest nobody read produces none either — manifest:null beside it is how to tell."
    },
    "manifest": {
      "type": [
        "object",
        "null"
      ],
      "description": "What AndroidManifest.xml declares beyond the permissions it requests: targetSdk, compileSdk, debuggable, usesCleartextTraffic, allowBackup, sharedUserId, installLocation, definesPermissions, usesFeatures, usesFeaturesOptional (the subset declared required=\"false\"; absent on older scans), formFactorMarkers (allow-listed meta-data and category names marking a device type; absent on older scans), usesLibraries, exportedUnguarded, and its own versionCode/versionName. NULL = nobody read the manifest. A key ABSENT INSIDE the object means the manifest does not declare that attribute — never that it declares the platform default."
    },
    "formFactors": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "formFactor",
          "support"
        ],
        "properties": {
          "support": {
            "enum": [
              "requires",
              "supports"
            ],
            "type": "string"
          },
          "formFactor": {
            "enum": [
              "automotive",
              "auto",
              "cardboard",
              "daydream",
              "desktop",
              "tv",
              "wear"
            ],
            "type": "string"
          }
        }
      },
      "description": "The device types this build declares, each 'requires' or 'supports'. A declaration rather than a compatibility test. EMPTY does not mean the app won't run on them: it also covers a manifest nobody read or a scan with no usesFeaturesOptional, and a phone build says nothing about a separate TV or watch build."
    },
    "packageName": {
      "type": "string",
      "description": "The package asked for."
    },
    "versionCode": {
      "type": "integer",
      "description": "Its build number."
    },
    "versionName": {
      "type": "string",
      "description": "Version name of that build."
    },
    "versionMismatch": {
      "type": [
        "object",
        "null"
      ],
      "description": "The artifact's own version against what the listing claimed for it, as {claimed, actual}. Non-null means the bytes are not the build the listing describes — a swapped or relabelled artifact — and it is the only check that version number ever gets."
    }
  }
}
get_manifest annotations
{
  "title": "Read what a build declares about itself",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_raw_scan — Read the stored scan record verbatim
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

The whole stored analysis record for one build, exactly as stored and with none of our rules applied — for when you want to judge the facts yourself rather than read our conclusions. It carries two things no other tool publishes: the EVIDENCE DIGEST every SDK match is drawn from (dex package prefixes, manifest component names, intent actions, how much code was read), so you can match it against your own corpus; and each signing certificate's serial and signature algorithm. Also the raw VirusTotal counters and every flagging vendor. Prefer the narrower tools when you want an answer — get_version_security, get_protection, list_trackers, list_permissions, get_manifest — and reach for this when you want the inputs. A build nobody has scanned is `scanned: false`, which is not a clean result.

get_raw_scan input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "A specific build from list_versions. Omit for the newest one."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_raw_scan output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId",
    "versionName",
    "versionCode",
    "scanned",
    "sha256",
    "virusTotal",
    "certs",
    "signature",
    "permissions",
    "manifest",
    "natives",
    "evidence"
  ],
  "properties": {
    "md5": {
      "type": [
        "string",
        "null"
      ],
      "description": "MD5 of the same artifact. An identifier for matching a published IOC, never an integrity check."
    },
    "sha1": {
      "type": [
        "string",
        "null"
      ],
      "description": "SHA-1 of the same artifact, ours where we computed one, else VirusTotal's."
    },
    "apkId": {
      "type": "string",
      "description": "The build this describes — the one asked for, or the newest held."
    },
    "certs": {
      "type": "array",
      "items": {
        "type": "object"
      },
      "description": "Every signer certificate found, with the full X.509 detail — including the serial and the certificate's own signature algorithm, which the projected signer shape does not carry. Empty on scans that predate multi-signature support; read `signature` below for those."
    },
    "pairip": {
      "type": [
        "boolean",
        "null"
      ],
      "description": "Google PairIP detected. NULL = not reported by the engine that scanned this."
    },
    "sha256": {
      "type": "string",
      "description": "The artifact's SHA-256, from the version row — the bytes a download serves."
    },
    "status": {
      "type": [
        "string",
        "null"
      ],
      "description": "Queue state of the record: PENDING | ACTIVE | COMPLETED | FAILED. Only COMPLETED is a finished analysis; FAILED is one that will not finish, and its stored facts are whatever completed before it stopped."
    },
    "natives": {
      "type": [
        "object",
        "null"
      ],
      "description": "The native inventory as stored: abis, libs, protectors."
    },
    "scanned": {
      "type": "boolean",
      "description": "A scan record exists. FALSE and everything below is null or empty — none of which means clean."
    },
    "verdict": {
      "type": [
        "string",
        "null"
      ],
      "description": "Our classification, as stored: verified | secure | caution | dangerous | unverified | pending."
    },
    "evidence": {
      "type": [
        "object",
        "null"
      ],
      "description": "What the scan SAW, with no judgement in it: dex package prefixes truncated at `depth`, manifest component class names, intent actions, how much code was read, and which tiers were capped. Every SDK match on this surface is drawn from this — published so you can run your own corpus against it instead of ours. NULL = scanned before the digest existed, or unreadable."
    },
    "manifest": {
      "type": [
        "object",
        "null"
      ],
      "description": "The manifest facts as stored. Same object the manifest endpoint publishes."
    },
    "scannedAt": {
      "type": [
        "string",
        "null"
      ],
      "description": "ISO 8601."
    },
    "signature": {
      "type": "object",
      "required": [
        "scheme",
        "status",
        "certSha256",
        "certSubject",
        "certMatch"
      ],
      "properties": {
        "scheme": {
          "type": [
            "string",
            "null"
          ],
          "description": "'v2' | 'v3' | 'v3.1'."
        },
        "status": {
          "type": [
            "string",
            "null"
          ],
          "description": "How signature extraction ended."
        },
        "certMatch": {
          "type": [
            "string",
            "null"
          ],
          "description": "'trusted' | 'known' | 'first-seen' | 'mismatch'."
        },
        "certSha256": {
          "type": [
            "string",
            "null"
          ],
          "description": "Primary certificate fingerprint."
        },
        "certSubject": {
          "type": [
            "string",
            "null"
          ],
          "description": "Its subject."
        }
      },
      "description": "The legacy single-signer columns, kept because scans predating multi-signature support have these and an empty `certs`."
    },
    "virusTotal": {
      "type": "object",
      "required": [
        "found",
        "flagged",
        "meta"
      ],
      "properties": {
        "meta": {
          "type": [
            "object",
            "null"
          ],
          "description": "firstSeen, timesSubmitted, reputation, threatLabel, threatCategories, yaraRules, sandboxVerdicts. Null for a verdict that came from a fresh upload — that path polls an analysis object, which has no submission history."
        },
        "found": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "VirusTotal had a report for this hash."
        },
        "total": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Vendors that ran."
        },
        "flagged": {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "vendor",
              "category"
            ],
            "properties": {
              "result": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The vendor's detection label."
              },
              "vendor": {
                "type": "string",
                "description": "Vendor name."
              },
              "category": {
                "type": "string",
                "description": "'malicious' | 'suspicious'."
              }
            }
          },
          "description": "Each flagging vendor, its category and its own label for the detection."
        },
        "harmless": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Vendors calling it harmless."
        },
        "scanDate": {
          "type": [
            "string",
            "null"
          ],
          "description": "ISO 8601."
        },
        "malicious": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Vendors calling it malicious."
        },
        "suspicious": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Vendors calling it suspicious."
        },
        "undetected": {
          "type": [
            "integer",
            "null"
          ],
          "description": "Vendors with no detection."
        }
      },
      "description": "The counters verbatim, every vendor that flagged the file, and the submission context. found:false means VirusTotal had no report for this hash, which is not a clean result."
    },
    "packageName": {
      "type": "string",
      "description": "The package asked for."
    },
    "permissions": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "string"
      },
      "description": "Raw manifest permission names, unlabelled and in the manifest's own order. NULL = the manifest was never read; [] = it declares none."
    },
    "versionCode": {
      "type": "integer",
      "description": "Its build number."
    },
    "versionName": {
      "type": "string",
      "description": "Version name of that build."
    },
    "evidenceVersion": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Which digest schema produced `evidence`."
    }
  }
}
get_raw_scan annotations
{
  "title": "Read the stored scan record verbatim",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
lookup_hash — Identify a file by its checksum
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Which app and which build a file checksum names. Use this when you have a hash and no package name — an APK pulled off a device, a sample from a threat feed, a digest in an incident report. Takes an MD5 (32 hex), SHA-1 (40) or SHA-256 (64) and works out which from the length, so do not try to label it; colons, spaces and upper case are fine and are normalised. A miss is a real answer, not an error, and it is free when nothing matches (`found: false`): we hold no build with that checksum, and says nothing about whether the file exists elsewhere. READ `note`: only SHA-256 is carried by every stored build, so a SHA-1 or MD5 miss can mean we hold the file and never computed that digest for it — re-check with the SHA-256 if you have it. Once you have the packageName and apkId, the per-build tools will tell you what the file is.

lookup_hash input schema
{
  "type": "object",
  "required": [
    "hash"
  ],
  "properties": {
    "hash": {
      "type": "string",
      "description": "An MD5, SHA-1 or SHA-256 hex digest of the file. The algorithm is taken from its length."
    }
  },
  "additionalProperties": false
}
lookup_hash output schema
{
  "type": "object",
  "required": [
    "hash",
    "algorithm",
    "found",
    "match",
    "note"
  ],
  "properties": {
    "hash": {
      "type": "string",
      "description": "The hash as matched: lowercase hex, separators stripped."
    },
    "note": {
      "type": "string",
      "description": "What a miss does and does not prove for THIS algorithm. Only SHA-256 is carried by every stored build, so a SHA-1 or MD5 miss can mean we hold the file and never computed that digest for it."
    },
    "found": {
      "type": "boolean",
      "description": "We hold a build with this checksum. FALSE is a complete answer and a 200 — it says nothing about whether the file exists anywhere else."
    },
    "match": {
      "type": [
        "object",
        "null"
      ],
      "description": "The build, when found: packageName, appName, apkId (the id every per-build call takes), versionName, versionCode, arch, format, fileSize in bytes, sha256, isLatest and uploadedAt."
    },
    "algorithm": {
      "enum": [
        "md5",
        "sha1",
        "sha256"
      ],
      "type": "string",
      "description": "Inferred from the value's own length. Not a parameter you send."
    }
  }
}
lookup_hash annotations
{
  "title": "Identify a file by its checksum",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
lookup_certificate — Find what a signing key signs
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Every package we hold that one signing certificate is on record for. Android only installs an update 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 you found on one repackaged app is worth asking about, because it has usually signed others. Takes the SHA-256 fingerprint of the DER certificate (64 hex, colons and case fine). A SHA-1 IS REFUSED, free, with the reason: the record stores one fingerprint per certificate and a SHA-1 cannot be resolved to it — get the SHA-256 (keytool prints both). BOUNDED, and `note` says how: it covers packages we have ingested and scanned, never every app the key has signed, so it is free when we hold nothing signed by the key (`total: 0`), which is not to say the key signs nothing; charged even when every package the key signed has been removed (an empty `items`). `blocked: true` means the key is on our catalogue-wide blocklist and nothing it signed can be downloaded here. Taken-down packages are counted in `blockedCount` and not listed, so a page may be shorter than `perPage`.

lookup_certificate input schema
{
  "type": "object",
  "required": [
    "fingerprint"
  ],
  "properties": {
    "per": {
      "type": "integer",
      "maximum": 96,
      "minimum": 1,
      "description": "Rows per page, 1–96. Defaults to 24."
    },
    "page": {
      "type": "integer",
      "minimum": 1,
      "description": "1-based page. Defaults to 1."
    },
    "fingerprint": {
      "type": "string",
      "description": "SHA-256 fingerprint of the signing certificate, 64 hex characters, with or without colons."
    }
  },
  "additionalProperties": false
}
lookup_certificate output schema
{
  "type": "object",
  "required": [
    "fingerprint",
    "algorithm",
    "subject",
    "blocked",
    "blockedCount",
    "items",
    "page",
    "perPage",
    "total",
    "totalPages",
    "note"
  ],
  "properties": {
    "note": {
      "type": "string",
      "description": "What this answer is drawn from and therefore what it does not cover: the signing record we keep per package, so it covers packages we hold files for and have scanned — never every app the key has signed. A page can also be short, because withheld packages are removed after paging."
    },
    "page": {
      "type": "integer",
      "description": "1-based page number of this result."
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "appName",
          "firstSeenAt",
          "trusted",
          "addedVia"
        ],
        "properties": {
          "appName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Its title. Null where we hold a signing record for a package whose catalogue row is gone."
          },
          "trusted": {
            "type": "boolean",
            "description": "A moderator vouched for this key on this package, above plain first use."
          },
          "addedVia": {
            "type": "string",
            "description": "How the record was made: 'scan' (trust on first use), 'google-play' or 'admin'."
          },
          "firstSeenAt": {
            "type": "string",
            "description": "When this key was first recorded against this package, ISO 8601."
          },
          "packageName": {
            "type": "string",
            "description": "Android package id."
          }
        }
      },
      "description": "Newest record first."
    },
    "total": {
      "type": "integer",
      "description": "Total matching rows across all pages."
    },
    "blocked": {
      "type": "boolean",
      "description": "This key is on the catalogue-wide signature blocklist: a download of anything it signed is refused whatever the anti-malware result said."
    },
    "perPage": {
      "type": "integer",
      "description": "Rows per page."
    },
    "subject": {
      "type": [
        "string",
        "null"
      ],
      "description": "Subject as recorded, e.g. 'CN=Example, O=Example Ltd'."
    },
    "algorithm": {
      "enum": [
        "sha256"
      ],
      "type": "string",
      "description": "Always sha256. The per-package signing record stores that one fingerprint, and a SHA-1 cannot be resolved to it — sending one is refused with that reason, free."
    },
    "totalPages": {
      "type": "integer",
      "description": "Number of pages available."
    },
    "fingerprint": {
      "type": "string",
      "description": "The fingerprint as matched: lowercase hex, no separators."
    },
    "blockedCount": {
      "type": "integer",
      "description": "Packages this key is on record for that are taken down. Counted across the whole key, not this page, and withheld from `items` — a lookup is not a way to read back what has been removed."
    }
  }
}
lookup_certificate annotations
{
  "title": "Find what a signing key signs",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
list_developer_certificates — List a publisher's signing keys
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Every signing key on record for one publisher's apps — lookup_certificate read the other way round. Each key says how many of the publisher's apps it signs and, in `elsewhere`, how many packages OUTSIDE the publisher it is also on: zero is ordinary, above zero is a key shared across the publisher's accounts or somebody else's builds signed with it, and lookup_certificate on that fingerprint lists which. All keys in one answer, no pages. BOUNDED, and `note` says how: it covers the publisher's apps we have ingested and scanned. Free when we hold no signing record for any of the publisher's apps (`total: 0`). The slug comes from `developerSlug` on any app row.

list_developer_certificates input schema
{
  "type": "object",
  "required": [
    "slug"
  ],
  "properties": {
    "slug": {
      "type": "string",
      "description": "Publisher slug, from an app's developerSlug field."
    }
  },
  "additionalProperties": false
}
list_developer_certificates output schema
{
  "type": "object",
  "required": [
    "developer",
    "total",
    "items",
    "note"
  ],
  "properties": {
    "note": {
      "type": "string",
      "description": "What this answer is drawn from and therefore what it does not cover: the signing record we keep per package, so it covers this publisher's apps we hold files for and have scanned, and `elsewhere` counts only packages we hold."
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "certSha256",
          "subject",
          "packages",
          "elsewhere",
          "firstSeenAt",
          "trusted",
          "blocked"
        ],
        "properties": {
          "blocked": {
            "type": "boolean",
            "description": "The key is on the catalogue-wide signature blocklist."
          },
          "subject": {
            "type": [
              "string",
              "null"
            ],
            "description": "Subject as recorded, e.g. 'CN=Example, O=Example Ltd'."
          },
          "trusted": {
            "type": "boolean",
            "description": "A moderator vouched for this key on at least one of the apps."
          },
          "packages": {
            "type": "integer",
            "description": "How many of this publisher's listed apps the key is on record for."
          },
          "elsewhere": {
            "type": "integer",
            "description": "How many packages OUTSIDE this publisher the key is also on record for. Zero is the ordinary case; above zero is a key shared across publisher accounts, or somebody else's builds signed with it — the certificate lookup lists them."
          },
          "certSha256": {
            "type": "string",
            "description": "SHA-256 fingerprint, lowercase hex with no separators — what /lookup/certificate/{fingerprint} takes."
          },
          "firstSeenAt": {
            "type": "string",
            "description": "When the key was first recorded against any of this publisher's apps, ISO 8601."
          }
        }
      },
      "description": "The key on the most apps first."
    },
    "total": {
      "type": "integer",
      "description": "Keys in `items`. Every one — not paged; a publisher has a handful."
    },
    "developer": {
      "type": "object",
      "required": [
        "slug",
        "name"
      ],
      "properties": {
        "name": {
          "type": "string",
          "description": "Publisher name."
        },
        "slug": {
          "type": "string",
          "description": "The publisher's slug."
        }
      }
    }
  }
}
list_developer_certificates annotations
{
  "title": "List a publisher's signing keys",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_signing_history — Trace the signing key
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Which certificate signed which releases of a package, and where the key moved. Android will only install an update signed by the key that signed what is already on the device, so a `hasKeyReplacement: true` means the newer build is not an update of the older one — it is a different app under the same package name, which is what a repackaged app looks like. `unreadVersions` lists releases whose signature we never read; those are gaps, not agreement.

get_signing_history input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
get_signing_history output schema
{
  "type": "object",
  "required": [
    "packageName",
    "summary",
    "hasKeyReplacement",
    "eras",
    "unreadVersions"
  ],
  "properties": {
    "eras": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "certShas",
          "versions"
        ],
        "properties": {
          "certShas": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The signer set."
          },
          "lastSeen": {
            "type": "string",
            "description": "Newest upload in the era, ISO 8601."
          },
          "versions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Version names in this era, newest first."
          },
          "firstSeen": {
            "type": "string",
            "description": "Oldest upload in the era, ISO 8601."
          },
          "transition": {
            "type": [
              "string",
              "null"
            ],
            "description": "How this era's keys relate to the era BEFORE it: 'same', 'added', 'narrowed', 'changed' or 'replaced'. Null on the oldest era. 'replaced' — no key in common — is the one that matters."
          }
        }
      },
      "description": "Newest era first."
    },
    "signers": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "sha256": {
            "type": "string"
          },
          "subject": {
            "type": [
              "string",
              "null"
            ]
          },
          "trusted": {
            "type": "boolean",
            "description": "Escalated above plain first-use by a moderator."
          },
          "addedVia": {
            "type": "string",
            "description": "'scan' (trust on first use), 'google-play' or 'admin'."
          },
          "inArchive": {
            "type": "boolean",
            "description": "This certificate signs a release we actually hold."
          },
          "firstSeenAt": {
            "type": "string",
            "description": "ISO 8601."
          }
        }
      },
      "description": "Certificates on record for the package."
    },
    "summary": {
      "type": "string",
      "description": "One sentence naming what the timeline shows."
    },
    "packageName": {
      "type": "string",
      "description": "The package this timeline covers."
    },
    "unreadVersions": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Releases whose signing block was never read. A gap in coverage, published as a gap so it is not mistaken for agreement."
    },
    "hasKeyReplacement": {
      "type": "boolean",
      "description": "At least one era shares no key with the era before it. Android will not install such a build over the existing app, and a key that changed without a rotation is the strongest repackaging signal there is."
    }
  }
}
get_signing_history annotations
{
  "title": "Trace the signing key",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
compare_versions — Compare two builds
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

What changed between two releases: permissions gained and lost, whether the signing key moved, size and minimum Android. Name either side by version name or by version code — the code is tried first, because a version name is free text the developer sets and is often a placeholder. Order does not matter; the higher build number is treated as the newer. Check `permissionsKnown` before reading two empty permission lists as 'nothing changed'.

compare_versions input schema
{
  "type": "object",
  "required": [
    "packageName",
    "from",
    "to"
  ],
  "properties": {
    "to": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "The other release, by version name or version code."
    },
    "from": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "One release, by version name or version code."
    },
    "packageName": {
      "type": "string",
      "description": "Android package id."
    }
  },
  "additionalProperties": false
}
compare_versions output schema
{
  "type": "object",
  "required": [
    "packageName",
    "version",
    "previousVersion",
    "permissionsKnown",
    "noteworthy"
  ],
  "properties": {
    "size": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "arch": {
          "type": "string"
        },
        "bytes": {
          "type": "integer"
        }
      },
      "description": "Byte delta for one arch present in both releases. Null when they share none — comparing across differing split sets would report a change that is only a change in what we store."
    },
    "minSdk": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "to": {
          "type": "integer"
        },
        "from": {
          "type": "integer"
        }
      },
      "description": "Minimum Android API level move, when both sides are known and differ."
    },
    "version": {
      "type": "string",
      "description": "The newer of the two, whichever order they were asked for in."
    },
    "noteworthy": {
      "type": "boolean",
      "description": "Worth a human's attention: a permission was gained, or the signer moved."
    },
    "newCertShas": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Certificates signing the newer release but not the older one."
    },
    "packageName": {
      "type": "string",
      "description": "The package compared."
    },
    "signerChange": {
      "type": [
        "string",
        "null"
      ],
      "description": "'same', 'added', 'narrowed', 'changed' or 'replaced'; null when either side's signature was never extracted."
    },
    "previousVersion": {
      "type": "string",
      "description": "The older of the two."
    },
    "permissionsAdded": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name",
          "label",
          "sensitive"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Raw manifest name, e.g. 'android.permission.CAMERA'."
          },
          "label": {
            "type": "string",
            "description": "Readable label ('Precise location')."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Capability family ('Location'); null for unrecognised names."
          },
          "standard": {
            "type": "boolean",
            "description": "Declared under Android's own namespace, not by the app or an SDK."
          },
          "sensitive": {
            "type": "boolean",
            "description": "Android-dangerous or special-access. These are the ones worth reading."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the app can do with it; null when unrecognised."
          }
        }
      }
    },
    "permissionsKnown": {
      "type": "boolean",
      "description": "Both releases had their manifests read. When FALSE the permission arrays are empty because we could not look, not because nothing moved."
    },
    "signerChangeLabel": {
      "type": [
        "string",
        "null"
      ],
      "description": "Short reading of `signerChange`."
    },
    "permissionsRemoved": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name",
          "label",
          "sensitive"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Raw manifest name, e.g. 'android.permission.CAMERA'."
          },
          "label": {
            "type": "string",
            "description": "Readable label ('Precise location')."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Capability family ('Location'); null for unrecognised names."
          },
          "standard": {
            "type": "boolean",
            "description": "Declared under Android's own namespace, not by the app or an SDK."
          },
          "sensitive": {
            "type": "boolean",
            "description": "Android-dangerous or special-access. These are the ones worth reading."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the app can do with it; null when unrecognised."
          }
        }
      }
    },
    "signerChangeDetail": {
      "type": [
        "string",
        "null"
      ],
      "description": "What it means for whoever installs the build."
    }
  }
}
compare_versions annotations
{
  "title": "Compare two builds",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_apps — Look up several apps
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Summary rows for up to 100 package ids in one call — use this instead of calling get_app in a loop. Costs 1 credit PER PACKAGE, so a hundred ids cost a hundred credits however they arrive. Ids we do not hold come back in `notFound` rather than failing the call. Returns the SUMMARY row, not the full record: it is cheaper than get_app (5) because it answers less, so reach for get_app only when you need the description or the screenshots for a particular one.

get_apps input schema
{
  "type": "object",
  "required": [
    "packageNames"
  ],
  "properties": {
    "packageNames": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 100,
      "minItems": 1,
      "description": "Android package ids, 1–100. Charged per id."
    }
  },
  "additionalProperties": false
}
get_apps output schema
{
  "type": "object",
  "required": [
    "items",
    "notFound"
  ],
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "packageName",
          "name",
          "developer",
          "type",
          "availability",
          "hasFiles"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "App title."
          },
          "type": {
            "enum": [
              "game",
              "app"
            ],
            "type": "string",
            "description": "Game or app."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "description": "Play star rating, 0–5. Null when the app is not on Google Play, or when Play gave it no rating."
          },
          "iconUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store artwork URL."
          },
          "ratings": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Vote count behind `rating`. Null on rows seeded from a list scrape."
          },
          "version": {
            "type": "string",
            "description": "Latest version NAME known ('2.24.9'). May be 'Varies with device'."
          },
          "hasFiles": {
            "type": "boolean",
            "description": "We hold at least one downloadable file. Check before spending a download."
          },
          "installs": {
            "type": [
              "string",
              "null"
            ],
            "description": "Google Play's install BUCKET, verbatim ('10,000,000+'). A string, not a number — Play publishes no exact count. Null when the app is not on Google Play."
          },
          "developer": {
            "type": "string",
            "description": "Publisher name as scraped."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When Play last published an update, ISO 8601."
          },
          "categoryId": {
            "type": "string",
            "description": "Category id, as listed by the categories call."
          },
          "packageName": {
            "type": "string",
            "description": "Android package id — the identifier every app call takes."
          },
          "availability": {
            "enum": [
              "available",
              "unavailable"
            ],
            "type": "string",
            "description": "'unavailable' means a download call will be refused — a takedown, a paid app we cannot obtain, or a listing under the operator's rating bar."
          },
          "categoryName": {
            "type": "string",
            "description": "Human-readable category."
          },
          "onGooglePlay": {
            "type": "boolean",
            "description": "False for an app its developer published on this site directly, with no Google Play listing: the Play fields are then null and the Play listing call answers 404."
          },
          "developerSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Key for the developer lookup. Null when the row has no publisher."
          }
        }
      }
    },
    "notFound": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Ids with no row in the catalogue."
    }
  }
}
get_apps annotations
{
  "title": "Look up several apps",
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true
}
get_account — Check credits
Advertised by the server during the last successful anonymous check.
Advertised read-only

How many credits are left on the key you are using, and what that key is. Free. Every successful call also reports this in `_meta`, so reach for this one when you need to know before you spend — a download costs 10 — or when your calls have been failing and you have not seen a balance in a while.

get_account input schema
{
  "type": "object",
  "additionalProperties": false
}
get_account output schema
{
  "type": "object",
  "required": [
    "keyId",
    "credits",
    "memberSince"
  ],
  "properties": {
    "keyId": {
      "type": "string",
      "description": "Id of the credential that made this call, as `<kind>:<id>` — `key:` for an API key, `oauth:` for an MCP client's OAuth grant. Treat it as an opaque string: it identifies the credential, not the account, and further kinds may appear."
    },
    "credits": {
      "type": "integer",
      "description": "Credits left on the account this key bills to, after this call."
    },
    "memberSince": {
      "type": "string",
      "format": "date-time",
      "description": "When the account was created."
    }
  }
}
get_account annotations
{
  "title": "Check credits",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false
}
console_list_apps — List my apps (Developers Console)
Advertised by the server during the last successful anonymous check.
Advertised read-only

The apps you manage in the APKCube Developers Console — every app of every developer whose team you are on, 50 a page, with your role on each. Only works for a signed-in user (OAuth) on a verified developer's team. Free.

console_list_apps input schema
{
  "type": "object",
  "properties": {
    "page": {
      "type": "integer",
      "description": "1-based page."
    }
  },
  "additionalProperties": false
}
console_list_apps output schema
{
  "type": "object",
  "required": [
    "items",
    "total"
  ],
  "properties": {
    "page": {
      "type": "integer",
      "description": "This page."
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object"
      }
    },
    "total": {
      "type": "integer",
      "description": "Apps across all your teams."
    },
    "perPage": {
      "type": "integer",
      "description": "Page size."
    }
  }
}
console_list_apps annotations
{
  "title": "List my apps (Developers Console)",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false
}
console_app_stats — Download stats for my app
Advertised by the server during the last successful anonymous check.
Advertised read-only

Downloads of one of your apps: per UTC day for the last 30 days (with the 30 before for comparison), the lifetime total, and the top countries of the last 7 days. Counts come from our own download log, not Google Play. Free.

console_app_stats input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Package id of an app on your Developers Console team, e.g. com.example.app."
    }
  },
  "additionalProperties": false
}
console_app_stats output schema
{
  "type": "object",
  "required": [
    "packageName",
    "last30",
    "lifetime"
  ],
  "properties": {
    "last30": {
      "type": "integer",
      "description": "Downloads in the last 30 days."
    },
    "perDay": {
      "type": "array",
      "items": {
        "type": "object"
      }
    },
    "lifetime": {
      "type": "integer",
      "description": "All-time downloads."
    },
    "countries": {
      "type": "object"
    },
    "previous30": {
      "type": "integer",
      "description": "Downloads in the 30 days before that."
    },
    "packageName": {
      "type": "string",
      "description": "The app."
    }
  }
}
console_app_stats annotations
{
  "title": "Download stats for my app",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false
}
console_list_uploads — List my uploaded builds
Advertised by the server during the last successful anonymous check.
Advertised read-only

Builds submitted for your apps from the console or the Publishing API, newest first, with each one's status (uploading, verifying, scanning, held, published, rejected …) and, for a held or rejected build, the reason and its stable code. Pass packageName for one app. Free.

console_list_uploads input schema
{
  "type": "object",
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Optional: only this app's uploads."
    }
  },
  "additionalProperties": false
}
console_list_uploads output schema
{
  "type": "object",
  "required": [
    "items"
  ],
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object"
      }
    },
    "total": {
      "type": "integer",
      "description": "Uploads matched."
    }
  }
}
console_list_uploads annotations
{
  "title": "List my uploaded builds",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false
}
console_upload_status — Check one uploaded build
Advertised by the server during the last successful anonymous check.
Advertised read-only

Where one upload stands, with the scan summary once it has one (VirusTotal counts, signing certificates and how each compares with the keys on record). Poll every 15–30 seconds while status is verifying, scanning or publishing; `held` waits for a person and can take days. Free.

console_upload_status input schema
{
  "type": "object",
  "required": [
    "uploadId"
  ],
  "properties": {
    "uploadId": {
      "type": "integer",
      "description": "The upload's id, from console_list_uploads."
    }
  },
  "additionalProperties": false
}
console_upload_status output schema
{
  "type": "object",
  "required": [
    "upload"
  ],
  "properties": {
    "scan": {
      "type": [
        "object",
        "null"
      ]
    },
    "upload": {
      "type": "object"
    }
  }
}
console_upload_status annotations
{
  "title": "Check one uploaded build",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false
}
console_security_report — Security report for my app
Advertised by the server during the last successful anonymous check.
Advertised read-only

Every build we store for one of your apps with our scan verdict, VirusTotal counts, signature scheme, how its signing key compares with the keys on record, and any notice you have put on it — plus a one-line verdict on the signing history. `verdict: null` means not scanned, which is not the same as clean. The `apkId` here is what console_set_version_notice takes. Free.

console_security_report input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "packageName": {
      "type": "string",
      "description": "Package id of an app on your Developers Console team, e.g. com.example.app."
    }
  },
  "additionalProperties": false
}
console_security_report output schema
{
  "type": "object",
  "required": [
    "packageName",
    "versions"
  ],
  "properties": {
    "signing": {
      "type": [
        "object",
        "null"
      ]
    },
    "versions": {
      "type": "array",
      "items": {
        "type": "object"
      }
    },
    "packageName": {
      "type": "string",
      "description": "The app."
    }
  }
}
console_security_report annotations
{
  "title": "Security report for my app",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false
}
console_update_listing — Edit my app's listing text
Advertised by the server during the last successful anonymous check.
Advertised destructive

Change the text visitors read on your app's page: the short description (≤80 characters), full description (≤4000, plain text), what's new (≤500) and a YouTube link. Omit a field to keep it; pass null or an empty string to show Google Play's text for it. Goes live immediately; our moderators review afterwards. Needs the admin or owner role. Name and icon cannot be changed.

console_update_listing input schema
{
  "type": "object",
  "required": [
    "packageName"
  ],
  "properties": {
    "summary": {
      "type": "string",
      "description": "Short description, up to 80 characters."
    },
    "videoUrl": {
      "type": "string",
      "description": "A youtube.com/watch?v= or youtu.be link."
    },
    "description": {
      "type": "string",
      "description": "Full description, plain text, up to 4000 characters."
    },
    "packageName": {
      "type": "string",
      "description": "Package id of an app on your Developers Console team, e.g. com.example.app."
    },
    "recentChanges": {
      "type": "string",
      "description": "What's new in the current release, up to 500 characters."
    }
  },
  "additionalProperties": false
}
console_update_listing output schema
{
  "type": "object",
  "required": [
    "packageName",
    "listing"
  ],
  "properties": {
    "listing": {
      "type": "object"
    },
    "packageName": {
      "type": "string",
      "description": "The app."
    }
  }
}
console_update_listing annotations
{
  "title": "Edit my app's listing text",
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
console_set_version_notice — Pull a release of my app
Advertised by the server during the last successful anonymous check.
Advertised destructive

Put a warning on one stored build of your app — `vulnerable` or `withdrawn`, a message of up to 300 characters, and optionally the version code to use instead — or lift it (`lift: true`). With `blockDownload: true` downloads of that build are refused. Applies immediately and is reversible; it does not delete anything. Take `apkId` from console_security_report. Needs the uploader role or above.

console_set_version_notice input schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId"
  ],
  "properties": {
    "kind": {
      "enum": [
        "vulnerable",
        "withdrawn"
      ],
      "type": "string",
      "description": "Why the build is pulled."
    },
    "lift": {
      "type": "boolean",
      "description": "True removes the notice; the fields below are ignored."
    },
    "apkId": {
      "type": "string",
      "description": "The stored build's apkId, from console_security_report."
    },
    "message": {
      "type": "string",
      "description": "Shown to visitors, up to 300 characters."
    },
    "packageName": {
      "type": "string",
      "description": "Package id of an app on your Developers Console team, e.g. com.example.app."
    },
    "blockDownload": {
      "type": "boolean",
      "description": "Also refuse downloads of this build."
    },
    "recommendedVersionCode": {
      "type": "integer",
      "description": "Optional: the version code visitors should take instead."
    }
  },
  "additionalProperties": false
}
console_set_version_notice output schema
{
  "type": "object",
  "required": [
    "packageName",
    "apkId"
  ],
  "properties": {
    "apkId": {
      "type": "string",
      "description": "The build."
    },
    "lifted": {
      "type": "boolean"
    },
    "notice": {
      "type": [
        "object",
        "null"
      ]
    },
    "packageName": {
      "type": "string",
      "description": "The app."
    }
  }
}
console_set_version_notice annotations
{
  "title": "Pull a release of my app",
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
console_publish_upload — Publish a build that is ready
Advertised by the server during the last successful anonymous check.
Advertised destructive

Publish one of your uploads that passed its checks and is waiting as `ready` — because it was uploaded with a manual release, or is scheduled and you want it out now. It becomes downloadable at once. Refused unless the upload's status is `ready`. Needs the uploader role or above.

console_publish_upload input schema
{
  "type": "object",
  "required": [
    "uploadId"
  ],
  "properties": {
    "uploadId": {
      "type": "integer",
      "description": "The upload's id, from console_list_uploads."
    }
  },
  "additionalProperties": false
}
console_publish_upload output schema
{
  "type": "object",
  "required": [
    "upload"
  ],
  "properties": {
    "upload": {
      "type": "object"
    },
    "apkVersionId": {
      "type": "integer",
      "description": "The published build."
    }
  }
}
console_publish_upload annotations
{
  "title": "Publish a build that is ready",
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
console_set_upload_release — Change when an upload goes live
Advertised by the server during the last successful anonymous check.
Advertised destructive

For one of your uploads that is not yet published: set its release to `auto` (publish when it passes), `manual` (wait for console_publish_upload) or `scheduled` with `releaseAt` (an ISO-8601 time with its offset, at least 10 minutes ahead). `manual` also cancels a schedule. Can instead (or as well) set `noteToModerators`, up to 1000 characters, which only our reviewers read if the build is held. A ready build cannot be set to `auto`; publish it instead. Needs the uploader role or above.

console_set_upload_release input schema
{
  "type": "object",
  "required": [
    "uploadId"
  ],
  "properties": {
    "uploadId": {
      "type": "integer",
      "description": "The upload's id, from console_list_uploads."
    },
    "releaseAt": {
      "type": "string",
      "description": "For `scheduled`: an ISO-8601 time with offset, e.g. 2026-11-20T09:00:00Z."
    },
    "releaseMode": {
      "enum": [
        "auto",
        "manual",
        "scheduled"
      ],
      "type": "string",
      "description": "When it goes live once it passes."
    },
    "noteToModerators": {
      "type": "string",
      "description": "Read only by our reviewers if the build is held; null clears it."
    }
  },
  "additionalProperties": false
}
console_set_upload_release output schema
{
  "type": "object",
  "required": [
    "upload"
  ],
  "properties": {
    "upload": {
      "type": "object"
    }
  }
}
console_set_upload_release annotations
{
  "title": "Change when an upload goes live",
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}

Prompts

8 advertised

vet_app — Vet an app before installing

Advertised by the server during the last successful anonymous check.

Decide whether a package is safe to install and explain why, using the listing, scan verdict, permissions and signing key.

packageName
Android package id, e.g. com.whatsapp. (required)
find_and_download — Find an app and get the APK

Advertised by the server during the last successful anonymous check.

Find an app by name, pick the right build and mint a download URL, checking first so you don't pay for the wrong file.

query
App name, publisher, or package id. (required)
requirements
Optional constraints, such as architecture, a minimum Android version or a specific release.
whats_changed — Compare two releases

Advertised by the server during the last successful anonymous check.

Compare two builds of a package and report new permissions and any change of signing key.

packageName
Android package id. (required)
from
One release, by version name or version code. Omit both to use the two newest; give one alone to compare it against the newest.
to
The other release.
audit_sdks — Audit an app's third-party SDKs

Advertised by the server during the last successful anonymous check.

List the third-party code inside an app (ads, analytics, attribution, crash reporting) and say how much of the build we could actually read.

packageName
Android package id, e.g. com.whatsapp. (required)
trace_artifact — Trace a file back to its family

Advertised by the server during the last successful anonymous check.

Start from a checksum or a signing-key fingerprint and find out what it is, who signed it, and what else that key signs.

artifact
A file checksum (MD5, SHA-1 or SHA-256) or a SHA-256 signing-certificate fingerprint. (required)
privacy_report — Report what an app collects

Advertised by the server during the last successful anonymous check.

Say what an app can reach and who it reports to: its trackers, its sensitive permissions and the manifest settings that expose data, with how much of the build we could read.

packageName
Android package id, e.g. com.whatsapp. (required)
compare_apps — Compare two apps on privacy

Advertised by the server during the last successful anonymous check.

Put two apps side by side — rating, installs, trackers and sensitive permissions — so you can choose between alternatives.

a
First Android package id. (required)
b
Second Android package id. (required)
vet_developer — Check a publisher's signing keys

Advertised by the server during the last successful anonymous check.

Check whether a publisher signs its apps consistently, and whether any of its keys also signs somebody else's packages.

slug
Publisher slug, from developerSlug on any app row. (required)

Resources

Not advertised

No resource definitions available.

90-day uptime

A lightweight connection check about every six hours, sampled on hosts with many listings. It shows the server answered, not that its tools work.

Too few checks for a percentage yet (3 of 3 answered).

  1. 2026-11-07: 2 of 2 checks answered
  2. 2026-11-09: 1 of 1 checks answered

30-day availability

Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.

Successful checks
1 of 1 (100%)
Average latency
698 ms
95th percentile latency
698 ms

Maintenance history

Public check and source-refresh events. Editorial and ownership records remain private.

  • · probe · passed · 698 ms · metadata changed · 38 tool(s), 8 prompt(s)

Connect from your client

Each snippet registers the same endpoint; pick the client you use.

This server is operated by the named third party. Listing and endpoint checks are not an endorsement or a security audit. Review the operator's permissions, terms, and data handling before connecting. How listings are checked and corrected.

Claude Code

Run this once in a terminal. It registers the remote server. Use --scope project to write .mcp.json for the repo.
Claude Code configuration
claude mcp add --transport http apkcube-mcp https://apkcube.com/api/mcp

Codex

Run this once. Codex stores MCP servers in ~/.codex/config.toml (or .codex/config.toml in a trusted project).
Codex configuration
codex mcp add apkcube-mcp --url https://apkcube.com/api/mcp

Cursor

Merge this server into .cursor/mcp.json in your project, then enable apkcube-mcp in your MCP settings.
Cursor configuration
{
  "mcpServers": {
    "apkcube-mcp": {
      "url": "https://apkcube.com/api/mcp"
    }
  }
}

Cline

Merge this into cline_mcp_settings.json (Cline → MCP Servers → Configure).
Cline configuration
{
  "mcpServers": {
    "apkcube-mcp": {
      "type": "streamableHttp",
      "url": "https://apkcube.com/api/mcp",
      "disabled": false
    }
  }
}

VS Code

Merge this server into .vscode/mcp.json. Use MCP: List Servers to start apkcube-mcp and inspect its tools.
VS Code configuration
{
  "servers": {
    "apkcube-mcp": {
      "type": "http",
      "url": "https://apkcube.com/api/mcp"
    }
  }
}

Gemini CLI

Run this once. It writes .gemini/settings.json in the current project; pass --scope user for ~/.gemini/settings.json.
Gemini CLI configuration
gemini mcp add --transport http apkcube-mcp https://apkcube.com/api/mcp

Claude Desktop

Open Claude Settings > Connectors > Add custom connector, enter this server URL and enable it for the conversation. This is a manual remote-connector handoff, not a desktop JSON configuration.
Claude Desktop configuration
https://apkcube.com/api/mcp

Windsurf

Merge this into ~/.codeium/windsurf/mcp_config.json, then refresh MCP in Cascade. Windsurf remote servers use serverUrl, not url.
Windsurf configuration
{
  "mcpServers": {
    "apkcube-mcp": {
      "serverUrl": "https://apkcube.com/api/mcp"
    }
  }
}

Let your agent find its tools

Find servers like APKCube MCP from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.

Do you run APKCube MCP?

Prove ownership with a DNS record, a well-known file, or your Official MCP Registry entry, and your edits publish immediately. Proofs are re-checked weekly; admins can still hide or remove a listing.

Claim this listing