npm Scraper - Package Statistics, Downloads & Dependencies
Pricing
from $1.00 / 1,000 run start fees
npm Scraper - Package Statistics, Downloads & Dependencies
Look up npm packages by name or search the registry. Every row carries version, licence, dependencies, release history, maintainers, deprecation flags and real download counts — one table for dependency audits, licence checks and ecosystem research. No key, no login.
Pricing
from $1.00 / 1,000 run start fees
Rating
0.0
(0)
Developer
SR
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
16 days ago
Last modified
Categories
Share
This npm scraper turns a list of package names — or one registry search — into a clean table of package statistics: version, licence, declared dependencies, release history, maintainers, deprecation flags and real download counts. Use it for dependency audits, licence checks and ecosystem research, then export the rows as JSON, CSV or Excel. No key, no login: it reads npm's own public APIs.
What the npm scraper does
The question it answers that a package page does not: is this dependency actually alive?
request is the example worth keeping in mind. It has been deprecated for years. It still
returns a complete package document, a valid version, a licence and a repository link — and
59,191,622 downloads in the last month, a real figure from the run below. Nothing about
the response looks wrong.
npm never deletes a package, it flags it. A scraper that reads the document and reports what
it finds will tell you a dead dependency is healthy. This Actor lifts deprecated and
deprecated_reason onto every row, and the run summary counts how many of your packages are
flagged. The companion signal is days_since_last_release: sort your dependency tree by it
and what has been abandoned under you becomes obvious.
Three endpoints, because each holds something the others do not
| Endpoint | What only it has |
|---|---|
registry.npmjs.org/<pkg> | Every version, dependencies, licence, maintainers, release dates |
registry.npmjs.org/-/v1/search | The composite score and the dependents count |
api.npmjs.org/downloads | Actual download numbers |
So the two modes return different columns, and the run summary says which mode you are in rather than leaving you to wonder why a field is empty.
By name returns the full document. This is the package statistics path: dependencies, release history, deprecation, tarball and dist-tags.
packages: ["express", "lodash", "request"]
By search returns scores and dependents, which the package document does not carry.
search: "http client"
Download counts are added in either mode, one extra request per package.
A note on npm's scores
npm publishes a score object with a composite final value and a detail breakdown of
quality, popularity and maintenance. The breakdown is dead: in the search results this Actor
was measured against, the composites ran from 414.12 to 663.79 while quality, popularity and
maintenance came back as exactly 1 for every single package. Three columns that are always 1
look like signal and are not, so this Actor does not return them.
What it does return is score, which genuinely varies, and dependents — the count of
packages depending on this one. npm sends that number as text, so it arrives as text
("45" for a niche HTTP client), not as an integer.
Input
| Field | Type | Default |
|---|---|---|
packages | list of exact names, scoped names included | no default; the editor prefills ["express","lodash","request"] |
search | registry search term | — |
include_downloads | fetch real download counts | true |
downloads_period | last-day, last-week, last-month | last-month |
limit | 1–2000 | 50 |
retries | 1–6 | 3 |
cursor | continuation from next_cursor | — |
Fill packages or search. When both are filled, packages wins; search is only read
when the package list is empty. A single string split on commas, semicolons or spaces also
works as a package list. downloads_period is validated even when include_downloads is
false — only its use depends on that flag — and it chooses the field name you get back:
downloads_day, downloads_week or downloads_month.
limit is clamped to 1–2000 and also caps the package list before anything is fetched, so a
prefill of 200 names with limit: 10 returns 10 rows. retries counts backoff attempts per
request: a 404 is definitive and is not retried, a 500 or timeout is.
Scoped names (@types/node, @actions/http-client) work everywhere a plain name does. The
slash is preserved through the request, which is the detail that most quickly-written npm
scrapers get wrong: a naive encode turns @types/node into %40types%2Fnode and the
registry answers 404.
Output
One dataset row per package. This is a real row, from a run on the prefilled input
(express, lodash, request) with downloads for the last month:
{"name": "request","version": "2.88.2","description": "Simplified HTTP request client.","license": "Apache-2.0","homepage": "https://github.com/request/request#readme","repository": "https://github.com/request/request","keywords": ["http", "simple", "util", "utility"],"dependencies": ["qs", "aws4", "uuid", "extend", "caseless", "isstream", "aws-sign2", "form-data", "mime-types", "oauth-sign", "safe-buffer", "tough-cookie", "tunnel-agent", "forever-agent", "har-validator", "is-typedarray", "http-signature", "combined-stream", "performance-now", "json-stringify-safe"],"dependency_count": 20,"dev_dependency_count": 20,"peer_dependency_count": 0,"engines": {"node": ">= 6"},"maintainers": ["fredkschott", "mikeal", "nylen", "simov"],"version_count": 126,"first_release_at": "2011-01-22 00:36:12","last_release_at": "2020-02-11 16:35:36","days_since_last_release": 2430,"deprecated": true,"deprecated_reason": "request has been deprecated, see https://github.com/request/request/issues/3142","tarball": "https://registry.npmjs.org/request/-/request-2.88.2.tgz","unpacked_size_bytes": 209281,"dist_tags": ["latest"],"url": "https://www.npmjs.com/package/request","downloads_month": 59191622,"position": 2,"fetched_at": "2026-10-07T03:23:49Z"}
Fields
- Identity:
name(scoped names included),version,description,keywords,url - Legal:
license - Links:
homepage,repository(normalised fromgit+https://….gitto a plain https URL) - Dependencies:
dependencies,dependency_count,dev_dependency_count,peer_dependency_count,engines - People:
maintainers,publisher - History:
version_count,first_release_at,last_release_at,days_since_last_release - Health:
deprecated,deprecated_reason - Artefact:
tarball,unpacked_size_bytes,dist_tags - Popularity:
downloads_month(or_week/_day),score,dependents - Run:
position(1..n across the whole job),fetched_at
The two modes do not return the same set. Name mode gives the full document above; search
mode gives publisher, score, dependents and downloads_weekly instead of the dependency
block. Fields a mode does not have are absent from the row, not null, so a missing value
always means "not applicable here" rather than "empty".
license comes back only when npm publishes it as a plain string. Some packages declare a
licence as an object or an SPDX expression array, and those arrive null rather than being
flattened into something that looks canonical and is not. Rows in name mode come back in the
order the registry answered, not necessarily the order you typed.
Summary, errors and empty results
Besides the rows, the run writes a summary record with the keys a report needs:
packagesReturned, mode (package or search), totalMatches plus a samplingNote when
the registry matched more than you asked for, deprecated with a deprecatedNote when it is
above zero, unmaintainedOver2Years (nothing released in the last 730 days), withDownloads,
downloadsPeriod, fetchedInSeconds and errorCount (always, next to rows as well as on an
empty run). Failures land in a separate errors
record with the source codes (bad_input, not_found, fetch_failed, no_results) so you
can tell "you typed something wrong" from "the registry said no".
On a free plan, a run that fills the 10-result cap also writes an OUTPUT record saying the
limit was reached, so the stopped-at-10 case is visible in the run rather than only in the row
count.
A name that does not exist returns a not_found error naming it, never a row of nulls. A run
with nothing to return finishes successfully with an empty dataset and no row charge.
Large jobs
Package documents are heavy: the typescript package document alone measured 15.4 MB and 19
seconds to fetch. A run that reaches its time budget stops cleanly and returns a
next_cursor; pass that value back as cursor with the same input and the job continues where
it stopped, so a list of thousands of packages is a series of runs rather than one that
quietly truncates.
Use cases
- Dependency audit. Paste the dependencies from your
package.jsonand get every licence, deprecation flag and last-release date back in one table. - Supply-chain review.
dependency_countand thedependencieslist show how much weight a package drags in;maintainersshows how many people can publish a release. - Abandonment check. Sort by
days_since_last_releaseand filter ondeprecated. - Ecosystem research. Search a term and rank by
dependentsto see what the ecosystem actually builds on rather than what markets itself best. - Package selection. Compare candidates on downloads, dependents, dependency weight and release recency in a single run.
- Download tracking. Re-run the same list on a schedule and keep the datasets to follow download counts over time for your own packages or a competitor's.
npm scraper compared with the alternatives
| This Actor | npm Registry API yourself | npm-stat, npmtrends & friends | Generic web scraper | |
|---|---|---|---|---|
| Setup | Paste names or a search term | Write three clients, retry and rate-limit code | One package at a time in a browser | Configure selectors per page |
| Columns in one row | Document, score, dependents and downloads together | One endpoint each, you join them | Downloads, or metadata, rarely both | Whatever the page renders |
| Deprecation | deprecated plus the reason, counted in the summary | Present in the document, you extract it | Usually absent | Often invisible |
| Output | Flat rows, JSON/CSV/Excel, ready to sort | Nested JSON per endpoint | Charts on screen | Depends on your selectors |
| Credentials | None | None | None | None |
If you already run your own integration against the registry API, you do not need this. If you want one table of package statistics in a minute — with deprecation, downloads and dependents already on the same row — this is quicker than assembling three endpoints by hand, and far sturdier than scraping package pages.
Pricing
The price is $0.001 per run (run_start) plus $0.001 per package row (package). A
run that returns nothing only incurs the run-start fee, and an empty result is free beyond
that. Users on a free Apify plan receive at most 10 package results per run; you can use a
lower limit to request fewer.
As an example: a dependency review of 200 packages costs $0.001 + 200 × $0.001 = $0.201.
FAQ
Do I need an npm token or API key to use this npm scraper?
No. Everything comes from npm's public endpoints, so there is no account to create and no key to paste. Rate limits are npm's, and the Actor retries politely before it reports anything.
Why are some licences empty?
npm publishes a licence as a plain string for most packages, but some declare an object or an
SPDX expression array. Those come back null instead of being flattened into a value that
looks canonical and is not.
What happens when a package does not exist?
The run finishes successfully and the errors record names the missing package with
not_found. You never get a row of nulls that you have to filter out later. A 404 is
definitive, so it is not retried; a 500 or a timeout is retried with backoff first.
Does the npm scraper return the full dependency tree?
No. You get a package's own declared dependencies, not the resolved tree beneath them. Feed
the dependency names back in as packages — dependency_count tells you which ones are worth
the next level — and walk the tree one layer per run.
Can I follow a very large job across several runs?
Yes. When a run reaches its time budget it returns a next_cursor; send it back as cursor
with unchanged input and the job continues. The summary reports what was delivered so far, and
rows keep their position across the pieces.
Does it return download history over time?
No. Each run returns the download count for the period you asked for (last-day,
last-week or last-month). Schedule the Actor and keep the datasets to build your own
history.