API REFERENCE
One path per package version
Every package version on npm has exactly one address in this API. Read it
with GET to get whatever is known. Write to it with POST to put it
in the sandbox queue. Same path, both directions, so nothing has to be looked up twice.
https://api.snappyfeet.orgBase URL. Every path below hangs off it. HTTPS only, and requests over plain HTTP are refused rather than redirected.
Authorization: Bearer <key>Required on every request. A free key reads verdicts. A paid key can also queue analyses.
Content-Type: application/jsonOn any request with a body. Responses are always JSON, including errors.
THE PACKAGE PATH
GET reads it, POST queues it
A GET never costs you an analysis. If the version has been through
the sandbox you get the verdict and the evidence immediately. If nobody has ever run it you
get 404 with "verdict": "unknown", and it is then up to you whether
to spend an analysis on it.
curl -H "Authorization: Bearer $COHEN_KEY" \ https://api.snappyfeet.org/api/package/npm/left-pad/1.3.0 { "ecosystem": "npm", "name": "left-pad", "version": "1.3.0", "verdict": "clean", "analysed_at": "2026-08-31T09:14:02Z", "engines": [ "dynamic", "static" ], "sources": [ "cohen", "osv", "npm-advisory" ], "integrity": "sha512-nbE1WxOT...", "findings": [], "evidence": "/api/package/npm/left-pad/1.3.0/evidence" }
curl -X POST -H "Authorization: Bearer $COHEN_KEY" \ https://api.snappyfeet.org/api/package/npm/polymarket-stake-mathss/3.5.2 // queued. one deep analysis has come off your monthly allowance. { "job": "job_01HXY5K9PNQE3", "state": "queued", "queue_position": 4, "eta_seconds": 180, "poll": "/api/job/job_01HXY5K9PNQE3", "allowance": { "used": 137, "limit": 150, "resets": "2026-10-01T00:00:00Z" } } // the same POST once the run has finished, 200 OK, no charge { "verdict": "review", "cached": true, "findings": [ "credentials_read_then_archived", "outbound_outside_registry" ] }
Posting a version that is already known returns
200 with the cached verdict and charges nothing. Posting one that is already in
the queue returns the existing job rather than creating a second. You cannot accidentally pay
twice for the same version.
SCOPED NAMES
A scope contains a slash, so encode it
This is the one place the path shape bites. An npm scoped name such as
@types/node has a slash in it, which would otherwise read as another path
segment. Percent encode it as %2F. The leading @ is safe either
raw or as %40.
/api/package/npm/@types%2Fnode/20.11.5Correct. One name, one version, two segments.
/api/package/npm/@types/node/20.11.5Rejected with 400. Three segments, so the version is ambiguous. The error names the fix rather than guessing.
/api/package/npm/%40types%2Fnode/20.11.5Also correct, if your client encodes aggressively.
ENDPOINTS
Everything, in one list
BULK SUBMIT
A whole tree in one request
Posting 1,284 packages one at a time works, and it is a waste of everybody's time. Send the tree instead. The response splits it into what is already known and what had to be queued, so you can act on the known part immediately.
curl -X POST -H "Authorization: Bearer $COHEN_KEY" \ -H "Content-Type: application/json" \ -d @tree.json https://api.snappyfeet.org/api/package/npm // request body { "versions": [ "left-pad@1.3.0", "@types%2Fnode@20.11.5", "slugify-fast@2.1.0" ], "queue_unknown": true, "skip_private": true } // response { "counts": { "clean": 1271, "review": 2, "queued": 11, "skipped": 0 }, "review": [ { "name": "polymarket-stake-mathss", "version": "3.5.2", "findings": [ "credentials_read_then_archived" ] } ], "queued": [ { "name": "tiny-parse", "version": "0.2.1", "job": "job_01HXY..." } ], "allowance": { "used": 148, "limit": 150 } }
Set queue_unknown to false
to get a pure read: you are told what is unknown without spending anything on it. With
skip_private set, names that do not resolve on the public registry are counted
as skipped and never leave your side of the wire.
THE VERDICT OBJECT
Three states, and only three
The sandbox saw behaviour a human should look at. Never a claim of malice, always a
pointer to evidence. findings[] is populated and evidence is
fetchable.
It ran, and nothing correlated. Recorded with a timestamp, because clean in August is not clean in October if the version was republished.
verdictOne of clean, review or unknown. Never a score, never a percentage. A number would imply a confidence nobody can honestly give.
findings[]Stable string identifiers, not prose, so you can match on them. Empty on a clean verdict.
engines[]Which engines produced the answer. A database only hit returns an empty array and a sources list instead.
analysed_atWhen the sandbox ran, not when you asked. Compare it against the version publish date if freshness matters to you.
integrityThe published integrity hash of the tarball that was actually analysed. If it differs from your lockfile, you and the sandbox looked at different bytes.
evidenceA path, not an object. The trace is large, so it is a second request rather than a bloated first one.
OBSERVATORY FEED
Cursor forward, or ask an indicator backward
The feed is a different product on a different meter: requests, not analyses. It reads the same records the scanner writes, so nothing is recomputed for it.
curl -H "Authorization: Bearer $COHEN_KEY" \ "https://api.snappyfeet.org/api/observatory/indicator/host/45.61.136.12" { "indicator": { "type": "host", "value": "45.61.136.12" }, "first_seen": "2026-08-31T09:14:22Z", "packages": [ { "name": "polymarket-stake-mathss", "version": "3.5.2", "by": "node postinstall.js" }, { "name": "fast-json-parse-native", "version": "0.4.1", "by": "curl" } ] }
?since={cursor}Opaque cursor from the previous page. Use this rather than a timestamp: it is stable across reruns and will not skip a record written out of order.
?verdict=reviewFilter the feed. Omit it to get clean versions too, which is what makes the feed usable as a baseline.
indicator/{type}host, domain, sha256 or path. Every one resolves back to the package and the process that produced it.
ERRORS AND LIMITS
What each status actually means
Errors are JSON with a stable error code and a message written for
a person. The code is what you should branch on.
200Known. On a POST this means it was already analysed and you were not charged.
202Queued. An analysis has come off your allowance. Poll the poll path.
207Bulk submit succeeded with a mixed outcome. Read counts, not the status line.
400Malformed path or body. Most often an unencoded scope. The message names the segment at fault.
401Missing or invalid key.
403Your key cannot do this. Either the allowance is spent, or the endpoint needs a paid key.
404Two different situations, distinguished by the body: verdict: "unknown" means we have never analysed it, error: "not_in_registry" means npm has never heard of it either.
429Rate limited. Retry-After is always set. Back off rather than retrying tightly.
503The sandbox is saturated. Reads keep working, queueing does not, and nothing is charged.