Check packages
Last updated: 2026-10-10
POST /v1/check/packages looks up 1 to 10 package names on npm, PyPI or crates.io in one call and says, for each, whether the registry lists that name, with its latest version, description, licence, links and a few flags, under one signed receipt. Price: US$0.005 per batch.
Use it before you install a package an agent or a model suggested: a name that does not exist may be a hallucination, and a squatter can register it later.
It answers "exists in registry", nothing more. It is not a security verdict. A listed package can still be malicious, abandoned, typo-squatted or not the one you meant. Description, licence and links are the publisher's own text, copied from the registry.
Request
{ "packages": [
{ "ecosystem": "npm", "name": "left-pad" },
{ "ecosystem": "pypi", "name": "requests" },
{ "ecosystem": "crates", "name": "serde" }
] }
packages is a list of 1 to 10 objects, each with exactly two fields:
ecosystem:npm,pypiorcrates(crates.io)name: the package name as you would install it. npm names may be scoped (@scope/name).
Every name is checked against its registry's naming rules before payment; if any one is invalid, the whole request is refused with 400 and not charged. No lookup happens before payment.
Response
From the free sample (GET /v1/sample/packages), shortened:
{
"results": [
{
"ecosystem": "npm",
"name": "left-pad",
"exists": true,
"status": "found",
"registry_name": "left-pad",
"latest_version": "1.3.0",
"description": "String left pad (demo copy of registry data)",
"license": "WTFPL",
"homepage": "https://github.com/stevemao/left-pad#readme",
"repository": "git+ssh://git@github.com/stevemao/left-pad.git",
"created_at": "2014-03-14T09:09:20.762Z",
"deprecated": "use String.prototype.padStart()",
"yanked": null,
"signals": ["deprecated"],
"registry_url": "https://www.npmjs.com/package/left-pad",
"http_status": 200,
"raw_sha256": "09093cc2…"
},
{
"ecosystem": "npm",
"name": "left-pad-demo-missing",
"exists": false,
"status": "not_found",
"latest_version": null,
"signals": [],
"registry_url": "https://www.npmjs.com/package/left-pad-demo-missing",
"http_status": 404,
"raw_sha256": "c8d3eae1…"
}
],
"note": "exists means the registry listed this name when we looked. It is not a security verdict: the package may still be malicious, unmaintained or not the one you meant; …",
"receipt": "eyJhbGciOiJFZERTQSIs…",
"tier": "paid"
}
| Field | Meaning |
|---|---|
exists | true the registry lists the name, false it answered 404, null the lookup failed |
status | found, not_found or lookup_failed (then reason: timeout, network_error, too_large, registry_rate_limited, http_<status> or bad_json) |
registry_name | the name as the registry spells it |
latest_version | npm latest tag, PyPI's current version, crates.io's default version |
created_at | npm: the package document's time.created (null if that document is over 4 MB or cannot be read); PyPI: first file upload; crates.io: crate creation |
license | npm and PyPI: the latest version's licence field; crates.io: the default version's licence |
deprecated | npm deprecation message of the latest version, else null |
yanked | PyPI: latest release yanked; crates.io: crate yanked; npm: null |
signals | flags worth a second look: name_differs, deprecated, yanked, created_recently (under 30 days). name_differs means the registry's own spelling (registry_name) is not the name you sent; PyPI and crates.io ignore case and treat - and _ (PyPI also .) as the same, so it is often only a spelling variant of the same package |
raw_sha256 | SHA-256 of the registry's answer as we received it (npm: the /latest answer) |
Signals are hints, not a security verdict: a package with no signals can still be malicious.
The receipt has kind packages and, per name, the ecosystem, name, exists, status, latest_version and raw_sha256.
Rate limits
crates.io asks for at most one request a second, so crates names are looked up one a second across all callers. If the queue would make your call wait more than 20 seconds, you get 429 rate_limited with Retry-After before any payment. Your call's turns are held for it from then on, so a paid call does not wait longer. Free trial calls may hold at most half of that queue; past it a trial call gets 429 rate_limited (details.scope trial) and can retry later or pay.
Charging
A failed lookup is a result and the batch is charged. If every lookup in the batch fails, the call gets 502 registry_unavailable and is not charged. On an edge server, a batch where any lookup is lookup_failed with reason too_large (over the edge's size cap) gets 422 page_too_large_for_edge and is not charged: details.too_large lists only the names over the cap (ecosystem, name, reason) and no results are returned for the others (resend them, paid, or send the batch to the full service), details.note starts "not charged: edge size limit", and details.full_service names the full service, which reads the whole answer. See Payments.