AttestPage · Docs · Pricing · OpenAPI · llms.txt

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:

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"
}
FieldMeaning
existstrue the registry lists the name, false it answered 404, null the lookup failed
statusfound, not_found or lookup_failed (then reason: timeout, network_error, too_large, registry_rate_limited, http_<status> or bad_json)
registry_namethe name as the registry spells it
latest_versionnpm latest tag, PyPI's current version, crates.io's default version
created_atnpm: 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
licensenpm and PyPI: the latest version's licence field; crates.io: the default version's licence
deprecatednpm deprecation message of the latest version, else null
yankedPyPI: latest release yanked; crates.io: crate yanked; npm: null
signalsflags 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_sha256SHA-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.