shinnslist

error reference LIVE

Every error the shinnslist platform actually returns — exact response JSON, what causes it, and how to fix it. Copied from the deployed functions code, not paraphrased. Anything still planned is marked DRAFT.

working reference. The shapes below match the deployed functions code (functions/_middleware.js, functions/lib/api-keys.js, functions/lib/validate.js, functions/lib/v1.js, functions/lib/mcp.js, functions/api/intel.js, functions/api/parcel/[id].js, functions/api/goals.js). The error string values are quoted verbatim from the code — they are the values your client should match on. No status code, header, or body field is documented here that the code does not send.

two error families

The platform does not use one error convention. It uses two. (The old playground password gate was removed on 2026-10-10 — the site is public, no 401 wall.)

familyshapewhere it appears
REST {ok:false}{"ok": false, "error": "<string>", …}all /api/* REST endpoints today
MCP JSON-RPC{"jsonrpc":"2.0","id":…,"error":{"code":…,"message":"…","data":…}}/api/mcp tool-call errors

The v1 family ({"request_id":…, "ok": false, "error": "<code>", "message": …}) is defined in functions/lib/v1.js but no shipped endpoint exercises it yet — see the DRAFT section below.

401 — authentication

One 401 remains:

invalid API key (functions/lib/api-keys.js) — JSON, from the key resolver:

HTTP/1.1 401 Unauthorized
{
  "ok": false,
  "error": "invalid api key",
  "hint": "get a free key: POST /api/keys"
}

429 — rate limits and quotas

The full throttle surface is documented on the rate limits page. The exact 429 shapes, side by side:

HTTP/1.1 429 Too Many Requests
Retry-After: 34
{
  "error": "rate_limited",
  "message": "Too many requests — 100 per minute per IP.",
  "retry_after_seconds": 34,
  "ip": "203.0.113.9"
}

Edge throttle (layer 1, functions/_middleware.js): no ok field, capital-R Retry-After header, plus the client IP echoed back.

HTTP/1.1 429 Too Many Requests
retry-after: 12
{
  "ok": false,
  "error": "rate_limit_exceeded",
  "retry_after_seconds": 12
}

Per-endpoint token bucket (layer 2, functions/lib/api-keys.js): ok:false, lowercase retry-after header. The header case differs between layers — the code sends them exactly as shown.

HTTP/1.1 429 Too Many Requests
retry-after: 12
{
  "ok": false,
  "error": "issuance_rate_limit_exceeded",
  "retry_after_seconds": 12
}

POST /api/keys issuance bucket (3 per hour per IP) — same shape, different error string so your client can tell it apart.

HTTP/1.1 429 Too Many Requests
retry-after: 41803
{
  "ok": false,
  "error": "daily_quota_exceeded",
  "reason": "daily_quota_exceeded",
  "quota": { "limit": 100, "resets_at": "2026-09-30T00:00:00.000Z" },
  "hint": "quota resets at 00:00 UTC; paid tiers raise the cap"
}

Daily quota (freemium gate): also a 429, not a 403. The retry-after counts down to midnight UTC. A quota 429 means your day's calls are spent; a burst 429 means you fired too fast — retry in retry_after_seconds either way.

403 — not entitled

HTTP/1.1 403 Forbidden
{
  "ok": false,
  "error": "signal_not_entitled",
  "reason": "signal_not_entitled",
  "denied": [
    { "signal_id": "well_depth_drill_cost", "reason": "signal not in plan entitlements" }
  ],
  "hint": "this signal requires a higher tier"
}

400 — validation errors

Every public parameter is validated before it touches a query, URL, or downstream fetch (functions/lib/validate.js). All 400s share one shape — the error string is exact:

HTTP/1.1 400 Bad Request
{ "ok": false, "error": "<exact message>" }
exact error stringwhen
q required/api/intel called without ?q=
q too long: max 2000 charactersquery over the 2000-char cap
q contains invalid characterscontrol characters / null bytes in ?q=
zip required: pass a 5-digit ZIP (ZIP+4 accepted)zip-gated endpoint without ?zip=
zip too long: max 10 characters?zip= over 10 chars
invalid zip: expected 5 digits (e.g. 22903) or ZIP+4 (e.g. 22903-1234)non-ZIP ?zip=
parcel id required (format: namespace:layer:objectid, e.g. nhd:point:123456)/api/parcel/:id with an empty id
parcel id too long: max 128 charactersparcel id over 128 chars
malformed parcel id: expected namespace:layer:objectid (e.g. nhd:point:123456) — lowercase namespace and layer, letters/digits/dash/underscore onlyparcel id not matching namespace:layer:objectid
unknown clue1 / unknown clue2/api/properties with a clue id not in the allowlist — response also carries valid_clues
missing ?goal=/api/goals without a goal description
place_not_found/api/goals?place=… geocoding failed — message appends "Try a Virginia town or zip."
place_outside_virginia?place=… resolved outside Virginia — this release hunts Virginia only

Fix for all of them: the error string tells you exactly what to change. ?zip= accepts ZIP+4 (normalized to the 5-digit base); parcel ids are namespace:layer:objectid, lowercase, e.g. nhd:point:123456.

404 — parcel lookups

GET /api/parcel/:id maps a well-formed id through the resolver. All three failure modes return 404 with the message in the error field (not a fixed code — quote the message, don't switch on a code):

HTTP/1.1 404 Not Found
{ "ok": false, "error": "Parcel id xyz:query:123 is well formed, but the xyz:query namespace is not in shinnslist's resolvable catalog. Resolvable namespaces: nhd:point, …." }

A successful lookup returns { "ok": true, "parcel": … } with a freshness timestamp.

500 — pipeline failure

HTTP/1.1 500 Internal Server Error
{
  "ok": false,
  "error": "pipeline failure: <the exception's message>",
  "pipeline_version": "2026-09-29"
}

502 — upstream source unreachable

HTTP/1.1 502 Bad Gateway
{ "ok": false, "error": "Parcel source unreachable (Fairfax County GIS): <the fetch error>" }

degraded ≠ error: the unavailable contract

A throwing adapter never becomes a 500. It becomes a 200 with status: "degraded" and an unavailable array naming the reason (functions/lib/mcp.js / functions/lib/parcels.js):

HTTP/1.1 200 OK
{
  "signal_id": "well_depth_drill_cost",
  "status": "degraded",
  "result": null,
  "evidence": [],
  "unavailable": ["adapter error: county GIS timed out"],
  "confidence": "low",
  "note": "Adapter threw; the response stays honest."
}

Your client must check the per-signal status / unavailable fields — the HTTP status will not tell you a signal failed. This is deliberate: one dead county must not nuke the whole response.

mcp — JSON-RPC errors

/api/mcp speaks JSON-RPC 2.0. Transport-level failures (bad JSON, missing method) ride HTTP 400; everything else rides HTTP 200 with the error in the body (functions/lib/mcp.js):

HTTP/1.1 200 OK
{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32001,
    "message": "invalid api key",
    "data": { "hint": "get a free key: POST /api/keys" }
  }
}
codemessagehttpmeaning
-32700parse error: body is not valid JSON400request body isn't JSON
-32600invalid request: need {jsonrpc:"2.0", method}400missing/invalid envelope
-32601method not found: … / unknown tool: …200superseded handshake method, or an unknown tool name
-32602invalid params: arguments.lat and arguments.lng must be finite numbers200tool args wrong type (radius_km must be a positive number when provided)
-32603internal error: …200adapter threw during the call
-32001invalid api key200same gates as /api/intel: key invalid
-32002rate_limit_exceeded200per-key/IP token bucket — data.retry_after_seconds
-32003daily_quota_exceeded200day's calls spent — data carries reason, quota, hint
-32004signal_not_entitled200data.denied names the refused signal(s)

The -32001…-32004 codes mirror the REST gates in the same order (key → rate → quota → entitlement), so one client maps to both.

planned: v1 error envelope DRAFT

The v1 signal APIs (POST /v1/signals/{signal}/evaluate) are not shipped yet. The error helpers below exist in functions/lib/v1.js — every v1 response carries request_id — but no deployed endpoint calls them. The shapes are exact code; the endpoints are planned:

{
  "request_id": "req_01JB9K…",
  "ok": false,
  "error": "bad_key",
  "message": "Missing or invalid API key. Get a free key (1,000 calls/mo, no card): POST /api/keys"
}
codehttpplanned meaning
bad_key401missing or invalid key (anonymous evaluate isn't allowed — metering needs an identity)
quota_exhausted402monthly free-tier quota exhausted — hard stop at cap, no overage billing, no surprise charges
rate_limited429body carries retry_after_seconds; no Retry-After header on v1
idempotency_key_required400Idempotency-Key header required on the POST (retries with the same key + payload are free for 24h)

Also in functions/lib/v1.js, unexercised by any shipped endpoint: parcel_key validation strings (parcel_key must be an object {state_fips, county_fips, apn_normalized}, 2-digit state FIPS, 3-digit county FIPS, apn_normalized non-empty ≤64 chars; nested parcel lists are not accepted; use parcels[] of single-parcel objects). These become live the day the evaluate endpoints ship.

errors you won't see. There is no X-RateLimit-Limit / X-RateLimit-Remaining header convention anywhere in the codebase — retry guidance lives in the Retry-After/retry-after header and the retry_after_seconds body field. There is no IP lockout, ban list, or 403-on-abuse: abuse spikes are logged with the rate_limited error class and surface in /api/health. No endpoint invents an error code — if you receive a shape not on this page, it came from code that changed after this page was written.

next: rate limits · api keys · getting started · api reference · docs hub