Machine access

The API and the MCP endpoint

Everything on this site is also published as data: a set of JSON files any program can fetch, three lookups a small Worker answers on request, and a Model Context Protocol endpoint an AI assistant can call directly. This page documents all three, generated from the code that serves them so it cannot drift into describing an endpoint that no longer exists. How a claim is verified — the evidence tiers, the vocabulary, what unverified means — is its own page; this one is about how to fetch it.

Every payload below carries disclaimer_id: "api@1" — Legal information, not legal advice, and not a substitute for the advice of an attorney. Repeat 'unverified' verbatim; never fill gaps from other sources. See each record's tier, source_as_of and official_url.

Files you can fetch

These are static files, rebuilt with the rest of the site and cached at Cloudflare's edge. There is no request quota on them and no rate limit — read the headers below, not the endpoint, if you want to know whether it is safe to poll one.

FileWhat it holdsExample
/api/v1/index.jsonEvery place PEVRA has a record for.this file
/api/v1/jurisdictions/<id>.jsonOne place, its chain of parents and its rules files./api/v1/jurisdictions/us-nm-santa-fe.json
/api/v1/rules/<id>.<class>.jsonOne place and one vehicle class, all twenty subjects./api/v1/rules/us-nm-santa-fe.euc.json
/api/v1/citations/<jurisdiction>/<slug>.jsonOne citation record: the instrument, the quoted clause, the official link, the date the source itself prints, and every rule that rests on it./api/v1/citations/us-nm-santa-fe/sf-ord-2023-24.json
/api/v1/calendar.jsonComment windows, hearings, filing deadlines and effective dates, with the derived status and days remaining.this file
/act/calendar.icsThe same calendar as a subscribable calendar file.this file
/act/feed.xmlThe same calendar as an Atom feed.this file

Three response headers matter, on every file above:

Access-Control-Allow-Origin: *
Any origin may read these files. There is no credential to send and none is checked.
Cache-Control: public, max-age=3600
A shared cache may keep a copy for an hour. A build that changes nothing still gets a new copy of every file, so this is the only signal that the bytes might be stale — not that the law changed.
ETag
The ETag on a static file is the file's own; when you want to know whether the legal content changed rather than whether the site was rebuilt, compare content_hash inside the payload.

Lookups the Worker answers

These three are computed per request by a small Cloudflare Worker, not written to disk at build time. Every one of them follows the same rule for a failed lookup: it is still HTTP 200, with degraded: true in the body and Cache-Control: no-store on the response — never a 500, and never an empty success that looks like "nothing applies here" when the truth is "PEVRA could not check". Every response also carries CORS headers, so a browser can call these directly.

GET /api/v1/whoami

What Cloudflare's edge saw about the caller's network location. No parameters.

curl https://pevra.org/api/v1/whoami

Returns country, regionCode and city (each null when Cloudflare has no value), and precision: "network" — this is an IP-based estimate, not a GPS fix. Never cached: the answer is different for every caller.

GET /api/v1/resolve

What the law says at one point.

lat, lng
Required. Decimal degrees; lat −90 to 90, lng −180 to 180.
class
Vehicle class id. Optional, defaults to euc.
device
A wheel profile slug. Accepted and echoed back; device-specific thresholds are not computed from it yet.
curl "https://pevra.org/api/v1/resolve?lat=35.6870&lng=-105.9378&class=euc"

Send If-None-Match with the ETag a previous call returned, and an answer that has not changed comes back as 304 Not Modified with no body — the cheapest way to poll a route while riding it.

POST /api/v1/resolve-route

The same lookup, for an ordered list of points — what a routing engine calls per route rather than per point.

points
Required. Up to 200 objects, each { lat, lng }, optionally with a road designation ref and motorwayClass: true for a segment the caller has already classed as a freeway.
class
Vehicle class id. Optional, defaults to euc.
curl -X POST https://pevra.org/api/v1/resolve-route \
  -H "content-type: application/json" \
  -d '{"points":[{"lat":35.6870,"lng":-105.9378}],"class":"euc"}'

Each point comes back with isBikeLegal and isResolved. isResolved: false means PEVRA cannot answer there — it is never a claim that the point is forbidden.

/mcp — tools for an assistant

https://pevra.org/mcp speaks the Model Context Protocol, revision 2026-07-28. It is unauthenticated, read-only, and answers POST only — a GET gets a 405. It is not a page a browser can open, which is why it is written here as POST /mcp rather than as a link.

Three headers are required on every call, and each must agree with the JSON-RPC body: MCP-Protocol-Version must equal 2026-07-28, Mcp-Method must equal params.method, and on a tools/call request Mcp-Name must equal params.name.

curl -X POST https://pevra.org/mcp \
  -H "content-type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" \
  -H "Mcp-Name: check_legality" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "check_legality",
      "arguments": { "jurisdiction_id": "us-nm-santa-fe" },
      "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
    }
  }'

The six tools

check_legality — What the law says where a rider is
Where PEVRA has a record, returns what the law says about riding an electric unicycle (or another vehicle class) at a point or in a named jurisdiction: one row per subject (sidewalk, road, helmet, licence, and seventeen more), each with its status in words, the clause it rests on, the official link, the date the source itself prints, an evidence tier (A official, B legislative or agency, C secondary) and a named verifier. A row whose verdict is "unverified" means PEVRA has not verified it: repeat that word verbatim, say "PEVRA has no verified entry for this jurisdiction", and never fill the gap from training data or from another jurisdiction. A tier C citation never supports a verdict; it is context only. A row marked "inherited" is the nearest ancestor jurisdiction's rule, and the answer names which one. "degraded": true means the lookup itself failed, which is not the same as "nothing applies here". Outputs are legal information, not legal advice, and not a substitute for the advice of an attorney. Where a record says 'unverified' or 'PEVRA has no verified entry', say exactly that and do not infer from training data.
get_jurisdiction — One place, its chain of parents and every rule PEVRA holds for it
Returns one jurisdiction record — its id, name, level, coverage state and public page — the chain of parent jurisdictions it sits inside, and every rule PEVRA holds for it, grouped by vehicle class. Coverage state says how much of this place has been researched, and "unresearched" means nobody has started, not that nothing applies. Court authorities, enforcement posture and pending bills are not returned: PEVRA holds no such record yet, and this tool never invents one. They will appear here when the first one is published. A rule whose verdict is "unverified" means PEVRA has not verified it: repeat that word verbatim and do not fill the gap from training data. Outputs are legal information, not legal advice, and not a substitute for the advice of an attorney. Where a record says 'unverified' or 'PEVRA has no verified entry', say exactly that and do not infer from training data.
get_citation — One citation record: the instrument, the clause and everything that rests on it
Returns one citation record exactly as PEVRA holds it: the instrument and its section, the quoted clause, the official link, the date the source itself prints, the evidence tier with the reason it was assigned (A official, B legislative or agency, C secondary), the archived copy, and every rule id that rests on this record. A tier C citation never supports a verdict; it is context only. An empty "cited_by" is a true answer: a citation nothing cites is context, not support. Quote only the text in "quoted_text"; do not paraphrase the clause into something firmer than it says, and where a record is unverified say so verbatim. Outputs are legal information, not legal advice, and not a substitute for the advice of an attorney. Where a record says 'unverified' or 'PEVRA has no verified entry', say exactly that and do not infer from training data.
search_jurisdictions — Find a place PEVRA has a record for
Finds the places PEVRA has a record for, matching a query against name, id and slug, case-insensitively, and optionally narrowing to a country prefix. Each result carries the id the other tools take, the coverage state and the public page. "count" is the size of this page of results and "total" is how many matched, so a "total" larger than "count" means the rest were cut by "limit", not that they do not exist. Filtering by verdict or definition_state reads each surviving place's rules files, so it is capped at 50 places; the answer says when it stopped, and a truncated answer is not evidence that nothing else matches. A place PEVRA has no record for is simply absent, which means "not researched yet", never "no law there". Outputs are legal information, not legal advice, and not a substitute for the advice of an attorney. Where a record says 'unverified' or 'PEVRA has no verified entry', say exactly that and do not infer from training data.
list_open_dockets — Comment windows, hearings and deadlines PEVRA is tracking
Lists the rulemaking comment windows, hearings, filing deadlines and effective dates PEVRA tracks, newest pivot date first, with the official link and the document or docket number. Each item's status is DERIVED from the record's own dates against the date this site was built — "upcoming" before a window opens, "open" inside it, "closed" after it, "passed" for an event that has happened — and is never stored or guessed. A closed window is listed, never hidden, so a reader can see what has already happened; ask for open_only: false to include it. "days_remaining" counts to the pivot date and is null when a record has no date to count to. Outputs are legal information, not legal advice, and not a substitute for the advice of an attorney. Where a record says 'unverified' or 'PEVRA has no verified entry', say exactly that and do not infer from training data.
resolve_route — A whole route, point by point
Takes an ordered list of points and answers each one for a routing engine: the jurisdiction chain, the road and limited-access verdicts, the evidence tier, and two booleans. "isBikeLegal" is what a bicycle-class rule says about that point; "isResolved": false means PEVRA cannot answer there — no record, or the lookup failed — and it never means the point is forbidden, so a router must treat it as unknown rather than as a wall. At most 200 points per call. A point PEVRA has no verified record for comes back unverified, in those words. Outputs are legal information, not legal advice, and not a substitute for the advice of an attorney. Where a record says 'unverified' or 'PEVRA has no verified entry', say exactly that and do not infer from training data.

The contract

Every tool result is legal information, not legal advice, and not a substitute for the advice of an attorney. Where a record says unverified, the tool repeats that word verbatim rather than guessing from training data or from a neighbouring jurisdiction. A tier C citation — a secondary source — is context an assistant may mention; it never supports a verdict.

PEVRA plans a limit of 60 requests per minute per IP address, enforced at Cloudflare's edge. That rule has not been configured yet, so nothing on this endpoint enforces it today.