API REFERENCE

One path per package version

Every version on npm has one address. GET reads whatever is known. POST puts it in the sandbox queue. Same path, both directions.

Quickstart#

Sign in with Google, create a key, read a verdict. No card, no waiting.

THREE STEPS60 SECONDS
# 1. Get a key at https://console.snappyfeet.org
export COHEN_KEY=cohen_live_...

# 2. Check it works. This endpoint is free and never metered.
curl -H "Authorization: Bearer $COHEN_KEY" \
  https://api.snappyfeet.org/api/key

# 3. Read a verdict.
curl -H "Authorization: Bearer $COHEN_KEY" \
  https://api.snappyfeet.org/api/package/npm/left-pad/1.3.0

Authentication#

Every request carries a key. Keys begin cohen_live_.

HEADERREQUIRED
Authorization: Bearer cohen_live_1WbwMTf2vx0t2NUWuZYSS3oQP26mU4JL...

# the scheme is optional, so this also works
Authorization: cohen_live_1WbwMTf2vx0t2NUWuZYSS3oQP26mU4JL...
PropertyBehaviour
LifetimeSeven days on Free, ninety on Individual. Extend at any time from the console.
GraceAfter expiry a key keeps reading verdicts for 48 hours, then stops. Queueing stops immediately.
StorageShown once, stored as a hash. Lost keys cannot be recovered, only revoked and replaced.
RevocationTakes effect within one minute.
Keys are secrets. Keep them out of public repositories and client side code. Give CI its own key so it can be revoked alone.

Conventions#

ItemValue
Base URLhttps://api.snappyfeet.org. HTTPS only. Plain HTTP is refused rather than redirected.
Content typeapplication/json on any request with a body. Every response is JSON, errors included.
Timestampsanalyzed_at is epoch seconds.
Scoped namesBoth spellings work. @types/node and @types%2Fnode resolve to the same package.
Ecosystemsnpm today. The path carries the ecosystem so others can be added without a new shape.

Plans and limits#

PlanScopesLookupsAnalysesKeys
Freeread100 per daynone1
Demoread queue evidence feedunmetered250 per month3
Individualread queue evidenceunmetered150 per month5

Reading is free. Only a version nobody has analysed costs an analysis. Demo access is by invitation; redeem a code at console.snappyfeet.org/demo.

Packages#

GET /api/package/npm/{name}/{version} free

One verdict. Never costs an analysis.

REQUEST200 OK
curl -H "Authorization: Bearer $COHEN_KEY" \
  https://api.snappyfeet.org/api/package/npm/@agimon-ai/doompi-help/0.0.1-alpha.26

{
  "ecosystem": "npm",
  "name": "@agimon-ai/doompi-help",
  "version": "0.0.1-alpha.26",
  "verdict": "clean",
  "analyzed_at": 1788180250
}

// 404 when nobody has run it. Yours to decide whether to spend an analysis.
{ "verdict": "unknown", "name": "left-pad", "version": "1.3.0" }
GET /api/package/npm/{name} free

Every analysed version of a package, newest first.

GET /api/package/npm/{name}/{version}/evidence

The full trace: syscalls, process tree, connections, file hashes, entropy. Large, so it is a second request rather than a bloated first one.

POST /api/package/npm/{name}/{version}

Queue a deep analysis. Charged only when the version is genuinely unknown. Posting something already analysed returns 200 and the cached verdict. Posting something already queued returns the existing job. You cannot pay twice for the same version.

REQUEST202 ACCEPTED
curl -X POST -H "Authorization: Bearer $COHEN_KEY" \
  https://api.snappyfeet.org/api/package/npm/tiny-parse/0.2.1

{
  "job": "job_01HXY5K9PNQE3",
  "state": "queued",
  "queue_position": 4,
  "eta_seconds": 180,
  "poll": "/api/job/job_01HXY5K9PNQE3",
  "allowance": { "used": 137, "limit": 150 }
}

Bulk submit#

POST /api/package/npm

Send a whole dependency tree in one call. The response splits it into what is already known and what had to be queued, so you can act on the known part immediately. This is what Mantis uses.

REQUEST207 MULTI STATUS
curl -X POST -H "Authorization: Bearer $COHEN_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: build-4471" \
  -H "X-Project: checkout-api" \
  -d @tree.json https://api.snappyfeet.org/api/package/npm

// body
{
  "versions": [ "left-pad@1.3.0", "@types/node@20.11.5" ],
  "queue_unknown": true,
  "skip_private": true
}

// response
{
  "scan": "1f9c0a2e...",
  "counts": { "clean": 1271, "review": 2, "queued": 11, "skipped": 0 },
  "review": [ { "name": "polymarket-stake-mathss", "version": "3.5.2" } ],
  "queued": [ { "name": "tiny-parse", "version": "0.2.1", "job": "job_01HXY..." } ],
  "allowance": { "used": 148, "limit": 150 }
}
Field or headerEffect
versions[]Up to 5,000 per request, as name@version.
queue_unknownfalse gives a pure read. You are told what is unknown and charged nothing.
skip_privateNames that do not resolve on the public registry are counted as skipped and never leave your side of the wire.
Idempotency‑KeyA retried request reuses the original reservation instead of charging twice.
X‑ProjectLabels the run in your console history.
X‑ClientSet to mantis to mark the source.

Every bulk submit becomes one scan in your history, with its counts and findings kept.

Jobs#

GET /api/job/{job} free

State of one queued analysis: queued, running, done or failed. Polling is free.

Observatory#

A different product on a different meter: requests, not analyses. It reads the same records the scanner writes, so nothing is recomputed for it. Access needs the feed scope, which is sold separately.

GET /api/observatory/records
QueryMeaning
?since=Opaque cursor from the previous page. Stable across reruns and will not skip a record written out of order.
?verdict=clean, review or unknown. Omit to get everything, which is what makes the feed usable as a baseline.
?limit=1 to 1,000. Defaults to 100.
GET /api/observatory/indicator/{type}/{value}

Reverse lookup. Give it a C2 host or a file hash, get every package version that touched it. Types are host, domain, sha256 and path.

REQUEST200 OK
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" }
  ]
}

Webhooks#

POST /api/webhook

Register a callback for when a package you watch flips to review. The response carries a signing secret, shown once. HTTPS URLs only.

REQUEST201 CREATED
curl -X POST -H "Authorization: Bearer $COHEN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/cohen"}' \
  https://api.snappyfeet.org/api/webhook

{
  "id": "9f2c...",
  "url": "https://example.com/hooks/cohen",
  "events": [ "verdict.review" ],
  "secret": "whsec_..."
}

Account#

GET /api/key free

Your plan, usage and reset date. Never counted against anything, so it is safe as a health check in scripts.

REQUEST200 OK
curl -H "Authorization: Bearer $COHEN_KEY" https://api.snappyfeet.org/api/key

{
  "tier": "individual",
  "analyses": { "used": 137, "limit": 150, "resets": "2026-10-01" },
  "lookups_today": { "used": 42, "limit": null },
  "key": { "scopes": ["read","queue","evidence"], "in_grace": false }
}
GET /api/plans no key

The current price list. Unauthenticated, because it is public information.

Verdict object#

REVIEW

The sandbox saw behaviour a human should look at. Never a claim of malice, always a pointer to evidence.

CLEAN

It ran, and nothing correlated. Recorded with a timestamp, because clean in August is not clean in October if the version was republished.

FieldMeaning
verdictclean, review or unknown. Never a score. A number would imply a confidence nobody can honestly give.
analyzed_atEpoch seconds. When the sandbox ran, not when you asked.
ecosystemnpm.
name, versionAs analysed. A scoped name comes back with its slash decoded.

Errors#

Every error is JSON with a stable error code and a message written for a person. Branch on the code, never the message.

SHAPEALL ERRORS
{ "error": "scope_required",
  "message": "This endpoint needs the 'queue' scope." }
CodeStatusMeaning
missing_key401No Authorization header.
invalid_key401Malformed or unrecognised. Checked before any lookup, so a typo never reaches the database.
key_expired401Past its grace window. Extend it in the console.
key_expired_grace403Expired but still reading. Queueing is refused.
account_suspended403Contact support.
scope_required403Your key cannot do this. Free keys read only.
paid_key_required403Evidence traces need a paid plan.
allowance_spent403Monthly analyses used. Nothing auto upgrades.
bad_path400The path is not a name and a version.
bad_json400Body is not valid JSON.
empty_body400versions is missing or empty.
too_many400Over 5,000 versions in one request.
bad_url400Webhook URL is not HTTPS.
bad_indicator400Type is not host, domain, sha256 or path.
unknown404Not an error code but a verdict. Nobody has analysed it.
not_in_registry404npm has never heard of it either.
no_such_job404No job with that id.
daily_cap429Free keys read 100 verdicts a day. Retry‑After is set.
rate_limited429Too many requests. Back off rather than retrying tightly.
sandbox_saturated503Queueing is unavailable. Reads keep working and nothing is charged.
verdict_store_unavailable503We could not reach the verdict store. This is not a statement about the package.
Do not treat 503 as clean. verdict_store_unavailable means we could not check, which is different from checking and finding nothing. Caching it as a pass is the wrong way to fail.

Status#

SurfaceState
Reading verdictslive Free and paid keys.
Bulk submit, read onlylive With queue_unknown: false.
Account and planslive
Queueing analysessoon Returns 503 until the sandbox queue is connected.
Evidence tracessoon
Observatory feedsoon
Webhookssoon Registration works, delivery does not.

A free key reads every verdict in the database

contact@snappyfeet.org