Required Reading
~5 min

API Reference

Every endpoint that powers npmscan.com — public, unauthenticated, and free to call directly.

Public & unauthenticated
No API key, no signup, no CORS restrictions. Base URL is https://npmscan.com/api— it's the same read-only data that powers the website. Please be a good citizen: cache results client-side and avoid tight polling loops.
Building an AI agent instead?
Skip raw HTTP — npmscan also runs a public MCP server at https://npmscan.com/api/mcp with 23ready-made tools for package, version, and vulnerability lookups. Same data, no API key, one line of config. There's also a plaintext /llms.txt if your agent just wants a map of the site.
Rate limited
Each endpoint group below (npm registry, vulnerabilities, advisories, CVE, analysis, feeds, GitHub) is capped at 30 requests / 60s per IP. Going over returns 429 Too Many Requests with X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers (plus the same fields in the JSON body) telling you the budget, how many requests are left, and how many seconds to wait. /api/mcp has its own separate limit at the same rate, surfaced as error.data on its JSON-RPC 429 response. Cache responses client-side to stay well under it.

Search the npm registry and pull package or version metadata — mirrored with a registry fallback for reliability.

Query / route params
NameTypeRequiredDescription
namestringrequirednpm package name (URL-encode scoped names, e.g. %40scope%2Fname). Passed as a path segment.
Request
curl "https://npmscan.com/api/npm/package/lodash"
Response200 OK
{
  "name": "lodash",
  "dist-tags": { "latest": "4.18.1" },
  "versions": {
    "4.18.1": {
      "name": "lodash",
      "version": "4.18.1",
      "license": "MIT",
      "dist": {
        "shasum": "ff2b66c1f6326d59513de2407bf881439812771c",
        "tarball": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
        "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBc..."
      }
    }
    /* ...every other published version */
  },
  "time": {
    "created": "2012-04-23T16:37:11.912Z",
    "modified": "2026-09-25T00:57:07.767Z",
    "4.18.1": "2026-04-01T21:01:20.458Z"
  },
  "maintainers": [{ "name": "jdalton", "email": "john.david.dalton@gmail.com" }],
  "license": "MIT",
  "repository": { "type": "git", "url": "git+https://github.com/lodash/lodash.git" }
}
Error — unknown package404 Not Found
{ "error": "Package not found" }
  • ›Response is the full npm registry packument — can be large for packages with many published versions.
  • ›Verified live — a "latest" dist-tag and `time.modified` are moving targets on an actively-maintained package; re-check before treating either as a fixed reference value.

Query / route params
NameTypeRequiredDescription
namestringrequirednpm package name.
versionstringrequiredExact version or dist-tag (e.g. 1.4.2 or latest).
Request
curl "https://npmscan.com/api/npm/package/example-package/version/1.4.2"
Response200 OK
{
  "name": "example-package",
  "version": "1.4.2",
  "description": "Example package for documentation purposes.",
  "license": "MIT",
  "scripts": {
    "preinstall": "node ./scripts/check-platform.js",
    "postinstall": "node ./scripts/postinstall.js",
    "test": "jest"
  },
  "dependencies": { "chalk": "^5.3.0" },
  "dist": {
    "integrity": "sha512-abc123...",
    "shasum": "9c1a4d2f...",
    "tarball": "https://registry.npmjs.org/example-package/-/example-package-1.4.2.tgz",
    "fileCount": 12,
    "unpackedSize": 48213
  },
  "maintainers": [{ "name": "maintainer-handle", "email": "maintainer@example.com" }]
}
Error — unknown version404 Not Found
{ "error": "Package version not found" }
  • ›`scripts.preinstall` / `scripts.postinstall` are what actually runs on install — check them here rather than trusting the README.

Proxies to OSV.dev, scoped to the npm ecosystem by default. Use the batch endpoint to scan an entire package.json or lockfile in one request — it's the same call npmscan.com/analyze makes under the hood.

Body params (application/json)
NameTypeRequiredDescription
namestringrequirednpm package name.
versionstringoptionalNarrow results to a specific version. Omit for all known advisories.
ecosystemstringoptionalDefaults to "npm".
Request
curl -X POST https://npmscan.com/api/osv/query \
  -H "Content-Type: application/json" \
  -d '{"name":"lodash","version":"4.17.15"}'
Response200 OK
{
  "vulns": [
    {
      "id": "GHSA-35jh-r3h4-6jhm",
      "summary": "Prototype Pollution in lodash",
      "aliases": ["CVE-2020-8203"],
      "modified": "2022-01-27T00:00:00Z",
      "published": "2020-07-15T00:00:00Z",
      "database_specific": { "severity": "HIGH" },
      "affected": [
        {
          "package": { "name": "lodash", "ecosystem": "npm" },
          "ranges": [
            { "type": "ECOSYSTEM", "events": [{ "introduced": "0" }, { "fixed": "4.17.19" }] }
          ]
        }
      ],
      "references": [
        { "type": "ADVISORY", "url": "https://github.com/advisories/GHSA-35jh-r3h4-6jhm" }
      ],
      "fixedVersion": "4.17.19"
    }
  ],
  "isVulnerable": true,
  "highestSeverity": "HIGH"
}
  • ›`vulns` is empty (not omitted) when the package has no known advisories for the given version.
  • ›Unlike the batch endpoint below, `ecosystem` isn't restricted to npm — this passes straight through to OSV.dev, so PyPI, Go, crates.io, Maven, RubyGems, and any other OSV-supported ecosystem work too.
  • ›`isVulnerable` and `highestSeverity` are direct top-level answers computed from `vulns` — use them instead of scanning the array yourself. Each entry in `vulns` also carries a merged-in `fixedVersion` (the version that patches it, extracted from `affected[].ranges[].events`, `null` when OSV can't resolve one) — everything else in `vulns` is OSV's own document shape, untouched.

Body params (application/json)
NameTypeRequiredDescription
packagesArray<{ name, version? }>optionalUp to 1000 items. `version` is optional per item. Use this OR `content`, not both.
contentstringoptionalRaw package.json, package-lock.json (v1-v3), yarn.lock (classic or Berry), pnpm-lock.yaml, CycloneDX JSON, or SPDX JSON — parsed into a package list for you. Use this OR `packages`, not both.
includeDevDependenciesbooleanoptionalOnly applies when `content` is a manifest/lockfile format that distinguishes dev dependencies.
includePeerDependenciesbooleanoptionalOnly applies when `content` is a package.json. peerDependencies are excluded from scanning by default (see `warnings`) since a peer is often intentionally left unresolved by the consumer.
Request
curl -X POST https://npmscan.com/api/osv/batch \
  -H "Content-Type: application/json" \
  -d '{"packages":[{"name":"lodash","version":"4.17.15"},{"name":"minimist","version":"1.2.0"}]}'
Response200 OK
{
  "inputFormat": "packages",
  "parsedPackageCount": 2,
  "results": [
    {
      "package": { "name": "lodash", "version": "4.17.15" },
      "scanStatus": "scanned",
      "vulns": [{ "id": "GHSA-35jh-r3h4-6jhm", "summary": "Prototype Pollution in lodash", "fixedVersion": "4.17.19", "...": "full OSV vuln doc" }],
      "isVulnerable": true,
      "highestSeverity": "HIGH"
    },
    {
      "package": { "name": "minimist", "version": "1.2.0" },
      "scanStatus": "scanned",
      "vulns": [{ "id": "GHSA-vh95-rmgr-6w4m", "summary": "Prototype Pollution in minimist", "fixedVersion": "1.2.3", "...": "full OSV vuln doc" }],
      "isVulnerable": true,
      "highestSeverity": "MODERATE"
    }
  ]
}
Error — batch too large400 Bad Request
{ "error": "\"packages\" must contain at most 1000 items" }
  • ›`results[i]` corresponds positionally to the package list you sent (or, with `content`, the packages npmscan parsed out of it) — same order, one entry per package.
  • ›Unlike OSV.dev's own batch endpoint (which only returns bare `{id, modified}` stubs), each `results[i].vulns` entry is enriched with full detail — summary, severity, aliases, `fixedVersion` — via a follow-up fetch per unique advisory; requests over 100 packages are chunked past OSV's own batch cap transparently.
  • ›`results[i].scanStatus: "not-scanned"` means this package's spec pointed somewhere other than the registry (git/file/workspace/URL) or never resolved to a version — it was excluded from the OSV query entirely, NOT queried by name alone (which would otherwise attach an unrelated public npm package's entire vulnerability history to it). `vulns: []`/`isVulnerable: false` there is NOT a clean bill of health. `results[i].package` also carries `actualName` (set only for an npm alias — the real package `vulns` attach to) and `declaredSpec` (set when `version` was resolved from a package.json range/tag rather than being an already-exact pin).
  • ›Optional response fields, present only when relevant: `ignoredCount`/`warnings` (entries from `content` that couldn't be identified as npm dependencies), `queryFailureCount` (packages an upstream OSV query failed for — treat those as unchecked, not confirmed clean), `enrichmentNote` (advisory detail truncated past this call's enrichment budget).
  • ›Unlike the batch_query_vulnerabilities MCP tool this mirrors, this endpoint does not currently return `signals` (popularity/maintenance/typosquat) or `source` (lockfile resolved-URL/integrity tamper detection) — those are MCP-only today.
  • ›This is exactly what powers the batch scan on npmscan.com/analyze: parse your package.json or package-lock.json (v1–v3) into a { name, version } list for every dependency, POST it here, then optionally call GET /api/npm/package/:name per package to flag outdated versions. Paste the file directly into /analyze for the full UI with that enrichment built in — or skip the parsing yourself and pass the file content via `content`.

Reviewed GitHub Security Advisories for the npm ecosystem — useful for a "what shipped this week" feed or as a fallback when OSV.dev hasn't ingested a just-published advisory yet.

Query / route params
NameTypeRequiredDescription
pagenumberoptionalPage number, 30 per page. Defaults to 1.
severitystringoptionalFilter: low, medium, high, critical, or all.
Request
curl "https://npmscan.com/api/advisories/latest?severity=critical&page=1"
Response200 OK
{
  "advisories": [
    {
      "id": "GHSA-xxxx-xxxx-xxxx",
      "cve": "CVE-2024-00000",
      "ghsaUrl": "https://github.com/advisories/GHSA-xxxx-xxxx-xxxx",
      "summary": "Remote code execution via crafted input",
      "severity": "critical",
      "publishedAt": "2024-05-01T00:00:00Z",
      "updatedAt": "2024-05-02T00:00:00Z",
      "packages": [
        { "name": "example-package", "affectedRange": "< 2.0.1", "patchedVersion": "2.0.1" }
      ]
    }
  ]
}
  • ›Flattened and simplified from GitHub's advisory schema; scoped to `ecosystem=npm`, `type=reviewed`.

Query / route params
NameTypeRequiredDescription
idstringrequiredGHSA id (e.g. GHSA-xxxx-xxxx-xxxx) or CVE id.
Request
curl "https://npmscan.com/api/advisories/GHSA-xxxx-xxxx-xxxx"
Response200 OK
{
  "id": "GHSA-xxxx-xxxx-xxxx",
  "summary": "Remote code execution via crafted input",
  "details": "Full advisory description...",
  "aliases": ["CVE-2024-00000"],
  "modified": "2024-05-02T00:00:00Z",
  "published": "2024-05-01T00:00:00Z",
  "database_specific": { "severity": "CRITICAL", "cwe_ids": ["CWE-94"] },
  "severity": [{ "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" }],
  "affected": [
    {
      "package": { "name": "example-package", "ecosystem": "npm" },
      "ranges": [{ "type": "ECOSYSTEM", "events": [{ "introduced": "0" }, { "fixed": "2.0.1" }] }]
    }
  ],
  "references": [{ "type": "ADVISORY", "url": "https://github.com/advisories/GHSA-xxxx-xxxx-xxxx" }]
}
Error — unknown advisory404 Not Found
{ "error": "Advisory not found" }
  • ›Same response shape as OSV — GitHub publishes immediately, OSV.dev syncs on its own delayed schedule. Query /api/osv/query first and fall back to this for a specific id.

Authoritative NIST NVD data, not OSV.dev — CVSS score/vector, CISA KEV (confirmed active exploitation) and FIRST.org EPSS (30-day exploitation probability) per CVE. Not npm-scoped like the rest of this API: NVD covers every ecosystem, so pass `keywordSearch` to narrow a search to a specific package. Shares its implementation with the get_cve MCP tool.

Body params (application/json)
NameTypeRequiredDescription
cveIdstringoptionalExact CVE ID, e.g. "CVE-2024-12345". Use this OR the search filters below, not both.
keywordSearchstringoptionalFree-text search, e.g. a package or product name (1-200 chars).
severitystringoptionalFilter by CVSS v3 base severity: CRITICAL, HIGH, MEDIUM, or LOW.
cweIdstringoptionalFilter by weakness type, e.g. "CWE-79".
publishedSincestringoptionalPublication date range start (YYYY-MM-DD). Must be given together with publishedUntil.
publishedUntilstringoptionalPublication date range end (YYYY-MM-DD). Range is capped at 120 days.
resultsPerPagenumberoptionalMax results for a search (default 10, capped at 50).
startIndexnumberoptionalPagination offset for a search.
Request — exact lookup
curl -X POST https://npmscan.com/api/cve \
  -H "Content-Type: application/json" \
  -d '{"cveId":"CVE-2024-12345"}'
Response200 OK
{
  "id": "CVE-2024-12345",
  "npmscanUrl": "https://npmscan.com/vulnerability/CVE-2024-12345",
  "vulnStatus": "Analyzed",
  "description": "...",
  "published": "2024-05-01T00:00:00.000",
  "lastModified": "2024-05-02T00:00:00.000",
  "cvss": { "version": "3.1", "baseScore": 9.8, "baseSeverity": "CRITICAL", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
  "cwes": ["Improper Input Validation"],
  "references": [{ "url": "https://example.com/advisory", "source": "cve@mitre.org", "tags": ["Third Party Advisory"] }],
  "source": "nvd",
  "kev": null,
  "epss": { "score": 0.42, "percentile": 0.91, "date": "2024-05-03" }
}
Error — invalid combination400 Bad Request
{ "error": "When \"cveId\" is provided, omit keywordSearch, severity, cweId, and date-range filters" }
  • ›A single-`cveId` lookup with no match returns `200 OK` with `{ "cveId": "...", "found": false, "npmscanUrl": "..." }` rather than a 404 — a bare CVE ID isn't guaranteed to exist yet.
  • ›If NVD has no record yet (or hasn't scored one), a single-`cveId` lookup falls back to the raw MITRE CVE record automatically — look for `"source": "mitre"` on the result.
  • ›A search (any of keywordSearch/severity/cweId/publishedSince+publishedUntil instead of cveId) returns `{ totalResults, startIndex, resultsPerPage, cves: [...] }` — each entry in `cves` has the same shape as the exact-lookup response above.
  • ›`kev` is non-null only if this CVE is a confirmed, actively-exploited-in-the-wild vulnerability on CISA's catalog — treat that as an urgent-patch signal regardless of CVSS score.
  • ›NVD enforces a strict shared rate limit; a 429 response means retry shortly rather than a genuine failure.

Deeper, heuristic checks beyond raw registry/vulnerability data: install-script scanning, transitive-dependency graphs, publish provenance, maintainer-change history, license policy, before/after diffing, remediation prioritization, and alternative-package suggestions. Each of these mirrors an MCP tool of the same shape — same request/response contract, same underlying logic.

Body params (application/json)
NameTypeRequiredDescription
namestringrequiredExact npm package name, e.g. "lodash" or "@scope/name".
versionstringoptionalExact version to analyze; omit to use the latest published version.
Request
curl -X POST https://npmscan.com/api/analysis/install-script \
  -H "Content-Type: application/json" \
  -d '{"name":"event-stream","version":"3.3.6"}'
Response200 OK
{
  "name": "event-stream",
  "version": "3.3.6",
  "npmscanUrl": "https://npmscan.com/package/event-stream",
  "hasLifecycleScripts": false,
  "lifecycleScripts": {},
  "filesScanned": [],
  "scanNote": null,
  "possibleTyposquatOf": null,
  "findings": [],
  "totalScore": 0,
  "riskTier": "none"
}
Error — package not found404 Not Found
{ "error": "Package \"left-pad-typo\" not found" }
  • ›This is a heuristic static scan of the published tarball — it does not execute any code and cannot see behavior gated on runtime conditions.
  • ›Fetches and gunzips the actual tarball, so it is slower than a plain registry lookup; budget for it accordingly in a CI gate.
  • ›Shares its implementation with the analyze_install_script MCP tool.

Body params (application/json)
NameTypeRequiredDescription
packagesArray<{ name, version? }>required1-15 root packages, e.g. a package.json's "dependencies". version accepts an exact version or a semver range; omitted = latest.
maxDepthnumberoptionalHow many levels of transitive dependencies to expand (0 = only the roots). Default 2, max 3.
Request
curl -X POST https://npmscan.com/api/analysis/transitive-dependencies \
  -H "Content-Type: application/json" \
  -d '{"packages":[{"name":"express","version":"^4.18.0"}],"maxDepth":2}'
Response200 OK
{
  "summary": "Scanned 62 packages (2 unresolved) up to depth 2; 1 vulnerable package found.",
  "roots": [{ "name": "express", "requestedVersion": "^4.18.0" }],
  "maxDepth": 2,
  "nodes": [ /* full resolved graph: depth, parents, resolutionError, vulnerabilities */ ],
  "vulnerablePaths": [
    {
      "name": "cookie",
      "version": "0.4.2",
      "highestSeverity": "MODERATE",
      "vulnerabilityCount": 1,
      "pulledInBy": ["express"],
      "npmscanUrl": "https://npmscan.com/package/cookie"
    }
  ],
  "totalPackagesScanned": 62,
  "unresolvedCount": 2,
  "vulnerablePackageCount": 1,
  "totalVulnerabilities": 1,
  "truncated": false,
  "truncationNote": null,
  "enrichmentNote": null
}
  • ›Only the "dependencies" field is followed (not devDependencies/peerDependencies/optionalDependencies), and each range is resolved independently per branch — this does not emulate npm/yarn's actual hoisting/dedup.
  • ›The traversal is capped at a total node budget per request; check `truncated`/`truncationNote` rather than assuming a large graph was scanned exhaustively.
  • ›Prefer POST /api/osv/batch instead when you already have a flat package@version list — this endpoint is for when you only have the direct/root dependencies and need the graph walked for you.

Body params (application/json)
NameTypeRequiredDescription
namestringrequiredExact npm package name.
versionstringoptionalExact version to check; omit to use the latest published version.
Request
curl -X POST https://npmscan.com/api/analysis/provenance \
  -H "Content-Type: application/json" \
  -d '{"name":"tinycolor2"}'
Response200 OK
{
  "name": "tinycolor2",
  "version": "1.6.0",
  "npmscanUrl": "https://npmscan.com/package/tinycolor2",
  "provenance": { "hasProvenance": false, "predicateType": null, "sourceRepository": null, "workflowPath": null, "builderId": null, "sourceCommit": null, "buildRunUrl": null, "declaredRepository": "github.com/bgrins/TinyColor", "repositoryMatchesBuild": null, "note": null },
  "peers": { "orgKind": null, "orgIdentifier": null, "peersChecked": 0, "peersWithProvenance": 0, "peerProvenanceRate": null, "note": null },
  "sourceDiff": { "checked": false, "gitRef": null, "refSource": null, "addedInstallScripts": [], "addedDependencies": [], "note": null },
  "findings": [],
  "totalScore": 0,
  "riskTier": "none"
}
  • ›Most packages do not use `npm publish --provenance` yet, so its bare absence is never scored on its own — only an org-norm anomaly (peers in scope mostly have it) or an actual source mismatch is.
  • ›This is a heuristic, structural check: it does not cryptographically re-verify the Sigstore bundle — it trusts npm's registry already rejected any publish that failed that verification.

Body params (application/json)
NameTypeRequiredDescription
namestringrequiredExact npm package name.
Request
curl -X POST https://npmscan.com/api/analysis/maintainer-changes \
  -H "Content-Type: application/json" \
  -d '{"name":"chalk"}'
Response (truncated, verified live)200 OK
{
  "name": "chalk",
  "npmscanUrl": "https://npmscan.com/package/chalk",
  "lookbackDays": 180,
  "currentMaintainers": [{ "name": "sindresorhus", "email": "sindresorhus@gmail.com" }],
  "history": {
    "versionsConsidered": 44,
    "firstTrackedVersion": { "version": "0.1.0", "publishedAt": "2013-08-03T00:21:59.499Z" },
    "latestTrackedVersion": { "version": "6.0.0", "publishedAt": "2026-07-26T14:51:07.269Z" },
    "latestVersionPublishedViaTrustedPublisher": false,
    "changes": [
      { "version": "1.1.2", "publishedAt": "2016-03-28T23:32:04.003Z", "added": ["qix"], "removed": ["jbnicolai"] },
      { "version": "5.6.2", "publishedAt": "2025-09-08T14:47:54.486Z", "added": [], "removed": ["qix"] }
    ],
    "changesTruncated": false,
    "note": null
  },
  "repository": { "checked": true, "declaredRepository": "git+https://github.com/chalk/chalk.git", "currentFullName": "chalk/chalk", "transferred": false, "archived": false, "reachable": true, "note": null },
  "findings": [],
  "totalScore": 0,
  "riskTier": "none"
}
  • ›Reconstructed entirely from the packument's per-version maintainer snapshots — no extra API calls beyond the registry doc and one GitHub repo lookup.
  • ›A maintainer-list change that happened on npm's site after the latest release (access changed but nothing published with it yet) surfaces as a `changes` entry with `version`/`publishedAt` both null.
  • ›`findings: []`/`riskTier: "none"` here does NOT mean chalk has a clean history — it means nothing in `history.changes` falls inside the 180-day `lookbackDays` window right now. qix was REMOVED as a chalk maintainer on 2025-09-08 (the real, well-documented "qix" chalk/debug compromise — access revoked after it was discovered), which is real and visible in `history.changes` above, but is now old enough to no longer score. Always read `history.changes` itself, not just `findings`/`riskTier`, for the full record.

Body params (application/json)
NameTypeRequiredDescription
maintainerUsernamestringrequiredExact npm username, e.g. "jaredwray" — as shown at npmjs.com/~username.
Request
curl -X POST https://npmscan.com/api/analysis/maintainer-blast-radius \
  -H "Content-Type: application/json" \
  -d '{"maintainerUsername":"jaredwray"}'
Response (truncated, verified live — the real Aug 2026 keyv/cacheable "Shai-Hulud" worm compromise)200 OK
{
  "maintainerUsername": "jaredwray",
  "npmscanUrl": "https://npmscan.com/profile/jaredwray",
  "npmProfileUrl": "https://www.npmjs.com/~jaredwray",
  "avatarUrl": "https://npmscan.com/api/avatar/f08cf036a76de57f0deb190a24970e29",
  "totalPackagesFound": 61,
  "packagesReturned": 61,
  "resultsTruncated": false,
  "clusterWindowHours": 72,
  "packages": [],
  "clusters": [
    /* 4 clusters as of this writing — this account keeps publishing, so
       both the count and totals below will keep moving; re-verify live */
    {
      "windowStart": "2026-06-27T18:19:51.509Z",
      "windowEnd": "2026-06-27T18:20:33.507Z",
      "packageNames": ["@cacheable/utils", "@cacheable/memory", "@cacheable/node-cache", "cache-manager", "cacheable", "@cacheable/net", "flat-cache", "file-entry-cache"],
      "packageCount": 8,
      "combinedWeeklyDownloads": 278348331,
      "stillCurrentMaintainerCount": 8
    }
  ],
  "findings": [ /* one tight-publish-cluster finding per cluster, 4 total */ ],
  "totalScore": 116,
  "riskTier": "critical",
  "note": null
}
  • ›Uses npm's own maintainer:<username> search index (registry.npmjs.org's /-/v1/search) — the same reverse lookup npmjs.com's own site search uses. The public registry API has no dedicated "list packages by maintainer" endpoint otherwise.
  • ›A large total package count is not itself a finding — only a tight cluster of packages published within `clusterWindowHours` of each other is scored, weighted by cluster size and by the packages' combined weekly downloads/dependentsCount. But a huge, entirely legitimate account is not automatically safe either: sindresorhus (1000+ packages, see /api/analysis/maintainer-profile below) currently combines 15 separate legitimate clusters accumulated over 5+ years into `riskTier: "critical"` — proof that this field is a prompt to verify with /api/analysis/maintainer-changes, never a standalone verdict, at any tier.
  • ›A cluster where most packages share one npm scope (e.g. `@docusaurus/*`) is dampened — that's a project's own monorepo doing one coordinated release, not a compromised account spread across unrelated packages — and multiple distinct clusters on one account combine with diminishing returns rather than a plain sum, so a prolific account with several independent legitimate release clusters doesn't accumulate an unbounded score just from being active over time. A single very large or high-exposure cluster can still reach `riskTier: "critical"` on its own; treat it as a prompt to verify with `/api/analysis/maintainer-changes`, not as a standalone verdict.
  • ›Results are capped at one search page (up to 250 packages, ranked by npm's own relevance/popularity scoring, not by recency) — `resultsTruncated`/`totalPackagesFound` say when a maintainer's real footprint exceeds that.

Body params (application/json)
NameTypeRequiredDescription
maintainerUsernamestringrequiredExact npm username, e.g. "sindresorhus" — as shown at npmjs.com/~username.
Request
curl -X POST https://npmscan.com/api/analysis/maintainer-profile \
  -H "Content-Type: application/json" \
  -d '{"maintainerUsername":"sindresorhus"}'
Response (truncated, verified live)200 OK
{
  "maintainerUsername": "sindresorhus",
  "npmscanUrl": "https://npmscan.com/profile/sindresorhus",
  "npmProfileUrl": "https://www.npmjs.com/~sindresorhus",
  "avatarUrl": "https://npmscan.com/api/avatar/d36a92237c75c5337c17b60d90686bf9",
  "totalPackagesFound": 1064,
  "packagesReturned": 250,
  "resultsTruncated": true,
  "currentlyMaintainsCount": 250,
  "totalWeeklyDownloads": 17893700000,
  "totalDependents": 620500,
  "packages": [
    {
      "name": "chalk",
      "version": "6.0.0",
      "lastPublished": "2026-07-26T14:51:07.269Z",
      "weeklyDownloads": 411482632,
      "dependentsCount": 160309,
      "isCurrentMaintainer": true,
      "npmscanUrl": "https://npmscan.com/package/chalk"
    }
  ],
  "note": null
}
  • ›Shares its business logic with the get_maintainer_profile MCP tool via runGetMaintainerProfile, and the same underlying npm search call as /api/analysis/maintainer-blast-radius (searchMaintainerPackages) — no duplicated fetch between the two endpoints.
  • ›This endpoint does not run publish-cluster/compromised-account detection. Use /api/analysis/maintainer-blast-radius when the goal is a security read on whether recent activity looks like a takeover, not just a profile summary.
  • ›Results are capped at one search page (up to 250 packages) — `resultsTruncated`/`totalPackagesFound` say when a maintainer's real footprint exceeds that, and the download/dependent totals are summed only over `packages` (packagesReturned), not the full totalPackagesFound.

Body params (application/json)
NameTypeRequiredDescription
packagesArray<{ name, version? }>required1-100 packages to check.
policy{ allow?: string[]; deny?: string[] }optionalSPDX ids, family prefixes (e.g. "GPL"), or category names. Omit for the default policy: only copyleft/network-copyleft/proprietary are violations.
Request
curl -X POST https://npmscan.com/api/analysis/license-compliance \
  -H "Content-Type: application/json" \
  -d '{"packages":[{"name":"left-pad"},{"name":"readline-sync"}],"policy":{"deny":["GPL"]}}'
Response200 OK
{
  "policy": { "mode": "deny", "allow": [], "deny": ["GPL"] },
  "summary": "2 packages checked: 2 compliant, 0 violations, 0 need review.",
  "results": [
    {
      "package": { "name": "left-pad" },
      "npmscanUrl": "https://npmscan.com/package/left-pad",
      "resolvedVersion": "1.3.0",
      "rawLicense": "MIT",
      "category": "permissive",
      "isCompliant": true,
      "needsReview": false,
      "violation": null,
      "resolutionError": null
    }
  ],
  "totalPackages": 2,
  "compliantCount": 2,
  "violationCount": 0,
  "needsReviewCount": 0,
  "unresolvedCount": 0
}
  • ›`policy.deny` always wins over `policy.allow`. With `policy.allow` set, anything not matching it is treated as a violation (unproven is non-compliant).
  • ›Understands simple SPDX expressions: "(MIT OR GPL-3.0)" is compliant if either side is permitted; "MIT AND Apache-2.0" requires both. A nested/mixed expression is reported as `needsReview` rather than guessed at.
  • ›Reads only the registry-declared `license` field — it does not fetch or parse LICENSE file contents from the source repository.

Body params (application/json)
NameTypeRequiredDescription
beforestringrequiredRaw file content: package.json, package-lock.json (npm v1-v3), yarn.lock (classic or Berry), or pnpm-lock.yaml.
afterstringrequiredRaw file content, same format rules as `before`. Format is auto-detected per side; before/after may differ.
Request
curl -X POST https://npmscan.com/api/analysis/dependency-diff \
  -H "Content-Type: application/json" \
  -d '{"before":"<package.json before>","after":"<package.json after>"}'
Response200 OK
{
  "summary": "Compared package.json snapshots: 0 added, 0 removed, 1 changed — 1 flagged for a newly introduced install script, source/integrity change, override change, and/or vulnerability.",
  "beforeFormat": "package.json",
  "afterFormat": "package.json",
  "comparisonNote": null,
  "added": [],
  "removed": [],
  "changed": [
    {
      "name": "ua-parser-js",
      "npmscanUrl": "https://npmscan.com/package/ua-parser-js",
      "beforeVersion": "0.7.28",
      "afterVersion": "0.7.29",
      "coexistingVersions": null,
      "changeType": "upgrade",
      "hasInstallScript": true,
      "installScriptIntroduced": true,
      "installScriptKeys": ["preinstall"],
      "installScriptKeysIntroduced": ["preinstall"],
      "sourceIntegrityChanged": null,
      "resolvedUrl": null,
      "integrity": null,
      "isVulnerable": true,
      "highestSeverity": "CRITICAL",
      "vulnerabilities": [],
      "vulnerabilityDelta": "introduced",
      "resolutionNote": null
    }
  ],
  "totalAdded": 0,
  "totalRemoved": 0,
  "totalChanged": 1,
  "flaggedCount": 1,
  "truncated": false,
  "truncationNote": null,
  "enrichmentNote": null,
  "projectLifecycleChanges": null,
  "overridesChanges": null
}
Error — unparseable snapshot400 Bad Request
{ "error": "before: unrecognized file format — expected package.json, package-lock.json, yarn.lock, or pnpm-lock.yaml" }
  • ›`installScriptIntroduced` and `sourceIntegrityChanged` are the two highest-signal per-package fields here: a routine-looking patch bump that quietly adds a postinstall script is the shape of a compromised-maintainer attack, and a lockfile entry whose resolved tarball URL/integrity hash changed while the version stayed IDENTICAL is a tampered lockfile or compromised mirror a version-only diff would miss.
  • ›Every lockfile format (package-lock.json, pnpm-lock.yaml, yarn.lock) reports its FULL resolved graph — direct and transitive dependencies alike — so a transitive-only bump is caught too; a package.json is diffed as its own declared list only, since a manifest has no transitive data at all.
  • ›`projectLifecycleChanges` (the scanned project's own root lifecycle scripts) and `overridesChanges` (`overrides`/`resolutions`/`pnpm.overrides`) are reported independently of `added`/`removed`/`changed` — a PR that only touches one of these changes nothing else here would normally flag.
  • ›Body size is capped at 8 MiB per snapshot to bound worst-case parse time for a pathological monorepo lockfile.
  • ›Ideal for a CI gate reviewing a dependency-changing PR — pass the file content directly, no shell-out to npm/yarn/pnpm required.

Body params (application/json)
NameTypeRequiredDescription
findingsArray<{ packageName, cveId?, severity?, currentVersion?, fixedVersion?, advisoryId?, findingType? }>required1-200 findings, typically straight from POST /api/osv/batch or /api/analysis/transitive-dependencies. findingType: "malware" (or a MAL-* advisoryId) forces tier "remove-now"; omit for an ordinary vulnerability.
Request
curl -X POST https://npmscan.com/api/analysis/prioritize-remediation \
  -H "Content-Type: application/json" \
  -d '{"findings":[{"packageName":"lodash","cveId":"CVE-2020-8203","severity":"HIGH","fixedVersion":"4.17.19"}]}'
Response200 OK
{
  "totalFindings": 1,
  "uniqueCveCount": 1,
  "summary": { "removeNow": 0, "patchNow": 0, "patchSoon": 1, "scheduled": 0, "monitor": 0, "kevListedCount": 0 },
  "ranked": [
    {
      "rank": 1,
      "packageName": "lodash",
      "cveId": "CVE-2020-8203",
      "advisoryId": null,
      "currentVersion": null,
      "fixedVersion": "4.17.19",
      "severity": "HIGH",
      "kev": null,
      "epss": { "score": 0.045, "percentile": 0.91, "date": "2024-01-15" },
      "score": 62,
      "tier": "patch-soon",
      "findingType": "vulnerability",
      "reason": "High EPSS (4.5%) with HIGH severity, not on CISA KEV.",
      "npmscanUrl": "https://npmscan.com/package/lodash",
      "cveNpmscanUrl": "https://npmscan.com/vulnerability/CVE-2020-8203"
    }
  ]
}
  • ›Does not re-query OSV/NVD itself — pass in findings other tools already returned; this only adds CISA KEV + FIRST.org EPSS enrichment and ranks the batch.
  • ›A CVE id shared by multiple findings in the same call is only looked up once.
  • ›A finding with no `cveId` (a GHSA advisory with no CVE alias) is still ranked, using severity alone as the fallback signal.
  • ›A confirmed-malware finding (`findingType: "malware"` in the request body, or a `MAL-*` `advisoryId` — auto-detected even without setting `findingType`) forces `tier: "remove-now"` ahead of every other signal, including KEV/EPSS.

Body params (application/json)
NameTypeRequiredDescription
packageNamestringrequiredExact npm package name, e.g. "lodash" or "@scope/name".
currentVersionstringrequiredCurrently installed version — exact version, semver range, or dist-tag.
targetVersionstringoptionalVersion to simulate upgrading to — exact version, range, or dist-tag (e.g. the fixedVersion a prioritize-remediation finding named). Omit for the registry's "latest" dist-tag.
Request
curl -X POST https://npmscan.com/api/analysis/simulate-dependency-upgrade \
  -H "Content-Type: application/json" \
  -d '{"packageName":"lodash","currentVersion":"3.10.1","targetVersion":"4.17.21"}'
Response200 OK
{
  "packageName": "lodash",
  "npmscanUrl": "https://npmscan.com/package/lodash",
  "resolvedCurrentVersion": "3.10.1",
  "resolvedTargetVersion": "4.17.21",
  "direction": "upgrade",
  "semverBump": "major",
  "isBreakingBySemver": true,
  "majorVersionsSkipped": 0,
  "targetDeprecated": null,
  "installScriptIntroduced": false,
  "engineChange": null,
  "currentIsVulnerable": false,
  "targetIsVulnerable": false,
  "vulnerabilityDelta": "still-clean",
  "targetVulnerabilities": [],
  "riskTier": "breaking-change-likely",
  "reasons": [
    "Major version bump — semver signals this release is allowed to contain breaking API changes."
  ],
  "verdict": "Upgrading \"lodash\" from the current to the target version is a major upgrade that is likely to require code changes."
}
  • ›Does not fetch the package's changelog/release notes or diff the target tarball's source — this is a fast, deterministic pre-check built from registry metadata and OSV, not a substitute for reading the release notes on a flagged major bump.
  • ›A minor bump between two pre-1.0 (0.x) versions is treated as breaking-risk, per semver's own convention that the public API isn't stable before 1.0.
  • ›An unresolvable version spec (a typo, an unsatisfiable range) comes back as a normal 200 with `resolvedCurrentVersion`/`resolvedTargetVersion: null` and an explanatory note, not a request error — only a nonexistent package name is rejected outright.

Body params (application/json)
NameTypeRequiredDescription
namestringrequiredExact npm package name, e.g. "request" or "node-sass".
reasonstringoptionalOne of deprecated/vulnerable/abandoned/typosquat/general — biases filtering/ranking. Auto-inferred when omitted.
limitnumberoptionalMax suggestions to return. Default 5, max 10.
Request
curl -X POST https://npmscan.com/api/analysis/suggest-alternative \
  -H "Content-Type: application/json" \
  -d '{"name":"request","limit":3}'
Response200 OK
{
  "source": { "name": "request", "latestVersion": "2.88.2", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "isLatestVersionVulnerable": false, "highestSeverity": null, "popularityTier": "high", "maintenanceTier": "stale", "possibleTyposquatOf": null, "npmscanUrl": "https://npmscan.com/package/request" },
  "reason": "deprecated",
  "confidence": "high",
  "categoryTokens": ["http", "request", "client"],
  "searchedQueries": ["http client"],
  "nonPackageAlternatives": ["fetch (built-in since Node 18)"],
  "suggestions": [
    {
      "name": "undici",
      "version": "6.19.2",
      "description": "An HTTP/1.1 client, written from scratch for Node.js",
      "npmscanUrl": "https://npmscan.com/package/undici",
      "weeklyDownloads": 42000000,
      "dependentsCount": 1200,
      "githubStars": 6200,
      "hasBuiltInTypes": true,
      "deprecated": null,
      "isLatestVersionVulnerable": false,
      "highestSeverity": null,
      "popularityTier": "very-high",
      "maintenanceTier": "active",
      "topPackagesRank": 42,
      "categoryOverlap": ["http", "client"],
      "matchedQueries": ["http client"],
      "whySuggested": "Actively maintained, very high adoption, and covers the same HTTP-client category."
    }
  ]
}
  • ›Combines maintainer-provided deprecation hints (parsed from the `deprecated` field) with deterministic npm-search category matching — it is not an LLM call.
  • ›Filters out typosquats and stale/weak contenders before ranking, so every suggestion is itself a reasonably healthy package.

Body params (application/json)
NameTypeRequiredDescription
packagesstring[]required2-5 exact npm package names, e.g. ["axios", "got", "node-fetch"].
Request
curl -X POST https://npmscan.com/api/analysis/compare-packages \
  -H "Content-Type: application/json" \
  -d '{"packages":["axios","got","node-fetch"]}'
Response200 OK
{
  "candidates": [
    { "name": "axios", "found": true, "weeklyDownloads": 65000000, "popularityTier": "very-high", "maintenanceTier": "active", "hasBuiltInTypes": true, "isLatestVersionVulnerable": false, "installScriptRisk": { "hasLifecycleScripts": false, "riskTier": "none", "scanScope": "lifecycle-scripts-only" }, "installSize": { "unpackedSize": 1983343, "transitive": { "transitiveUnpackedSize": 2271546, "transitiveDependencyCount": 12, "sizeUnknownCount": 1, "truncated": false } }, "score": 41 },
    { "name": "got", "found": true, "weeklyDownloads": 26000000, "popularityTier": "very-high", "maintenanceTier": "active", "hasBuiltInTypes": true, "isLatestVersionVulnerable": false, "installScriptRisk": { "hasLifecycleScripts": false, "riskTier": "none", "scanScope": "lifecycle-scripts-only" }, "installSize": { "unpackedSize": 448958, "transitive": { "transitiveUnpackedSize": 1553040, "transitiveDependencyCount": 19, "sizeUnknownCount": 0, "truncated": false } }, "score": 26 },
    { "name": "node-fetch", "found": true, "weeklyDownloads": 45000000, "popularityTier": "very-high", "maintenanceTier": "aging", "hasBuiltInTypes": true, "isLatestVersionVulnerable": false, "installScriptRisk": { "hasLifecycleScripts": false, "riskTier": "none", "scanScope": "lifecycle-scripts-only" }, "installSize": { "unpackedSize": 107319, "transitive": { "transitiveUnpackedSize": 9236710, "transitiveDependencyCount": 6, "sizeUnknownCount": 0, "truncated": false } }, "score": 20 }
  ],
  "differentiators": {
    "mostDownloads": "axios",
    "mostGithubStars": "axios",
    "hasTypeScriptSupport": ["axios", "got", "node-fetch"],
    "hasKnownVulnerabilities": [],
    "deprecated": [],
    "possibleTyposquat": [],
    "installScriptRiskFlagged": [],
    "smallestInstallSize": "got",
    "largestInstallSize": "node-fetch"
  },
  "recommendation": {
    "pick": "axios",
    "runnerUp": "got",
    "rationale": "axios: widely used (65,000,000 weekly downloads); actively maintained (published 12d ago); ships built-in TypeScript types; no known vulnerabilities in its latest version.",
    "confidence": "high"
  }
}
  • ›Shares its business logic with the compare_packages MCP tool via runComparePackages — same enrichment get_package uses, fanned out across candidates.
  • ›installScriptRisk is a lightweight, tarball-free scan of lifecycle script command strings only — call POST /api/analysis/install-script on a specific candidate for the deeper, file-content-aware scan.
  • ›installSize.unpackedSize is the candidate's own package size; installSize.transitive sums dist.unpackedSize across its resolved dependency tree (depth 2, capped at 60 nodes per candidate) — a small package can still have a large transitive footprint, so check both. sizeUnknownCount/truncated flag when that sum is partial rather than exact; call POST /api/analysis/transitive-dependencies on a specific candidate for the full graph.
  • ›A name that fails to resolve (typo, unpublished, malformed) still appears in `candidates` with `found:false` and `resolutionError` set rather than failing the whole request; duplicate names are rejected with a 400.

Body params (application/json)
NameTypeRequiredDescription
urlstringrequiredGitHub repository URL, e.g. "https://github.com/owner/repo".
refstringoptionalBranch, tag, or commit SHA to audit; omit to use the repository default branch.
includeDevDependenciesbooleanoptionalInclude package.json devDependencies — root and, for a monorepo, every merged workspace member. Default false; ignored when a pnpm/yarn lockfile is used instead.
policy{ allow?: string[], deny?: string[] }optionalSame shape as /api/analysis/license-compliance. Omit for the default policy (only copyleft/network-copyleft/proprietary are violations).
Request
curl -X POST https://npmscan.com/api/analysis/audit-github-repository \
  -H "Content-Type: application/json" \
  -d '{"url":"https://github.com/expressjs/express"}'
Response200 OK
{
  "summary": "Audited 28 dependencies from expressjs/express: 0 with known vulnerabilities, 0 license violation(s), 3 flagged install script(s).",
  "owner": "expressjs",
  "repoName": "express",
  "ref": "master",
  "defaultBranchUsed": true,
  "manifestPath": "package.json",
  "lockfilePath": null,
  "inputFormat": "package.json",
  "isMonorepo": false,
  "workspacePatterns": [],
  "workspacePackageCount": 0,
  "workspaceNote": null,
  "policy": { "mode": "default", "allow": [], "deny": [] },
  "findings": [
    {
      "name": "content-disposition",
      "requestedVersion": null,
      "resolvedVersion": "3.0.0",
      "npmscanUrl": "https://npmscan.com/package/content-disposition",
      "deprecated": null,
      "possibleTyposquatOf": null,
      "isVulnerable": false,
      "highestSeverity": null,
      "vulnerabilities": [],
      "rawLicense": "MIT",
      "licenseCategory": "permissive",
      "isLicenseCompliant": true,
      "licenseNeedsReview": false,
      "licenseViolation": null,
      "hasLifecycleScripts": true,
      "installScriptRiskTier": "low",
      "installScriptScore": 3,
      "installScriptScanScope": "deep-tarball-scan",
      "installScriptFindings": [
        { "rule": "lifecycle-present", "text": "Lifecycle scripts present", "points": 3, "note": "preinstall/install/postinstall/prepare detected in package.json or script files.", "locations": [{ "file": "package.json#scripts", "snippet": "prepare" }] }
      ],
      "resolutionError": null
    }
  ],
  "overflowPackages": [],
  "totalPackages": 28,
  "vulnerablePackageCount": 0,
  "licenseViolationCount": 0,
  "installScriptFlaggedCount": 3,
  "deepScannedCount": 3,
  "warnings": [],
  "truncationNote": null,
  "deepScanNote": null
}
Error — no package.json404 Not Found
{ "error": "No package.json found in \"owner/repo\" at ref \"main\"" }
  • ›Shares its business logic with the audit_github_repository MCP tool via runAuditGithubRepository.
  • ›Tries pnpm-lock.yaml, then package-lock.json, then yarn.lock, in that order — the first one found is used for exact resolved versions; falls back to package.json alone (ranges resolved against the registry, so requestedVersion comes back null and each package is checked at its current latest registry version) when none exist.
  • ›A monorepo (package.json#workspaces, Yarn's {packages:[...]} form, or pnpm-workspace.yaml) is detected automatically. pnpm-lock.yaml and yarn.lock already record every workspace member's dependencies directly; for a package-lock.json or no-lockfile repo, the repo's file tree is additionally listed, the declared glob patterns resolved to member directories, and each member's dependencies merged into the audit (capped at 50 member packages). See isMonorepo/workspacePatterns/workspacePackageCount/workspaceNote in the response.
  • ›Every dependency (up to 100 per call, across the root and any merged workspace members) gets a vulnerability check, a license-compliance verdict, and a tarball-free install-script signal. Up to 10 packages that actually declare a lifecycle script — prioritized by already-vulnerable, then possible-typosquat — additionally get the full tarball-fetching deep scan (installScriptScanScope: "deep-tarball-scan"); the rest are noted in deepScanNote.
  • ›This is the most expensive endpoint in the suite (a repo lookup, a handful of file fetches, up to 100 registry doc fetches, one OSV batch call, up to 10 tarball fetches, and — for a monorepo needing enumeration — one file-tree listing plus up to 50 more manifest fetches) — avoid calling it in a tight loop across many repos.

Body params (application/json)
NameTypeRequiredDescription
packagesArray<{ name, version? }>optional1-1000 items, capped to 100 when includeLicenses is on. Use this OR `content`, not both.
contentstringoptionalRaw package.json / lockfile / CycloneDX JSON / SPDX JSON content. Use this OR `packages`, not both.
format"cyclonedx" | "spdx"optionalDefault "cyclonedx".
includeDevDependenciesbooleanoptionalOnly applies when `content` is a manifest/lockfile format that distinguishes dev dependencies. Default false.
includeVulnerabilitiesbooleanoptionalQuery OSV.dev and embed findings natively. Default true.
includeLicensesbooleanoptionalResolve registry license data and embed it natively. Default true.
policy{ allow?: string[]; deny?: string[] }optionalSame shape as /api/analysis/license-compliance. Only affects the echoed policy/licenseViolationCount, never blocks generation.
componentNamestringoptionalName of the SBOM's own root component/document, if known — sets SPDX documentDescribes to the matching package.
componentVersionstringoptionalPaired with componentName.
Request
curl -X POST https://npmscan.com/api/analysis/generate-sbom \
  -H "Content-Type: application/json" \
  -d '{"packages":[{"name":"minimist","version":"1.2.5"}]}'
Response (truncated)200 OK
{
  "format": "cyclonedx",
  "sbom": {
    "bomFormat": "CycloneDX",
    "specVersion": "1.6",
    "serialNumber": "urn:uuid:62464a1d-22b7-4c8b-86e3-e7760f344b2f",
    "components": [
      { "bom-ref": "pkg:npm/minimist@1.2.5", "type": "library", "name": "minimist", "version": "1.2.5", "purl": "pkg:npm/minimist@1.2.5", "licenses": [{ "expression": "MIT" }] }
    ],
    "vulnerabilities": [
      {
        "id": "GHSA-xvch-5gv4-984h",
        "source": { "name": "GitHub Advisories", "url": "https://npmscan.com/vulnerability/GHSA-xvch-5gv4-984h" },
        "references": [{ "id": "CVE-2021-44906", "source": { "name": "NVD" } }],
        "ratings": [{ "source": { "name": "OSV" }, "severity": "critical" }],
        "recommendation": "Upgrade to 1.2.6 or later.",
        "affects": [{ "ref": "pkg:npm/minimist@1.2.5" }],
        "analysis": { "state": "in_triage" }
      }
    ]
  },
  "inputFormat": "packages",
  "parsedPackageCount": 1,
  "policy": { "mode": "default", "allow": [], "deny": [] },
  "totalVulnerabilities": 1,
  "packagesWithVulnerabilities": 1,
  "licenseViolationCount": 0
}
Error — neither packages nor content given400 Bad Request
{ "error": "Provide either \"packages\" or \"content\"" }
  • ›Shares its business logic with the generate_sbom MCP tool via runGenerateSbom.
  • ›CycloneDX gets a top-level `vulnerabilities[]` array (one entry per unique advisory id, `affects[]` listing every impacted component, `analysis.state` always "in_triage") and per-component `licenses[]`. SPDX 2.3 has no vulnerabilities array of its own, so each finding becomes a `packages[].externalRefs[]` entry (`referenceCategory: "SECURITY"`, `referenceType: "advisory"`) instead, alongside the native `licenseDeclared`/`licenseConcluded` fields.
  • ›A package with no OSV findings gets no vulnerability entry at all — no fabricated "clean" record. The CycloneDX `dependencies[]` transitive graph and any SPDX package hierarchy are intentionally omitted: this only has a flat inventory, not resolved edges between packages.
  • ›`sbom` is validated against the official CycloneDX 1.6 / SPDX 2.3 JSON Schemas in npmscan's own test suite.

Body params (application/json)
NameTypeRequiredDescription
contentstringrequiredRaw `npm audit --json` stdout — either npm 7+'s {"vulnerabilities": {...}} format (auditReportVersion 2) or legacy npm 6's {"advisories": {...}}.
Request
curl -X POST https://npmscan.com/api/analysis/enrich-npm-audit \
  -H "Content-Type: application/json" \
  -d '{"content":"{\"auditReportVersion\":2,\"vulnerabilities\":{\"minimist\":{\"name\":\"minimist\",\"severity\":\"critical\",\"isDirect\":true,\"via\":[{\"source\":1179,\"name\":\"minimist\",\"url\":\"https://github.com/advisories/GHSA-xvch-5gv4-984h\",\"title\":\"Prototype Pollution in minimist\",\"severity\":\"critical\"}],\"fixAvailable\":true}}}"}'
Response200 OK
{
  "inputFormat": "npm-audit-v2",
  "totalFindings": 1,
  "uniqueCveCount": 1,
  "ghsaResolvedToCveCount": 1,
  "summary": { "removeNow": 0, "patchNow": 0, "patchSoon": 1, "scheduled": 0, "monitor": 0, "kevListedCount": 0 },
  "ranked": [
    {
      "rank": 1,
      "packageName": "minimist",
      "cveId": "CVE-2021-44906",
      "advisoryId": "GHSA-xvch-5gv4-984h",
      "advisoryTitle": "Prototype Pollution in minimist",
      "currentVersion": null,
      "fixedVersion": null,
      "severity": "CRITICAL",
      "kev": null,
      "epss": { "score": 0.021, "percentile": 0.88, "date": "2024-01-15" },
      "score": 12.6,
      "tier": "scheduled",
      "findingType": "vulnerability",
      "reason": "Low predicted exploitation probability (EPSS 2.1%), CRITICAL severity.",
      "npmscanUrl": "https://npmscan.com/package/minimist",
      "cveNpmscanUrl": "https://npmscan.com/vulnerability/CVE-2021-44906",
      "isDirect": true,
      "fixAvailable": true,
      "fixTarget": null
    }
  ],
  "warnings": [],
  "skippedCount": 0
}
Error — a clean audit (nothing to rank)400 Bad Request
{ "error": "No advisory-bearing packages found in this npm audit report — metadata.vulnerabilities.total may be 0 (a clean audit has nothing to rank)." }
  • ›Shares its business logic with the enrich_npm_audit MCP tool via runEnrichNpmAudit — which itself composes parseNpmAuditJson with runPrioritizeRemediation, the same function POST /api/analysis/prioritize-remediation calls directly.
  • ›npm audit JSON almost never includes a CVE id itself, only a GHSA advisory URL — each GHSA is resolved to a CVE alias via OSV.dev first (ghsaResolvedToCveCount reports how many); a GHSA with no CVE alias falls back to severity-only ranking, same as prioritize-remediation's own documented behavior for a missing cveId.
  • ›A package whose `via` array is only chain pointers to another package's own advisory (npm v2 format) contributes no separate finding — counted in `skippedCount`, not dropped silently.
  • ›`fixTarget` can name a different package than the vulnerable one (e.g. bumping a parent to pull in a patched transitive dependency) — its version is only ever surfaced as `fixedVersion` when it matches the vulnerable package itself.
  • ›`yarn audit --json` and `pnpm audit --json` use different report shapes and are not supported — parse those as a lockfile instead via /api/analysis/... endpoints that accept `content`.
  • ›A `MAL-*`-shaped advisory id in the audit report is auto-detected as malware and forces `tier: "remove-now"` ahead of every other signal — same behavior as /api/analysis/prioritize-remediation's `findingType`.

Live XML feeds for anyone who'd rather subscribe in a feed reader, Slack/Discord RSS bridge, or SIEM than poll a REST endpoint. These live off the site root, not under /api.

Request
curl "https://npmscan.com/latest-vulnerabilities/rss.xml"
Response200 OK — application/rss+xml
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>NPMSCan - Latest NPM Vulnerabilities</title>
  <link>https://npmscan.com/latest-vulnerabilities</link>
  <atom:link href="https://npmscan.com/latest-vulnerabilities/rss.xml" rel="self" type="application/rss+xml" />
  <description>Live feed of reviewed security advisories affecting the npm ecosystem (GitHub Advisory Database).</description>
  <ttl>60</ttl>
  <item>
    <title>GHSA-xxxx-xxxx-xxxx (CVE-2024-00000): Remote code execution via crafted input</title>
    <link>https://npmscan.com/vulnerability/GHSA-xxxx-xxxx-xxxx</link>
    <atom:link href="https://npmscan.com/vulnerability/GHSA-xxxx-xxxx-xxxx/rss.xml" rel="related" type="application/rss+xml" />
    <guid isPermaLink="false">GHSA-xxxx-xxxx-xxxx</guid>
    <pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate>
    <description>severity: critical | packages: example-package | rss: https://npmscan.com/vulnerability/GHSA-xxxx-xxxx-xxxx/rss.xml | source: https://github.com/advisories/GHSA-xxxx-xxxx-xxxx</description>
  </item>
</channel>
</rss>
  • ›`<ttl>60</ttl>` — feeds are effectively live; a reader polling once a minute won't miss anything.
  • ›Each item links to its own per-vulnerability feed via an atom:link rel="related", so a reader can offer "subscribe to just this CVE" from the main feed.

Query / route params
NameTypeRequiredDescription
idstringrequiredAn OSV-recognized id (GHSA ids reliably resolve). This hits OSV.dev's /v1/vulns/:id directly — unlike /api/advisories/:id, it doesn't go through GitHub's API or accept a bare CVE id.
Request
curl "https://npmscan.com/vulnerability/GHSA-35jh-r3h4-6jhm/rss.xml"
Response200 OK — application/rss+xml
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>NPMSCan - GHSA-35jh-r3h4-6jhm</title>
  <link>https://npmscan.com/vulnerability/GHSA-35jh-r3h4-6jhm</link>
  <ttl>1440</ttl>
  <item>
    <title>GHSA-35jh-r3h4-6jhm: Prototype Pollution in lodash</title>
    <link>https://npmscan.com/vulnerability/GHSA-35jh-r3h4-6jhm</link>
    <guid isPermaLink="false">GHSA-35jh-r3h4-6jhm</guid>
    <pubDate>Wed, 15 Jul 2020 00:00:00 GMT</pubDate>
    <category>high</category>
    <category>CVE-2020-8203</category>
    <description>id: GHSA-35jh-r3h4-6jhm
aliases: CVE-2020-8203
severity: high
published: 2020-07-15T00:00:00Z

affected:
- lodash (&lt; 4.17.19)

references:
- ADVISORY: https://github.com/advisories/GHSA-35jh-r3h4-6jhm</description>
  </item>
</channel>
</rss>

Repo and maintainer signals pulled live from the GitHub API — stars, recent activity, and account age are useful trust signals alongside the package data above.

Query / route params
NameTypeRequiredDescription
urlstringrequiredFull GitHub repo URL, e.g. https://github.com/owner/repo.
Request
curl "https://npmscan.com/api/github/stars?url=https://github.com/lodash/lodash"
Response200 OK
{ "stars": 59892 }
Error — bad url400 Bad Request
{ "error": "Invalid GitHub repository URL" }

Query / route params
NameTypeRequiredDescription
urlstringrequiredFull GitHub repo URL.
Request
curl "https://npmscan.com/api/github/repo/commits?url=https://github.com/lodash/lodash"
Response200 OK
{
  "commits": [
    {
      "sha": "a1b2c3d",
      "html_url": "https://github.com/owner/repo/commit/a1b2c3d",
      "message": "Fix regression in X",
      "author_login": "octocat",
      "author_name": "The Octocat",
      "author_avatar_url": "https://avatars.githubusercontent.com/u/1?v=4",
      "date": "2024-05-01T12:00:00Z"
    }
  ]
}
  • ›Last 10 commits.

Query / route params
NameTypeRequiredDescription
urlstringrequiredFull GitHub repo URL.
Request
curl "https://npmscan.com/api/github/repo/contributors?url=https://github.com/lodash/lodash"
Response200 OK
{
  "contributors": [
    { "login": "octocat", "contributions": 482, "html_url": "https://github.com/octocat", "avatar_url": "https://avatars.githubusercontent.com/u/1?v=4" }
  ]
}
  • ›Top 10 by contribution count.

Query / route params
NameTypeRequiredDescription
urlstringrequiredFull GitHub repo URL.
Request
curl "https://npmscan.com/api/github/repo/issues?url=https://github.com/lodash/lodash"
Response200 OK
{
  "issues": [
    {
      "number": 123,
      "title": "Memory leak on large payloads",
      "html_url": "https://github.com/owner/repo/issues/123",
      "state": "open",
      "labels": ["bug"],
      "author_login": "someuser",
      "author_avatar_url": "https://avatars.githubusercontent.com/u/2?v=4",
      "created_at": "2024-04-20T09:00:00Z",
      "updated_at": "2024-04-25T09:00:00Z"
    }
  ]
}
  • ›Up to 10 issues, sorted by most recently updated.

Query / route params
NameTypeRequiredDescription
usernamestringoptionalGitHub username. Provide this or `url`.
urlstringoptionalA repo URL to derive the owner from, e.g. https://github.com/owner/repo.
Request
curl "https://npmscan.com/api/github/user?username=sindresorhus"
Response200 OK
{
  "login": "sindresorhus",
  "name": "Sindre Sorhus",
  "html_url": "https://github.com/sindresorhus",
  "avatar_url": "https://avatars.githubusercontent.com/u/170270?v=4",
  "created_at": "2010-03-01T12:00:00Z",
  "public_repos": 1000,
  "followers": 50000,
  "following": 50,
  "blog": "https://sindresorhus.com",
  "twitter_username": "sindresorhus",
  "company": null,
  "location": "Oslo, Norway",
  "bio": null
}
  • ›A brand-new account with one popular package is a very different risk profile than a maintainer active for a decade — this endpoint is what powers that check.