The registry serves the npm registry protocol. The npm CLI, bun, and other npm clients use it without changes. This page lists each route, the rules that each route applies, and the features that are not available yet.
The examples use example.com for the site. In local development, the site is localhost:9876, and the other hosts are its subdomains, such as registry.localhost:9876.
Hosts#
| Host | Serves |
|---|---|
example.com |
The website. |
registry.example.com |
The public registry. |
SLUG.registry.example.com |
A private registry. It serves only the scopes that are bound to it. |
api.example.com |
The downloads API for every registry, with the paths of api.npmjs.org. |
A request to any other host gets 404. The slug public is reserved.
Authentication#
Send a token with Authorization: Bearer TOKEN. A token starts with nextpm_. A password never works as a credential, so HTTP Basic authentication gets 401: a password can be phished, and stuffed from other sites' leaks.
No token can write alone. Each write of an npm login token waits for its owner's passkey (see Approvals). A token from settings or npm token create, and an Org token, can only read. CI publishes with trusted publishing.
A request with an invalid or revoked token gets 401 with www-authenticate: Bearer. This also applies to reads of public packages. The login routes are the only exception. A token revoke or an Org membership change applies at the next request.
Errors use the npm form, { "error": "message" }. A private package that you cannot read gets 404, the same as a package that does not exist.
Approvals#
Each write of an npm login token, such as a publish, a dist-tag, an owner change, or npm trust, waits for a passkey approval of that exact request. This is npm's web two-factor flow, so npm 9 and later, pnpm, and Bun handle it in a terminal. Yarn asks for a typed code, so it cannot write. The steps:
- The write gets
401withwww-authenticate: OTPand{ error, authUrl, doneUrl }. - npm prints
authUrl, a website page that shows the change, where it came from, and a warning when the browser is on another network. The person approves it with their passkey, or refuses it. - npm polls
doneUrl, with any method:202withretry-after: 1while it waits, then200with{ "token" }once, or404when it was refused or expired. - npm sends the write again with
npm-otp: TOKEN. The code works once, only for the same token, method, path, and body, within 5 minutes of the request. Any other request with it gets401.
A token can have 20 writes waiting at once; the next gets 429. Some writes need no approval: npm logout, a staged publish (its approval comes later), audits, and the tarball delete that npm sends after an unpublish, which changes nothing.
A website session approves its package changes the same way: trusted publishers, staged versions, and whether CI releases wait. Its account changes use a 10-minute window that a passkey opens instead (see Account).
| Method | Path | Purpose |
|---|---|---|
GET |
/-/v1/approvals/:id |
{ id, action, origin, registry, created, expires } of an approval that waits. action lists the change in words; origin has the requester's ip and agent. |
POST |
/-/v1/approvals/:id/options |
{ publicKey } for navigator.credentials.get(), with a challenge that names this approval only. |
POST |
/-/v1/approvals/:id |
Approve with { credential }. When the website session asked for the write itself, the answer has { otp, replay }, so the website can send the write again. |
DELETE |
/-/v1/approvals/:id |
Refuse it. |
| any | /-/v1/approvals/done/:user/:done |
npm's doneUrl. |
The first four routes are for the website session of the token's owner. An approval of another account answers 404.
Configure npm#
registry=https://registry.example.com/
//registry.example.com/:_authToken=${NPM_TOKEN}
@acme:registry=https://acme-internal.registry.example.com/
//acme-internal.registry.example.com/:_authToken=${NPM_TOKEN}The second pair of lines sends the @acme scope to a private registry. The same user token works on each host. A token in CI only reads, to install private packages; run npm login to publish from a terminal.
Packages#
A package name in a path can be name, @scope%2fname, or @scope/name.
| Method | Path | Purpose |
|---|---|---|
GET, HEAD |
/:name |
The full packument. Send accept: application/vnd.npm.install-v1+json to get the abbreviated packument. |
GET |
/:name/:spec |
One version manifest, by exact version or dist-tag. A semver range gets 404, as on npmjs. |
GET, HEAD |
/:name/-/:file.tgz |
The tarball. Each GET with status 200 adds one download. |
PUT |
/:name |
Publish, when the body has _attachments. Otherwise, update the document (see below). |
PUT |
/:name/-rev/:rev |
Update the document. :rev must be the current _rev, or the request gets 409. |
DELETE |
/:name/-rev/:rev |
Unpublish every version. |
DELETE |
/:name/-/:file/-rev/:rev |
Accepted for the npm CLI. The version unpublish already deleted the tarball. |
Packuments#
The full packument has the same keys and key order as npmjs: _id, _rev, name, dist-tags, versions, time, maintainers, the root fields of the latest version, readme, and readmeFilename. Each version has _id, _npmUser, maintainers, directories, _hasShrinkwrap, and a dist with integrity, shasum, tarball, fileCount, unpackedSize, and signatures. The registry moves readme from the version to the root.
The abbreviated packument has only name, dist-tags, versions, and modified. Each version has only the install fields. Unlike npmjs, it also keeps libc.
Both packuments send etag, last-modified, and vary: accept-encoding, accept. A matching if-none-match gets 304. A public package sends cache-control: public, max-age=300. A restricted package sends cache-control: private, max-age=0, must-revalidate. ?write=true always returns the full packument.
Publish#
The body is the document that npm publish sends. The registry checks each tarball against its manifest and the dist hashes that the client sent. Then it stores the tarball and adds the registry fields and signatures.
- A user must confirm their email address before they publish. Until then, a publish gets
403. A trusted publisher and an Org token are not users, so the rule does not apply to them. - An unscoped package is public. Only a user token can create one, and only on the public registry.
- A scoped package needs the scope owner: the user with the scope's name, or a member of the Org with the scope's name. An Org token cannot publish: Org CI publishes with trusted publishing.
- A new scoped package is restricted, unless the body has
"access": "public". A package in a private registry is always restricted. - After the package exists, its maintainers and, for an Org scope, the Org members can publish.
- A version number cannot be used again, also after an unpublish. The response is
403. - When the package has no
latesttag, the first published version gets it. - A body with a
<name>-<version>.sigstoreattachment, asnpm publish --provenancesends, must hold a valid Sigstore bundle v0.3 with SLSA provenance v1 for that tarball. The registry checks the signature, the Fulcio certificate chain, the certificate transparency timestamp, and the Rekor entry against the public Sigstore root. The build must be from a public GitHub repository on a GitHub-hosted runner, and the repository must matchrepositoryinpackage.json. An invalid bundle gets422.
Trusted publishing#
A GitHub Actions job can publish without a stored token, as on npmjs. npm 11.5.1 and later does this by itself. The job sends its OIDC identity token, with the audience npm:<registry host>, to POST /-/npm/v1/oidc/token/exchange/package/:name. When the token matches a trusted publisher of the package, the response is 201 with { "token" }: a token that can publish only this package, for 15 minutes. It cannot change tags, owners, access, or other packages. Each identity token can be exchanged once. A version from trusted publishing has _npmUser set to GitHub Actions, with trustedPublisher: { id, oidcConfigId }.
A trusted publisher matches the token's issuer, its repository_owner_id, its repository_id, its workflow file, and its environment. The environment is required, so that its protection rules in the repository guard each publish. It matches no names, so a renamed repository keeps working and a new repository with an old name never matches. A provenance bundle in a trusted publish must name the same repository_id, or the publish gets 403.
The package's website settings, and npm trust, add and remove trusted publishers, and a passkey approves each change. The repository must belong to the person's linked GitHub account, or to the GitHub organization linked to the Org that owns the package. The publisher pins that owner's id. A public repository also pins its id at once; a private one pins it on its first exchange. A scoped name that is not published yet can have trusted publishers too, from the members of its scope. Its first trusted release always stages. Once a member of its scope approves it, it creates the package, with the person who added the publisher as its maintainer.
| Method | Path | Purpose |
|---|---|---|
GET |
/-/package/:name/trust |
The trusted publishers, as [{ id, type, claims, issuer, owner, repository, creator, created }]. repository is null until the first exchange of a private repository. For the package's maintainers, or before the first publish, the members of its scope. |
POST |
/-/package/:name/trust |
Add one: { "type": "github", "claims": { "repository": "owner/repo", "workflow_ref": { "file": "release.yml" }, "environment": "npm" }, "permissions"?: ["createPackage", "createStagedPackage"] }, or an array of one, as npm trust github sends. createPackage lets it publish when the package lets CI publish with no approval; createStagedPackage lets it stage. Both by default. |
DELETE |
/-/package/:name/trust/:id |
Remove one. Its unexpired tokens stop working. |
GET /-/v1/account/pending and GET /-/org/:org/pending list the names of a scope that have trusted publishers and wait for their first publish.
The token must also come from a push, release, workflow_dispatch, or schedule event. A pull request, a comment, or a workflow_run can run code that an outsider controls, so their tokens get 401. The runner must be GitHub-hosted (runner_environment: github-hosted).
The registry trusts https://token.actions.githubusercontent.com. A deployment can trust other issuers, such as GitHub Enterprise Server, with OIDC_ISSUERS: {"https://ghes.acme.dev/_services/token":{"provider":"github","runners":"self-hosted"}}. Their runners must be hosted too, unless the entry sets "runners": "self-hosted". An entry cannot change the built-in issuer. Only github.com publishers can be added for now, because a linked GitHub account vouches only for github.com ids.
Staged publishing#
Each release from a trusted publisher waits in the package's stage until a maintainer approves it with a passkey, so a workflow that someone changed cannot ship a version alone. A maintainer can turn this off for a package, on its Publishing page. A direct trusted publish of a package that stages gets 403; run npm stage publish instead. A person can stage a version too, with no approval yet. The first version of a name outside the person's scopes cannot stage: nobody could approve it yet. Approving a stage checks the stager's current rights again, and only a new package takes the access that its stage asks for; the approval page names that access. A trusted publish never changes the access of a package that exists.
| Method | Path | Purpose |
|---|---|---|
POST |
/-/stage/package/:name |
npm stage publish: the publish body of one version. 201 with { stageId }. The same checks as a publish run, and run again at approval. A package holds at most 20 staged versions. |
GET |
/-/stage?package=&page=&perPage= |
{ items, total }: the staged versions that the caller may approve, or those of one package. |
GET |
/-/stage/:id |
One staged version: { id, packageName, version, tag, createdAt, actor, actorType, shasum, integrity, status }. |
GET |
/-/stage/:id/tarball |
Its tarball, for review. |
POST |
/-/stage/:id/approve |
Publish it, after a passkey approves this. A removed trusted publisher cannot go live. |
DELETE |
/-/stage/:id |
Reject it, after a passkey approves this. Its tarball goes away. |
GET |
/-/package/:name/staging |
{ required }: whether CI releases wait. |
POST |
/-/package/:name/staging |
Change it: { "required": false }. A passkey approves this. |
Attestations#
GET /-/npm/v1/attestations/:name@:version returns { attestations: [{ predicateType, bundle }] } for a version with provenance, as npmjs does. Such a version has dist.attestations with url and provenance.predicateType. The registry does not add its own publish attestation yet, because that needs an entry in the public Rekor log.
Document updates#
A PUT without attachments changes only these fields. npm sends the whole document back, so the other fields can come back unchanged. A changed value of any other field gets 400, and the error names the field. The registry never ignores a change.
| Field | Effect |
|---|---|
versions |
A live version that is not in the body is unpublished. A changed deprecated value deprecates the version. An empty string removes the deprecation. |
dist-tags |
Replaces the tags. A tag that points to a version that is not published gets 400. |
maintainers |
Replaces the maintainers. Each name must be an existing user, and the list cannot be empty. |
A published version's manifest cannot change, apart from deprecated. A changed manifest gets 403 with "Changing published version metadata is not allowed", as npm's registry answered. A version that is not published gets 400; publish it with its tarball. A body that removes every version gets 400. Use DELETE /:name/-rev/:rev instead. A body with only users gets 400, because stars are not available.
Package settings#
| Method | Path | Purpose |
|---|---|---|
GET |
/-/package/:name/dist-tags |
The tags, such as { "latest": "1.2.0" }. |
PUT |
/-/package/:name/dist-tags/:tag |
Point a tag at a version. The body is a JSON string, such as "1.2.0". A tag cannot be a semver range. |
DELETE |
/-/package/:name/dist-tags/:tag |
Remove a tag. latest cannot be removed. |
GET |
/-/package/:name/visibility |
{ "public": true } or { "public": false }. |
POST |
/-/package/:name/access |
Set { "access": "public" } or { "access": "restricted" }. Only a scoped package can change. A package in a private registry cannot become public. |
GET |
/-/package/:name/collaborators |
The maintainers and, for an Org scope, the Org members, each with write. |
GET |
/-/package/:name/readme/:version |
{ readme, readmeFilename } of one version. The packument has only the readme of latest. This route is not part of the npm protocol. |
Accounts#
| Method | Path | Purpose |
|---|---|---|
GET |
/-/ping |
{}. |
GET |
/-/whoami |
{ "username": "alice" }. An Org token gives the Org name. |
GET |
/-/npm/v1/user |
The profile of the signed-in user. email_verified is true after the user confirms the address. |
PUT |
/-/user/org.couchdb.user:name |
Legacy npm adduser and npm login --auth-type=legacy: always 403. A password cannot sign in the CLI. |
GET |
/-/user/org.couchdb.user:name |
{ name, email } of a user. A signed-in caller only. |
POST |
/-/v1/login |
Start a web login. The loginUrl opens the website's sign-in page, or its sign-up page for { "create": true }. |
GET |
/-/v1/done?sessionId=ID |
The CLI polls here: 202 with retry-after: 2, then 200 with { "token" } one time. |
GET |
/-/login/:id |
{ create, origin, created } while a web login waits, else 404. origin has the CLI's ip and agent, for the person who approves it. |
POST |
/-/login/:id/options |
{ publicKey } for navigator.credentials.get(), with a challenge that names this login only. A website session only. |
POST |
/-/login/:id/approve |
Approve the login with { credential }, from the signed-in user's passkey. The CLI then gets a token that lives 30 days, and whose writes wait for approvals. |
POST |
/-/v1/session |
Sign in to the website: { name, password }, or sign up with { name, password, email, "create": true }. 201 with { token, name, expires }. |
DELETE |
/-/v1/session |
Sign out: revoke the website session token that the request sends. |
POST |
/-/v1/email/confirm |
Confirm an address with the code from its link: { name, code }. 410 when the code is wrong, used, or older than 1 hour. No token is needed. |
POST |
/-/v1/session/passkey/options |
{ publicKey }: the options for navigator.credentials.get() to sign in with a passkey. The challenge is sealed with an HMAC and lives 5 minutes. |
POST |
/-/v1/session/passkey |
Sign in with { credential }, whose user handle names the account. 201 with { token, name, expires }. Each answer works once, and the new session is approved for 10 minutes. |
GET |
/-/v1/profile/:name |
The public profile of a user or an Org, with its packages and their weekly downloads. |
POST |
/-/v1/github |
Link the signed-in user's GitHub account: { code, redirect_uri } from a GitHub OAuth sign-in. |
DELETE |
/-/v1/github |
Unlink it. |
Account#
These routes are for website sessions only. The website's origin and host, from WEB_URL, are the passkey origin and relying party.
| Method | Path | Purpose |
|---|---|---|
GET |
/-/v1/account |
{ name, fullName, email, created, github, passkeys, recovery, sudo, verified, pending, packageMail }. recovery counts the codes left; sudo is when this session's approval ends; pending is a new address that waits for its link. |
PATCH |
/-/v1/account |
Change { fullName, email, packageMail }. A new email waits in pending until its link is used; see Mail. |
DELETE |
/-/v1/account |
Delete the account. 409 with { packages, orgs } while the user is the only maintainer of a package that no Org owns, or the only owner of an Org. The name stays taken. |
POST |
/-/v1/account/passkeys/options |
{ first, publicKey }: the options for navigator.credentials.create(). |
POST |
/-/v1/account/passkeys |
Add a passkey: { credential, name, code? }. The first passkey needs a session that signed in within 10 minutes, and the answer has 10 recovery codes. A later one needs the session's approval, or, when the person lost their passkeys, a session that signed in within 10 minutes and a recovery code, which then stops working. A new passkey also approves the session. |
DELETE |
/-/v1/account/passkeys/:id |
Remove a passkey. |
POST |
/-/v1/account/sudo/options |
{ publicKey }: the options for navigator.credentials.get(). 409 with { setup: true } when the account has no passkey. |
POST |
/-/v1/account/sudo |
Approve this session for 10 minutes of account changes: { credential } from a passkey. A recovery code cannot: it only adds a passkey, because a person can type it into a phishing site. |
POST |
/-/v1/account/recovery |
Replace the recovery codes, and return the new codes once. |
POST |
/-/v1/account/email/resend |
Mail a new link for the waiting address, or for an unconfirmed one. 429 within a minute of the last link, 409 when nothing waits. |
DELETE |
/-/v1/account/email |
Stop a waiting change. The current address stays, and the link stops working. |
DELETE |
/-/user/token/:token |
npm logout. It revokes the token and always answers 200. |
GET |
/-/user/:user/package |
The packages that a user maintains. Other callers see only public packages. |
A user name has 1 to 39 lowercase letters, digits, or hyphens. Sign up on the website, then run npm login, which opens the website to approve the login with a passkey.
A web login session expires after 10 minutes.
A website session token lives 30 days. It can manage the account, its tokens, and its Orgs within 10 minutes after a passkey confirmed it. It cannot publish. It changes a package's trusted publishers and staged versions only with a passkey approval of each change.
A profile of a user is { kind: "user", name, created, github, orgs, packages, weekly }; the user also sees their email, and the role and visibility of each Org. A profile of an Org is { kind: "org", name, created, registry, github, members, packages, weekly }. packages lists each package with its version, description, date, and weekly downloads, and weekly is their sum. github is the verified { id, login }, or null. The registry trades the OAuth code with GitHub itself, so a caller cannot claim a GitHub account that it did not sign in to. Linking needs GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET on the api Worker.
Mail#
The registry sends mail from MAIL_SENDER through Azure Communication Services Email. Mail is off unless MAIL_URL and MAIL_SENDER are set.
A new account gets a link to confirm its address. The link works one time, for 1 hour, and the registry keeps only a hash of its code. The link opens a page on the website that confirms only when the person presses its button, because mail scanners open links on their own. A confirmed address stays until a new one is confirmed, and it gets a notice of the change. An unconfirmed address changes at once.
An unconfirmed address gets only its confirmation mail. A confirmed address gets these notices:
| Kind | When | Can be turned off |
|---|---|---|
| Security | A sign-in with the password, a new token, a passkey added or removed, new recovery codes, a recovery code used, GitHub linked or unlinked, a request to change the address | No |
| Account | The account was deleted, or an Org added, changed, or removed the user | No |
| Package | A version published or unpublished, a maintainer added or removed, or the access changed. Every maintainer gets it. | Yes, packageMail |
Each User cell keeps its mail in an outbox, and its alarm sends each message with the message id as the operation id, so a retry never sends a message twice. Package cells keep their notices until each User cell takes them.
Tokens#
| Method | Path | Purpose |
|---|---|---|
GET |
/-/npm/v1/tokens |
The signed-in user's tokens, in the form that npm token list reads. |
POST |
/-/npm/v1/tokens |
Create a read-only token. A passkey approves this for an npm login token. |
DELETE |
/-/npm/v1/tokens/token/:key |
Revoke a token by its key or by the full token. |
The POST body accepts name and expires (days). A new token can only read: readonly: false, packages_and_scopes_permission: "read-write", or bypass_2fa gets 400. A read-only token can live without an expiry. A login with npm login gives a token that lives 30 days, and whose writes each wait for a passkey approval.
Orgs#
These routes are not part of the npm protocol. npm org uses only the /user routes.
| Method | Path | Purpose |
|---|---|---|
PUT |
/-/org/:org |
Create an Org. The signed-in user becomes its owner. The name must be free. |
GET |
/-/org/:org |
{ name, registry, created }. created is an ISO time. |
GET |
/-/org/:org/user |
The members and their roles. Members see everyone; other callers see the public members. |
PUT |
/-/org/:org/user |
Add a member or change a role: { "user": "bob", "role": "developer" }. |
DELETE |
/-/org/:org/user |
Remove a member: { "user": "bob" }. |
GET |
/-/org/:org/package |
The Org's packages. A member sees each package with write. Other callers see only public packages, with read. |
GET |
/-/org/:org/token |
The Org tokens. Owners and admins only. |
POST |
/-/org/:org/token |
Create a read-only Org token: { "name": "ci", "expires": 30 }. Owners and admins only. readonly: false gets 400: Org CI publishes with trusted publishing. |
DELETE |
/-/org/:org/token/:key |
Revoke an Org token. |
POST |
/-/org/:org/visibility |
Show or hide the signed-in member's own membership: { "public": true }. |
POST |
/-/org/:org/github |
Link a GitHub organization: { code, redirect_uri, org }. The OAuth sign-in must be an owner of the GitHub organization, with the read:org scope. Owners only. |
DELETE |
/-/org/:org/github |
Unlink it. Owners only. |
The roles are owner, admin, and developer. Every member can read the Org's packages, and publish them with npm login and a passkey approval of each write. Owners and admins can change members. Only an owner can grant or remove the owner role. An Org always keeps one or more owners. A member can remove themselves. Each membership is private until its member makes it public.
Private registries#
| Method | Path | Purpose |
|---|---|---|
PUT |
/-/registry/:slug |
Create a private registry: { "scopes": ["acme"], "passthrough": false }. passthrough is optional. |
PUT |
/-/registry/:slug |
Change a registry: { "passthrough": true }. Only the user who made it. |
GET |
/-/registry/:slug |
{ slug, owner, scopes, created, passthrough, url }. created is an ISO time. Readers only. |
Each create or change waits for a passkey approval (see Approvals). The caller must own each scope: the scope is the caller's user name, or an Org where the caller is an owner. A scope can belong to only one private registry. When a scope moves to a private registry, its existing packages move with it and become restricted. After the move, the public host answers 404 for these packages.
A private registry serves the packages of its scopes. Readers are the scope's user, and the members of each Org scope, with a user token or an Org token.
A reader can also publish any other name to a private registry, such as a patched copy of a public dependency. The copy belongs to the registry, so every reader can publish new versions of it. It is always restricted, and it never changes the public package of the same name.
With passthrough on, the registry answers its readers for names that it does not hold with the public registry's packages, so a team can use the private host as its only registry. A name in one of the registry's scopes, or one that the registry holds, never passes through. Passthrough is off by default.
Passthrough to npm#
Until the backfill brings npm's packages here, the public registry can pass the names that it never held to an upstream registry, set by PASSTHROUGH_URL. Packuments and version manifests pass through with cache-control: public, max-age=300. Their tarball URLs still point at the upstream. A name that this registry ever held, or a name in a scope that it knows, never passes through. While passthrough is on, a new name that the upstream has cannot be published here.
POST /-/npm/v1/security/advisories/bulk (npm audit) and POST /-/npm/v1/security/audits/quick (pnpm audit, yarn audit) go to the upstream too. Before they leave, the registry removes every name in a scope that it knows, so the names of restricted packages stay here. A private registry with passthrough audits for its readers, and also removes the names that it keeps its own copies of.
Search#
GET /-/v1/search?text=TEXT&size=20&from=0
size is 1 to 250. The words of text match the name, description, keywords, author, and maintainers. The last word also matches as a prefix. These qualifiers filter the results: keywords:a,b, maintainer:name, author:name, and scope:name.
The response has the npmjs form: { objects, total, time }. Each object has package, downloads (weekly and monthly), score, searchScore, updated, flags, and dependents. dependents is always 0.
The public registry indexes only public packages. A private registry indexes all of its packages, and only its readers can search it. The index updates a short time after each change. Download totals in the index update once each day.
Front page#
GET /-/v1/front returns the totals and lists that the website's front page shows. Only the public registry host answers it, with cache-control: public, max-age=300.
| Field | Meaning |
|---|---|
packages, publishers |
Public packages in the index, and the users who published their latest versions. |
weekly |
Public downloads in the last 7 days, today included. |
private |
{ packages, registries, weekly }: restricted packages, private registries, and their downloads. Counts only. |
days |
{ day, downloads } for each day with public downloads, oldest first. Today is left out until it ends. |
recent, top, fresh |
The 6 latest releases, the 7 most downloaded this week, and the 7 newest packages by first publish. |
Each list entry has name, version, description, published, created, publisher, weekly, integrity (of the latest version), and provenance. Each Package cell adds its downloads to the totals once a day, and again after midnight for the rest of a day it counted. The counts that scan the index can be up to 10 minutes old.
Signatures#
GET /-/npm/v1/keys returns the registry's public key, in the npmjs form. Each version has dist.signatures: an ECDSA P-256 signature of name@version:integrity, in base64 DER. The keyid is the OpenSSH SHA256: fingerprint of the key. npm audit signatures verifies these signatures.
Downloads API#
These routes are on the api. host. The paths and responses are the same as api.npmjs.org. A scoped package's counts come from the registry that its scope is bound to.
| Path | Response |
|---|---|
/downloads/point/:period/:name |
{ downloads, start, end, package } |
/downloads/point/:period/a,b,c |
One object per package, by name. A package without data is null. |
/downloads/range/:period/:name |
{ start, end, package, downloads: [{ downloads, day }] }, with one entry for each day. |
/versions/:name/:period |
{ package, downloads: { "1.2.0": 10 } } |
A period is last-day, last-week, last-month, last-year, a day such as 2026-10-01, or a range such as 2026-01-01:2026-03-31. A range can be 550 days or less. The named periods end on the last complete day, in UTC. A day or range can include today, so it shows counts in real time.
A bulk query can name 128 or fewer packages, and none can be scoped. A restricted package needs a token that can read it. Each response sends content-type: application/json; charset=utf-8 and cache-control: public, max-age=300.
Not available#
| Feature | Behavior |
|---|---|
| Security advisories | Without PASSTHROUGH_URL, the audit routes return 501. npm install ignores it. npm audit shows an error. |
| TOTP codes | The registry never asks for a TOTP code: a passkey approves each write. publish_requires_tfa returns 400, as every write already needs a passkey. |
| Token limits | A token with packages, scopes, orgs, packages_all, or cidr_whitelist returns 400. |
| Teams and stars | These routes return 404, or 400 for a star update. |
| Unpublish policy | A maintainer can unpublish at any time. |