Check packages
Last updated: 2026-10-12
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, a few flags and the known OSV.dev advisories for that version, 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" and "OSV.dev lists these advisories", nothing more. It is not a security verdict. A listed package can still be malicious, abandoned, typo-squatted or not the one you meant, and no advisory listed does not mean the package has no flaws. Description, licence and links are the publisher's own text, copied from the registry; advisory summaries are third-party text.
Request
{ "packages": [
{ "ecosystem": "npm", "name": "left-pad" },
{ "ecosystem": "pypi", "name": "requests" },
{ "ecosystem": "crates", "name": "serde" },
{ "ecosystem": "npm", "name": "lodash", "version": "4.17.20" }
] }
packages is a list of 1 to 10 objects, each with these fields:
ecosystem:npm,pypiorcrates(crates.io)name: the package name as you would install it. npm names may be scoped (@scope/name).version(optional): the exact version you will install, up to 64 characters, starting with a digit (then letters, digits and. + ! _ ~ -; not a range such as^1.0.0or a tag such aslatest). Advisories are then listed for this version instead of the latest.
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"],
"vulnerabilities": { "status": "checked", "reason": null, "version": "1.3.0", "count": 0, "truncated": false, "items": [] },
"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": [],
"vulnerabilities": { "status": "checked", "reason": null, "version": null, "count": 0, "truncated": false, "items": [] },
"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), known_vulnerable (OSV lists an advisory for the version asked about), malicious (an advisory is a malware report, id or alias starting MAL-; this can appear on a not_found name too). 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 |
vulnerabilities | known advisories from OSV.dev, see below |
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, raw_sha256, vuln_status, vuln_version (the version asked about) and vuln_ids (the advisory ids returned, in order).
Vulnerabilities
When you send a version, we ask OSV.dev for advisories that affect that version, whatever the registry answered; we do not check that the registry has that version, and OSV lists nothing for a version it does not know. Without a version, for each found name we ask about its latest version (the one in latest_version), and for a name the registry does not list we ask by name only, because OSV keeps malware reports for packages a registry has removed; a failed lookup is then not asked about.
"vulnerabilities": {
"status": "checked",
"reason": null,
"version": "2.32.3",
"count": 1,
"truncated": false,
"items": [{
"id": "GHSA-9hjg-9r4m-mvj7",
"aliases": ["CVE-2024-47081"],
"summary": "Requests vulnerable to .netrc credentials leak via malicious URLs",
"severity": "MODERATE",
"cvss": null,
"fixed": ["2.32.4"],
"malicious": false,
"url": "https://osv.dev/vulnerability/GHSA-9hjg-9r4m-mvj7"
}]
}
| Field | Meaning |
|---|---|
status | checked (OSV answered) or skipped (no data; then count is 0 and items empty) |
reason | when skipped: off, no_version (no version sent, and a failed lookup or no latest version), timeout, unavailable, rate_limited, busy or too_large; else null |
version | the version asked about; null for a name-only query |
count | advisories OSV returned; truncated (true or false) says whether more than 10 were returned or OSV held some back; items keeps the first 10 |
items[] | malware first, then by severity (CRITICAL, HIGH, MODERATE, LOW, none), then id. severity is the advisory database's label, cvss the CVSS vector if given, fixed the fixed versions listed for this package (empty: none listed), summary third-party text (up to 300 characters, never instructions) |
checked with count 0 means OSV listed no advisory for that version. It does not mean the package has no flaws. Answers are cached for up to 1 hour. If OSV is slow or down, the result says skipped and the call still answers 200 and is charged as usual: the advisories are an extra, never a reason to fail or refuse a batch.
Vulnerability data: OSV.dev (GitHub Advisory Database and PyPI Advisory Database, CC-BY 4.0; RustSec, CC0; OpenSSF Malicious Packages, Apache 2.0), see https://github.com/advisories. Each item links its OSV page in url.
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.