Skip to content
taxon-sync

API

A small public, versioned JSON API — six endpoints (/translate, /taxon, /versions, /diff, /dataset, /health), the same engine behind the web translator. CORS-open, read-only, no key required. Every name traces to an official release file; nothing is invented.

Base URL https://taxon-sync.vercel.app

Try it

A live request against the real endpoint. Type a name or an NCBI taxid.

Endpoint

GET  /api/v1/translate?input=<query>
POST /api/v1/translate     body: {"input": "line1\nline2"}  (or text/plain)

A query is a name (Bacteroides vulgatus) or an NCBI taxid (821). For many queries, repeat &input= or newline-separate them (POST). Capped at 2000.

Gotcha

; > | are lineage separators inside a single query (it collapses to the deepest filled segment, skipping empty ranks and strain ids) — they do not split into multiple queries. Use newlines or repeated &input= for that.

Choosing a release

Taxonomy drifts between releases of the same database. Pass gtdb, silva, gg2 or ehomd to translate against a specific one (default = current). The releases block in the response echoes exactly what was used. An unknown release returns 400 with the valid list. A release marked subset in /api/v1/versions was built from a clustered subset export (SILVA 144 ships only its Ref NR 99 taxmap): a taxon missing from it has not been dropped by that database, so drift never reports it as absent there, and a /diff onto it counts those taxa in lostFromTo without that meaning a removal. A release with a scope (eHOMD covers the human oral and aerodigestive microbiome) is complete for that scope only, so a missing taxon there is reported as out-of-scope-or-not-present, never as dropped.

# same organism, two GTDB releases — see the reclassification
curl "https://taxon-sync.vercel.app/api/v1/translate?input=Buchnera%20aphidicola&gtdb=R220"
#   → "Buchnera aphidicola_K"
curl "https://taxon-sync.vercel.app/api/v1/translate?input=Buchnera%20aphidicola&gtdb=R226"
#   → "Buchnera_F aphidicola_K"   (genus reassignment)

# which releases exist?
curl "https://taxon-sync.vercel.app/api/v1/versions"

# what changed for a whole database between two releases?
curl "https://taxon-sync.vercel.app/api/v1/diff?db=gtdb&from=R220&to=R226"
#   → { summary: { renamed: 6314, gainedInTo: 11242, ... }, renamed: [...] }

A bare number is read as an NCBI taxid. Pass idSpace=ehomd (query, or idSpace in a JSON body) to read it as an eHOMD HMT id instead: 389 is HMT-389, bound to its NCBI taxon through eHOMD's own NCBI Taxon ID column, and the row reports matchType ehomd. A number eHOMD has no record for is none; it is never re-read as an NCBI taxid. Names are unaffected. Greengenes2 ids are not offered: they are sequence features, millions per release. An unknown value returns 400.

curl "https://taxon-sync.vercel.app/api/v1/translate?input=389&idSpace=ehomd"
#   → taxid 46125, Abiotrophia defectiva (HMT-389)

Each release in /api/v1/versions carries a source object recording what was ingested: the upstream url, every raw input file with its sha256 and size in bytes, and the recordCount of taxids in the release. The NCBI spine has the same under spineSource. Each release also carries released, the year and month the database published it (YYYY-MM), omitted when the database publishes no date, and releasedFrom, which says where that month came from or why there is none. The same object appears on from and to in /api/v1/diff. Hashes are of the files as the build read them. Some cannot be checked against the upstream download: GTDB releases before R220 (R207, R214) ship as a .tar.gz that is unpacked and re-gzipped first, and the spine hashes four .dmp files extracted from the taxdump.tar.gz its url names. Their hashes will not match the upstream download. The spine's url is overwritten daily, so its note says to identify the snapshot by the hashes.

Health check

GET /api/v1/health

Returns ok, the spine label (the same string as on /versions), the spineId pack id, the defaults and the held releases per database. It reads the release manifest only and loads no data, so it is safe to poll. It is never cached. If the manifest cannot be read it answers 503 with { "ok": false, "error": "..." }.

Single taxon

GET /api/v1/taxon?taxid=821
GET /api/v1/taxon?name=Bacteroides%20vulgatus
GET /api/v1/taxon?taxid=821&gtdb=R226      # restrict to given releases

Everything every held release says about one taxon — the data behind the /t/<taxid> pages. Pass taxid or name (names resolve through the same lookup as /translate, so redirects and GTDB suffixes behave identically; a name that means two or more NCBI taxa answers 404 with candidates). Optional gtdb / silva / gg2 / ehomd pins restrict the result to those releases. Returns taxid, sciName, lineage, knownAs (synonyms/equivalents) and cells — one entry per database release, each with the name, split parts, genome count, source URL, ranks (the lineage one level at a time, each with the rank label its source gave it, or null; never inferred from position; the view carries the NCBI one too) and via (how that release was joined to the taxon — see below). drift lists the databases that renamed or dropped the taxon across the releases we hold, the same hint as rows[].drift on /translate; it is omitted when every database is stable. 404 when nothing matches; unknown release → 400.

How the databases are joined

Every row is anchored to one NCBI taxid, and the databases reach that anchor by different routes. This is why two names can look nothing alike and still be the same organism — most of the joins never look at a name at all.

  • GTDB — by identifier. Its genome metadata carries NCBI taxids. Where the genome's raw id names a strain, GTDB's own ncbi_species_taxid column is used instead: GTDB's statement about its own data outranks anything we could infer.
  • eHOMD — by identifier. Its taxon table has an explicit NCBI Taxon ID column.
  • Greengenes2 — via GTDB genome accessions. GG2 feature ids embed a GCA/GCF genome accession, which we resolve through the GTDB release GG2 was actually built on (2024.09 → R220). The link is a physical genome, so the two names may differ completely and it still holds.
  • SILVA — by name. The taxmap we ingest carries no NCBI taxid, so the leading binomial of the organism name is matched against NCBI's name index. This is the weakest join we ship and the only one where a name mismatch means no row at all, rather than silently substituting a different organism's.

Two repairs apply to the identifier joins. A taxid retired since that release was cut is followed forward through NCBI's merged.dmp. A taxid naming a strain or subspecies is walked up NCBI's tree to its species ancestor — the walk stops at species and never goes higher, so nothing is ever bound to a genus or a family to make a match happen.

Each cell says which route it took, on the /t/<taxid> page in words and in /api/v1/taxon as cells[db][].via = { kind, steps?, backbone? } — taxid (exact), species-col, merged, rank-walk (with steps), name, accession (with the GTDB backbone). A cell covers many source records, so it reports the weakest route any of them took rather than the flattering one. Bulk /translate rows carry the same thing as via[db], and for every empty cell an absent[db] reason, so no blank column is left to guesswork. The web translator's TSV export writes both into one <db>_<release>_evidence column per result column.

Download everything

The entire cross-database mapping table as one TSV — every taxon we hold, one row each. If you were about to loop this API a few hundred thousand times, take the file instead.

↓ taxon-sync.tsv — ~97,000 rows, ~10 MB

# current default releases
curl -O -J "https://taxon-sync.vercel.app/api/v1/dataset"

# pinned to specific releases (the header row and the filename name them)
curl -O -J "https://taxon-sync.vercel.app/api/v1/dataset?gtdb=R226&silva=138.2"

Columns are ncbi_taxid, ncbi_name, then one per database named for its release (gtdb_R232, silva_138.2, gg2_2024.09, ehomd_4.2) with _status / _split_count alongside where that database has them — the same naming the translator's TSV export uses. Split lineages are written out in full (A / B / C), not summarised as (+n). The gtdb / silva / gg2 / ehomd params pin releases exactly as on /translate; unknown release → 400. The file carries names and status, not the per-record lineage a taxon page shows — and lineage is the only thing separating SILVA 138.1 from 138.2, so pinning that one changes the column heading and nothing else. Row count is echoed in the x-row-count header, and the response is cached hard at the edge.

Licence

The sources are licensed open / free for use, and so is this reconciliation (MIT). Attribute NCBI Taxonomy, GTDB, SILVA, Greengenes2 and eHOMD on publication — cite the releases named in the header row, not just “taxon-sync”.

Examples

curl

curl "https://taxon-sync.vercel.app/api/v1/translate?input=Bacteroides%20vulgatus"

# batch (POST)
curl -X POST "https://taxon-sync.vercel.app/api/v1/translate" \
  -H "content-type: application/json" \
  -d '{"input": "821\nEscherichia coli"}'

JavaScript

const res = await fetch(
  "https://taxon-sync.vercel.app/api/v1/translate?input=" + encodeURIComponent("821")
);
const { rows } = await res.json();
rows[0].row.gtdb; // "Phocaeicola vulgatus / ..."

Python (the package)

# not yet on PyPI (publication coming) — install from source:
pip install "git+https://github.com/JamesDimonaco/taxon-sync.git#subdirectory=python"

from taxon_sync import lookup, translate

t = lookup("821")            # or "Bacteroides vulgatus"
t.gtdb                        # "Phocaeicola vulgatus / ..."
t.matched                     # True

res = translate(["821", "Escherichia coli"])
res.count["matched"]

Response

FieldTypeNotes
apiVersionstringAlways "1". Bump = breaking change (won't happen in v1).
releasesobjectThe release each DB column derives from (ncbi, gtdb, silva, gg2, ehomd). Pin against these.
countobjectTallies: total, matched, gtdb, silva, gg2, ehomd, splits, redirected, reverseLookups, deleted, ambiguous, closest, drift, truncated.
rows[]Taxon[]One entry per query, in order.
rows[].inputstringThe query as you sent it.
rows[].normalisedstringThe query after normalisation.
rows[].matchTypeenumtaxid · ncbi · gtdb · silva · gg2 · ehomd · redirected · deleted · none. A bare number read with idSpace=ehomd reports ehomd.
rows[].taxidstring?Resolved NCBI taxid, when matched.
rows[].hmtstring?With idSpace=ehomd, the HMT the number named ("HMT-058"). Several HMTs can share one species, so the eHOMD cell may show a sibling HMT's name; this is the one asked for.
rows[].rowobject?Confirmed cross-DB names: ncbi, gtdb, silva, gg2, ehomd. Splits read 'A / B / C (+n)'.
rows[].candidatesarray?The binomial names two or more NCBI taxa at the same name class, so no taxid was chosen: matchType stays none and no cell is filled. Each { taxid, name } is a taxon it could mean; re-submit the taxid you mean.
rows[].nameClassenum?For matchType ncbi: current · synonym · equivalent (the same words as /taxon knownAs[].class). A synonym hit is an alias lookup, not an identity match on the current name.
rows[].viaobject?Per database with a filled cell: how that release was joined to the NCBI taxon, { kind, steps?, backbone? } (see Joins below).
rows[].absentobject?Per database with an empty cell: why. not-covered (subset export) · in-other-releases (another release we hold names it) · out-of-scope-or-not-present (scoped database, e.g. eHOMD) · not-found. Omitted when nothing is empty.
rows[].closestarray?Approximate fills for empty columns. Not authoritative — verify by taxid.
rows[].driftarray?Databases that renamed (or dropped) this taxon across the releases we hold, including ones this request didn't select: { db, kind: renamed|absent, releases[] }. For renamed, releases[] holds a name in each; for absent, releases[] is the ones holding NOTHING. One entry per database, absent taking precedence over renamed; an absent entry also carries alsoRenamed[] (the releases holding a name) when those releases disagree on it. Precomputed — see /taxon or /diff for the names.

Full schema: /api/v1/openapi.json (OpenAPI 3.1).

Notes

  • /api/translate (unversioned) is a backward-compat alias of /api/v1/translate. New code should target /v1.
  • The v1 contract is frozen — fields get added, never removed or renamed.
  • closest fills are best-effort (nearest record a DB holds by genus + epithet), always approximate. Confirmed names live in row.