Skip to content

For machines

Take the data. It is CC BY, there is no key, and nothing is rate limited

Everything Orvik publishes for a reader is also published as JSON: the 749 entries in our identification keys with their full trait coding, the 570 taxa and specimens matched to GBIF, iNaturalist, Macrostrat and the Paleobiology Database, and the five reference tables the field calculators run on. No account, no key, no quota. Credit Orvik with a link and use it for whatever you like, including commercially.

Start here

One request returns every dataset with its row count, its licence and what it is for:

curl https://orvik.app/api/v1/index.json

The OpenAPI 3.1 description is at /api/v1/openapi.json, and /api/v1/status.json reports when each file was last rebuilt. Every response is application/json, Access-Control-Allow-Origin: *, and cached for an hour. There is no authentication of any kind — see /auth.md, which says so formally rather than leaving you to discover it.

The six endpoints, with the number of rows each currently carries. Counts are computed at build time, never typed.
Endpoint Rows What it is
/api/v1/key-entries.json 749 Every specimen in every key, with its trait coding, the character that separates it from its lookalikes, and joined occurrence figures.
/api/v1/field-data.json 570 Occurrence counts joined from GBIF, iNaturalist, Macrostrat and PaleoBioDB. Snapshot 2026-09-04.
/api/v1/tools.json 28 The catalogue of free browser tools, each with the facets its key can filter on.
/api/v1/reference-tables.json 5 ISA growth factors for 32 trees, 5 pet size bands, metal content for 22 coins, the 10-step Mohs scale, sowing timing for 34 crops.
/api/v1/orvik.json The product: subjects, capabilities, which services process photographs, and what it does not do.
/api/v1/status.json When each dataset was last rebuilt, and the field-data snapshot date.

Four fields people read backwards

A schema tells you a field is an array of strings. It does not tell you that adding two of the rows together produces a number that is wrong by a factor of a thousand. These are the four places this dataset has actually been misread, and each one is carried as a caveat inside the payload as well as here, because a retrieved passage travels alone.

traits — intersection across facets, union within one

Each entry carries an object keyed by facet id, valued with the list of option ids that entry satisfies. A caterpillar listed as both green and yellow satisfies either, not both at once: a reader who says "green" should still see it. But an entry must satisfy every facet the reader has answered. Filter by union inside a facet and intersection across facets, and the key behaves the way the page does. Do it the other way round and a key with four facets returns almost everything.

species_rankfalse means do not add this row up

Not every entry in a key is a species. "Rhododendron species" is a genus, and its GBIF count already contains every species inside it. "Killdeer, tern and plover scrapes" resolves to an order holding 201 million records. Summing a species total that includes one of those double-counts, and ranking a list that includes one puts an order at the top of a table of spiders. Every row states its rank; species_rank: false is the flag to filter on before any arithmetic.

months — this measures photographers

Twelve integers, January first, counting records in that month across all years. It is extremely tempting to read the maximum as a season, and for most taxa that is wrong: 14% of every record in this corpus falls in July and 4% in January, because that is when people are outdoors with a camera. The shape you are looking at belongs to the observers before it belongs to the organism.

The fix is published alongside the data. corpus_month_baseline in field-data.json is the whole-corpus histogram; divide a taxon's curve by it and the shared seasonal effect cancels, leaving the part that is about the organism. The figures on our own pages draw both curves and the caption says which is which. And claim no peak below 120 records — under that the month-to-month wobble exceeds the signal, and our own tables print a dash instead of a month.

risk — absent does not mean safe

danger and caution flag harm to a person: a sting, a bite, a toxic berry. The field is absent on every entry of a key whose subject does not carry that kind of risk — a cloud, an arrowhead, a feather. An absent risk is the absence of a claim, never a clearance. If you are building anything that a person might act on, treat a missing flag as unknown.

What this API will not do

There is no endpoint that identifies a specimen, and there is not going to be one here. What the keys do is narrow: a reader supplies characters they have observed, and the key returns everything consistent with them. That list is a shortlist, and presenting it as an answer changes what the tool is. On five of these subjects — mushrooms, wild berries, snakes, caterpillars and plant toxicity — that change can put somebody in hospital.

So if you build on this, keep the alternatives visible and keep the confused_with sentence attached. It names the specific mistake and how to split the pair, which is the most useful thing in the row and the first thing a summary throws away.

Nothing here certifies that anything is safe to eat, touch or handle, and no endpoint returns a price, a valuation or an appraisal. For a suspected poisoning, a bite or a sting the correct answer is a poison centre or a doctor, immediately, rather than a lookup. In the United States that is Poison Control on 1-800-222-1222.

One copy of the arithmetic

The five field calculators are a single ES module, /js/engine.b78b80c8.js, and it is the same file in three places: the browser imports it on the calculator pages, the JSON endpoints import it at build time to publish the reference tables, and the MCP server imports it to compute. A tree of a given circumference and species therefore gets the same age from the page, from the table and from the tool, because there is nothing left to disagree.

That is worth stating because the ordinary shape of a site like this is three copies of the same formula — one in a browser script, one in an endpoint, one in a service — each individually correct, with nothing comparing them. The drift is invisible until somebody checks two answers against each other, which nobody does.

The five calculators, the function each is, and the page it is documented on.
FunctionComputesPage
treeAge Trunk circumference to tree age by the ISA growth-factor method. /tools/tree-age-calculator/
petAge Dog or cat age in human years on the AAHA life-stage curve. /tools/pet-age-calculator/
coinMelt Intrinsic metal value of a silver or gold coin at a spot price you supply. /tools/coin-melt-value-calculator/
mohsStep / mohsBracket A step of the Mohs scale, or the bracket a scratch test leaves you in. /tools/mohs-hardness-scale/
seedSchedule Indoor sowing and transplant dates for 34 crops from one frost date. /tools/seed-starting-calculator/

Reading the site without parsing HTML

Every indexable page has a markdown twin at roughly a quarter of the bytes — and far less than that on the key pages, where most of the HTML is filter-card markup. /tools/leaf-identifier/ is 226 KB of HTML for about 18 KB of text. Two ways to ask for it, because agent stacks are split about evenly between them:

curl https://orvik.app/tools/leaf-identifier/index.md
curl -H "Accept: text/markdown" https://orvik.app/tools/leaf-identifier/

/llms.txt indexes every page with its description. /llms-full.txt carries the full text of the keys, the data documentation and the app pages in one file. Four agent skills describe how to use each part of this correctly, including the mistakes above.

There is an MCP server at https://orvik.app/mcp (JSON-RPC 2.0 over HTTP POST; its card is at /.well-known/mcp/server-card.json) and an A2A endpoint at https://orvik.app/a2a. Both are read-only: they write nothing, spend nothing and expose no user data, which is why neither needs authentication. The tools are the lookups and the five calculators, and every one returns the source URL alongside the figure so an agent that used a tool still has a page to cite.

Licence, and one restriction that is easy to miss

The compilation — the key structure, the entry list, the trait coding, the joins and every derived column — is CC BY 4.0. Use it commercially, modify it, redistribute it; credit Orvik with a link.

The underlying records belong to four institutional databases and carry their own terms, which travel with any redistribution of the numbers. One of them is not CC BY. iNaturalist's observation metadata is CC BY-NC 4.0 — NonCommercial — so the iNaturalist columns inside field-data.json cannot be redistributed commercially even though the file around them can. Every row that carries one says so. It is the kind of thing that is only ever noticed if the payload states it, which is why it does.

  • GBIF — Global Biodiversity Information Facility — CC BY 4.0. Georeferenced occurrence counts, month histograms, country splits, accepted name and rank.
  • iNaturalist — CC BY-NC 4.0. Research-grade observation counts and month histograms. NonCommercial. This is the one upstream term that is not CC BY, and it travels with any redistribution of the iNaturalist columns. Orvik's own compilation is CC BY 4.0; the iNaturalist figures inside it are not.
  • Macrostrat — CC BY 4.0. Mineral formula, published Mohs range, crystal system and lustre.
  • The Paleobiology Database — CC BY 4.0. Fossil occurrence counts and first / last appearance in millions of years.

A suggested citation: Orvik open data, snapshot 2026-09-04. Vast Flow, LLP. https://orvik.app/api/ — with the source databases credited alongside, because the occurrence counts are theirs and not ours.

What is deliberately not here

An absent field reads as an oversight. A stated absence reads as an answer, so here is what we hold and do not publish, and why.

  • The competitor app snapshot behind /compare/ and /alternatives/. The metadata and in-app purchase lists come from the Apple App Store and are Apple's to license, not ours. The positioning is each publisher's own marketing. Republishing either keyless and CC BY would relicense content we do not own and distribute our competitors' copy at our own cost. https://orvik.app/compare/
  • Individual occurrence records — coordinates, dates, observer, photographs. Orvik holds only aggregate counts and month histograms. The records themselves are the upstream databases' to distribute, and they already do so properly. https://www.gbif.org/occurrence/search
  • Any identification made by the Orvik app, and any user photograph. None is retained in a form that could be published, and publishing it is not something a user agreed to. https://orvik.app/privacy/

The first of those is the one worth expanding on, because it is the obvious dataset for a site with 28 comparison pages and it is the one that must not ship. We keep a dated App Store snapshot of 21 identification apps, and it is genuinely good data — it is what lets /compare/ state a rival's in-app purchase list without anybody typing a price. But the metadata is Apple's to license and the positioning is each publisher's own marketing. Republishing it keyless and CC BY would relicense content we do not own and hand our competitors a machine-readable promo pack at our own expense, which travels through agents faster than any page. Judgement about other apps belongs on the comparison pages, where it is signed, has context, and concedes where they win — including the pages that recommend keeping a rival, or using no app at all.

The build enforces this rather than trusting it: a gate reads the rival snapshot and fails if any listing's name, developer, store URL, App Store id or in-app purchase price appears anywhere under /api/v1/. The next person to extend the generator will not have read this paragraph.

Stability, freshness and what breaks

The datasets are re-derived on every build from the same modules the pages render from, so there is no second copy to go stale. What can go stale is the field-data snapshot: it is a dated capture of four external databases, and status.json reports the capture date separately from the build date for exactly that reason. Cite the snapshot date, not the build. Our own gate fails the build once the snapshot passes 180 days.

Occurrence counts only ever grow, but coverage and the seasonal shapes do move, and a figure attributed to a date the source no longer agrees with is worse than no figure. If a number here disagrees with the source database today, the source database is right and this file is old.

URLs under /api/v1/ are stable. A breaking change to a payload shape would appear as /api/v2/ rather than silently altering v1, because a model that cited a URL last month is only a source of traffic if the URL still resolves and still means the same thing. New fields may be added to existing objects; treat unknown keys as ignorable rather than as an error.

Using it for something

If you build anything on this we would like to know, and we will link it: write to [email protected]. If a taxon resolved to the wrong name, a trait is coded wrongly, or a diagnostic sentence is simply incorrect, that is the more valuable message — the joins are automated and the failure mode of an automated join is a plausible wrong answer rather than an obvious one. We have already had to guard against GBIF walking up the tree when it cannot find a name, which once returned Tracheophyta and 540 million records for a Christmas cactus, in a file that looked entirely correct.

The whole method, including the parts that went wrong, is written up on the open data page. Who compiles this, and what he is not qualified in, is on the about page.