Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Live examples against the Terpedia Knowledge API

Every result on this page came from the public Terpedia Knowledge API at build time. Nothing is mocked. The same service powers kb.terpedia.com and chat.terpedia.com.

The endpoint used throughout is the open encyclopedia resolver:

GET https://terpedia-knowledge-nanrsdlaoa-uc.a.run.app/v1/cyc/{keyword}

It accepts a compound name, synonym, cultivar name, or protein name and returns:

KeyWhat it holds
entryThe resolved entity: label, type, identifiers, structure, provenance
entitiesRelated entities in the graph (up to 30)
statementsTyped relationships (found_in_taxon, contains, associated_with ...), each with sources
citationsThe literature and database records behind those statements
contextA plain-text evidence summary that begins with the instruction do not extend claims beyond the cited evidence

Partner endpoints (/v1/tabular/search, /v1/tabular/count, /v1/semantic/search, /v1/entities/{id}/neighbors) require an API key and are described in the white paper.

import json, sys

API = "https://terpedia-knowledge-nanrsdlaoa-uc.a.run.app"

def kb(keyword: str) -> dict:
    """Resolve a keyword with the Terpedia Knowledge API.

    Works in CPython (urllib) and in the browser under Pyodide (fetch).
    Returns {} when the API has no entry for the keyword (HTTP 404).
    """
    from urllib.parse import quote
    url = f"{API}/v1/cyc/{quote(keyword)}"
    if sys.platform == "emscripten":                       # JupyterLite / Pyodide
        from pyodide.http import open_url
        text = open_url(url).read()
        data = json.loads(text)
        return {} if "error" in data else data
    from urllib.request import urlopen
    from urllib.error import HTTPError
    try:
        with urlopen(url, timeout=60) as r:
            return json.load(r)
    except HTTPError as e:
        if e.code == 404:
            return {}
        raise

print("helper ready ->", API)
helper ready -> https://terpedia-knowledge-nanrsdlaoa-uc.a.run.app

1. Resolve a name to a chemical identity

The first thing any downstream system needs is an unambiguous identity. A trade name, a lab-report label and a literature synonym must all land on the same record. Terpedia returns the structure (InChI, InChIKey, SMILES) and the cross-references that let you join to PubChem, ChEBI, ChEMBL, CAS, KEGG, FooDB and others.

d = kb("limonene")
e = d["entry"]

print("label      :", e["label"])
print("type       :", e["type"])
print("formula    :", e.get("molecularFormula"))
print("SMILES     :", e.get("smiles"))
print("InChIKey   :", e["identifiers"].get("inchiKey"))
print("class      :", e.get("taxonomy", {}).get("direct_parent"))
print()
print("cross-references:")
for k, v in sorted(e["identifiers"].items()):
    print(f"  {k:12s} {v}")
label      : (-)-Limonene
type       : chemical
formula    : C10H16
SMILES     : CC(=C)[C@@H]1CCC(C)=CC1
InChIKey   : XMGQYMWWDOXHJM-JTQLQIEISA-N
class      : Menthane monoterpenoids

cross-references:
  biocyc       CPD-4886
  cannabisdb   CDB005271
  cas          5989-27-5
  chebi        15382
  chembl       CHEMBL15799
  chemspider   389747
  drugbank     DB08921
  foodb        FDB006329
  inchiKey     XMGQYMWWDOXHJM-JTQLQIEISA-N
  kegg         C06099
  knapsack     C00010868
  pubchem      440917
  pubchemCid   22311
  wikidata     Q278809
  wikipedia    Limonene

Every record carries its own provenance: which dataset it came from, the source record id, when it was retrieved, and under which license it may be reused. That last field matters for commercial users; it is how Terpedia keeps license-scoped sources visible instead of laundering them into one undifferentiated table.

p = e["provenance"]
for k in ("datasetId", "datasetVersion", "sourceRecordId", "curationStatus",
          "licenseTerms", "license", "retrievedAt"):
    print(f"{k:16s} {p.get(k)}")
datasetId        cannabisdb
datasetVersion   1.0-a7d05067088ee5b8
sourceRecordId   CDB005271
curationStatus   source_curated
licenseTerms     CC BY-NC 4.0
license          https://creativecommons.org/licenses/by-nc/4.0/
retrievedAt      2026-09-06T11:18:33.538Z

2. Why identity resolution is hard: one molecule, dozens of names

Limonene alone arrives under many names in COAs, ingredient lists, and papers. The alias list is what lets a search for d-limonene, (+)-limonene, dipentene, or a CAS number resolve to the same node.

aliases = e["aliases"]
print(len(aliases), "aliases on record. A sample:")
for a in aliases[:18]:
    print("  -", a)
58 aliases on record. A sample:
  - (+)-(4R)-Limonene
  - (+)-(R)-Limonene
  - (+)-4-Isopropenyl-1-methylcyclohexene
  - (+)-Limonene
  - (4R)-1-Methyl-4-isopropenylcyclohex-1-ene
  - (4R)-4-Isopropenyl-1-methylcyclohexene
  - (R)-(+)-Limonene
  - (R)-(+)-p-Mentha-1,8-diene
  - (R)-1-Methyl-4-(1-methylethenyl)cyclohexene
  - (R)-4-Isopropenyl-1-methyl-1-cyclohexene
  - (R)-p-Mentha-1,8-diene
  - 4BetaH-p-mentha-1,8-diene
  - D-(+)-Limonene
  - D-Limonen
  - AISA 5203-L (+)limonene
  - Dipentene
  - (-)-Limonene
  - 1-Methyl-4-(1-methylethenyl)cyclohexene

3. Statements with sources: what is this molecule associated with?

Beyond identity, the graph holds typed statements. For a compound these include taxa it has been reported in (found_in_taxon), protein associations from the source database (associated_with), and class membership. Each statement is a claim with a source, not a fact about biology. Note the evidenceType field: source_curated_protein_association tells you exactly what kind of evidence you are looking at.

from collections import Counter

stmts = d["statements"]
print("statement predicates for limonene:")
for pred, n in Counter(s["predicate"] for s in stmts).most_common():
    print(f"  {n:3d}  {pred}")

print()
print("protein associations (source-curated, not efficacy):")
for s in stmts:
    if s["predicate"] == "associated_with":
        src = (s.get("sources") or [{}])[0]
        print(f"  {s['subjectLabel']} -> {s['objectLabel']:<32s} "
              f"[{src.get('evidenceType')}] {src.get('url')}")
statement predicates for limonene:
   20  found_in_taxon
    3  part_of
    2  associated_with
    2  has_part
    1  classified_as
    1  described_as
    1  has_molecular_formula
    1  image
    1  mass
    1  canonical_smiles
    1  inchi
    1  inchi_key
    1  chemical_formula
    1  subclass_of
    1  instance_of
    1  chembl_id
    1  pubchem_cid

protein associations (source-curated, not efficacy):
  (-)-Limonene -> Cytochrome P450 2C9              [source_curated_protein_association] https://cannabisdatabase.ca/proteins/CDBP00973/compound_protein_links
  (-)-Limonene -> Cytochrome P450 2C19             [source_curated_protein_association] https://cannabisdatabase.ca/proteins/CDBP00975/compound_protein_links

4. Resolve a protein target

The same resolver handles proteins. CB2 lands on the reviewed UniProt entry P34972 from Terpedia’s curated core graph; CYP2C9 and TRPV1 come from the CannabisDatabase.ca protein set and bring their compound associations with them.

for name in ("CB2", "CYP2C9", "TRPV1"):
    r = kb(name)
    if not r:
        print(f"{name}: no entry")
        continue
    ent = r["entry"]
    assoc = [s for s in r.get("statements", []) if s["predicate"] == "associated_with"]
    print(f"{name:7s} -> {ent['label']:<48s} id={ent['id']:<16s} "
          f"source={ent['provenance']['datasetId']:<24s} associations={len(assoc)}")
CB2     -> Cannabinoid receptor 2                           id=uniprot:P34972   source=terpedia-curated-core    associations=1
CYP2C9  -> Cytochrome P450 2C9                              id=CDBP00973        source=cannabisdb               associations=16
TRPV1   -> Transient receptor potential cation channel subfamily V member 1 id=CDBP01848        source=cannabisdb               associations=6
r = kb("CYP2C9")
print("compounds associated with", r["entry"]["label"], "in the source graph:")
seen = set()
for s in r["statements"]:
    if s["predicate"] == "associated_with" and s["subjectLabel"] not in seen:
        seen.add(s["subjectLabel"])
        print("  -", s["subjectLabel"])
compounds associated with Cytochrome P450 2C9 in the source graph:
  - Delta-9-tetrahydrocannabinol
  - Limonene
  - Formic acid
  - NADP
  - NADPH
  - Oxygen
  - Salicylic acid
  - Water
  - Rutin
  - Hydrogen sulfide
  - Heme
  - (-)-Limonene
  - Dopamine
  - Paraxanthine
  - Nicotine
  - Ethanol

5. Compare a panel of terpenes in one pass

A formulator or QA lead rarely wants one molecule. This loop pulls a small panel and shows how much the graph currently knows about each: formula, mass, number of reported taxa, number of statements, and which dataset supplied the primary record. Where a record is thin, the table says so; a missing value is reported as missing, not filled in.

panel = ["myrcene", "limonene", "linalool", "alpha-pinene", "caryophyllene",
         "humulene", "valencene", "nootkatone"]

rows = []
for name in panel:
    r = kb(name)
    if not r:
        rows.append((name, "not found", "", "", "", "", ""))
        continue
    ent = r["entry"]
    st = r.get("statements", [])
    taxa = sum(1 for s in st if s["predicate"] == "found_in_taxon")
    mass = ""
    for s in st:
        if s["predicate"] == "mass":
            raw = str(s.get("objectLabel") or s.get("objectValue") or s.get("value") or "")
            mass = raw.split()[0] if raw.split() else ""
            break
    rows.append((name, ent["type"], ent.get("molecularFormula") or "", mass,
                 str(taxa), str(len(st)), ent["provenance"]["datasetId"]))

hdr = ("keyword", "type", "formula", "mass", "taxa", "stmts", "primary source")
w = [max(len(str(x[i])) for x in rows + [hdr]) for i in range(len(hdr))]
print("  ".join(h.ljust(w[i]) for i, h in enumerate(hdr)))
print("  ".join("-" * w[i] for i in range(len(hdr))))
for row in rows:
    print("  ".join(str(c).ljust(w[i]) for i, c in enumerate(row)))
keyword        type                    formula  mass        taxa  stmts  primary source           
-------------  ----------------------  -------  ----------  ----  -----  -------------------------
myrcene        chemical                C10H16   136.125201  25    40     cannabisdb               
limonene       chemical                C10H16   136.125201  20    40     cannabisdb               
linalool       terpene                 C₁₀H₁₈O  154.135765  27    40     terpedia-terpene-registry
alpha-pinene   terpene                 C₁₀H₁₆   136.125     27    40     terpedia-terpene-registry
caryophyllene  essential_oil_compound  C₁₅H₂₄   204.187801  28    40     terpedia-terpene-registry
humulene       chemical                C15H24   204.187801  24    40     cannabisdb               
valencene      essential_oil_compound  C₁₅H₂₄   204.188     29    40     terpedia-terpene-registry
nootkatone     essential_oil_compound  C₁₅H₂₂O  218.167065  25    35     terpedia-terpene-registry

6. From a cultivar name to quantified chemistry, with the paper behind it

Cultivar names are notoriously unreliable, so Terpedia never treats a name as chemistry. What it can do is return the measured records that exist for a name, each with its quantification, status, and the publication it came from. Here the contains statements for Blue Dream carry concentrations in mg/g dry weight and resolve to a PubMed identifier. A compound appears once per measured sample, so repeated rows are separate measurements, not duplicates.

r = kb("Blue Dream")
ent = r["entry"]
print(ent["label"], "-", ent["type"], "-", ent["id"])
print()
print(f"{'compound':<36s} {'value':<26s} status")
for s in r["statements"][:14]:
    if s["predicate"] != "contains":
        continue
    q = s.get("qualifiers", {})
    print(f"{s['objectLabel']:<36s} {q.get('value',''):<26s} {q.get('status','')}")

src = (r["statements"][0].get("sources") or [{}])[0]
print()
print("source:", src.get("title", "")[:140], "...")
print("pmid  :", src.get("pmid"), "->", src.get("url"))
print("evidence type:", src.get("evidenceType"))
Blue Dream - cannabis_cultivar - STRAIN0007

compound                             value                      status
Cannabichromene                      1.3 +/- 0.1 mg/g dry wt    Detected and Quantified
Cannabidiol                          0.5 +/- 0.1 mg/g dry wt    Detected and Quantified
Delta-9-tetrahydrocannabinol         109.9 +/- 24.1 mg/g dry wt Detected and Quantified
Delta-9-tetrhydrocannabivarin        0.3 +/- 0.1 mg/g dry wt    Detected and Quantified
Delta-9-cis-tetrahydrocannabinol     109.9 +/- 24.1 mg/g dry wt Detected and Quantified
(+)-nerolidol                        0.353 mg/g dry wt          Detected and Quantified
(+)-nerolidol                        0.414 mg/g dry wt          Detected and Quantified
(+)-nerolidol                        0.794 mg/g dry wt          Detected and Quantified
(+)-nerolidol                        0.8 mg/g dry wt            Detected and Quantified
beta-Myrcene                         2.146 mg/g dry wt          Detected and Quantified
beta-Myrcene                         2.405 mg/g dry wt          Detected and Quantified
beta-Myrcene                         5.113 mg/g dry wt          Detected and Quantified
beta-Myrcene                         6.512 mg/g dry wt          Detected and Quantified
beta-Myrcene                         7.5 +/- 2.8 mg/g dry wt    Detected and Quantified

source: Richins RD, Rodriguez-Uribe L, Lowe K, Ferral R, O'Connell MA: Accumulation of bioactive metabolites in cultivated medical Cannabis. PLoS On ...
pmid  : 30036388 -> https://pubmed.ncbi.nlm.nih.gov/30036388/
evidence type: curated_cultivar_concentration

7. Where the compound has been reported: occurrence statements

found_in_taxon statements link a compound to organisms in which it has been reported. An occurrence is a report that a compound was detected in an organism in a cited work. It is not a concentration and not evidence that the organism is a meaningful commercial source. The statement tells you where the record came from and under what license. Where a taxon label has not yet been resolved, the bare Wikidata identifier is shown; that coverage gap is visible rather than papered over.

r = kb("linalool")
occ = [s for s in r["statements"] if s["predicate"] == "found_in_taxon"]
print(len(occ), "occurrence statements returned for linalool (page 1). First ten:")
for s in occ[:10]:
    src = (s.get("sources") or [{}])[0]
    print(f"  {s['objectLabel']:<32s} {src.get('datasetId'):<10s} {src.get('license')}")
27 occurrence statements returned for linalool (page 1). First ten:
  Baccharis dracunculifolia        wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Pilocarpus grandiflorus          wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Myrcianthes cisplatensis         wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Q15583465                        wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Q15586596                        wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Q13939086                        wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Q536839                          wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Q15583174                        wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Q3595850                         wikidata   https://creativecommons.org/publicdomain/zero/1.0/
  Q28801933                        wikidata   https://creativecommons.org/publicdomain/zero/1.0/

8. The evidence boundary is part of the payload

Every response includes a context block written for downstream language models. It opens with an explicit instruction not to extend claims beyond the cited evidence, then lists the statements with their sources. This is how the chat agents at chat.terpedia.com are kept honest: the knowledge layer, not the prompt, sets the boundary.

ctx = kb("limonene")["context"]
print(ctx[:1100])
print("...")
Verified Terpedia semantic graph (do not extend claims beyond the cited evidence):
- (-)-Limonene --associated_with--> Cytochrome P450 2C9 [https://cannabisdatabase.ca/proteins/CDBP00973/compound_protein_links]
- (-)-Limonene --associated_with--> Cytochrome P450 2C19 [https://cannabisdatabase.ca/proteins/CDBP00975/compound_protein_links]
- (-)-Limonene --classified_as--> Menthane monoterpenoids [https://cannabisdatabase.ca/compounds/CDB005271]
- (-)-Limonene --described_as--> d-Limonene, also known as dipentene, belongs to the class of organic compounds known as menthane monoterpenoids. These are monoterpenoids with a structure based on the o-, m-, or p-menthane backbone. P-menthane consists of the cyclohexane ring with a methyl group and a (2-methyl)-propyl group at the 1 and 4 ring position, respectively. The o- and m- menthanes are much rarer, and presumably arise by alkyl migration of p-menthanes. Thus, D-limonene is considered to be an isoprenoid lipid molecule. d-Limonene is a very hydrophobic molecule, practically insoluble (in water), and relatively neutral. (-)-Limonene is e
...

9. Honest misses

A keyword the graph cannot resolve returns HTTP 404 and {"error": "entry not found"} rather than a fuzzy guess. That is deliberate. Coverage is versioned and expanding; the counting reports in the white paper describe exactly what is and is not in scope today.

for k in ("thujone", "artemisinin", "not-a-real-compound"):
    print(f"{k:22s} ->", "found" if kb(k) else "no entry (404)")
thujone                -> no entry (404)
artemisinin            -> no entry (404)
not-a-real-compound    -> no entry (404)

10. The same data, rendered

The entity pages on kb.terpedia.com are built from the same responses shown above.

Figure 1:The live limonene entity page at kb.terpedia.com.

Ask a question in natural language at chat.terpedia.com, or browse the graph by category (terpenes, molecules, proteins, enzymes, reactions, pathways, products, ingredients, diseases, organisms) at kb.terpedia.com.