EMDB Database
Overview
The Electron Microscopy Data Bank (EMDB) at EBI archives 3D electron microscopy density maps — primarily cryo-EM and cryo-ET — for macromolecular assemblies (30,000+ entries: ribosomes, membrane proteins, viruses, large complexes). Access is split across two services:
- EMDB Entry API (
https://www.ebi.ac.uk/emdb/api/entry/{EMD-XXXXX}) — the canonical per-entry JSON containing metadata, map header, fitted PDB list, and citation. Sub-endpoints like/map,/fitted,/publicationsdo not exist — all those data live inside the single entry response. - EBI Search WS (
https://www.ebi.ac.uk/ebisearch/ws/rest/emdb) — the real keyword search backend (the barehttps://www.ebi.ac.uk/emdb/api/search/endpoint ignores the query and just returns recent entries).
No authentication or API key is required.
When to Use
- Finding cryo-EM density maps by keyword (e.g., "spike protein", "ribosome 70S")
- Fetching the download URL of a
.map.gzdensity file for use in ChimeraX / PyMOL - Identifying fitted PDB atomic models for an EMDB map (and the reverse)
- Retrieving entry metadata — resolution, reconstruction method, organism, sample
- Listing cryo-EM structures filtered by organism or resolution cutoff
- Pulling the primary citation (journal, DOI, PubMed ID) for an EMDB entry
- Use
pdb-databaseinstead when you need experimentally determined atomic coordinates - Use
alphafold-database-accessfor AI-predicted structures; EMDB is for experimental EM maps only
Prerequisites
- Python packages:
requests,pandas,matplotlib - Data requirements: EMDB entry IDs (
EMD-XXXXX), keyword search strings, or PDB IDs for cross-referencing - Environment: internet connection; no API key
- Rate limits: no official published limits; add
time.sleep(0.2)between requests in batch loops for polite access
pip install requests pandas matplotlib
Quick Start
import requests
EMDB_API = "https://www.ebi.ac.uk/emdb/api"
EBI_SEARCH = "https://www.ebi.ac.uk/ebisearch/ws/rest/emdb"
# Keyword search via EBI Search WS (NOT /emdb/api/search/, which ignores q=)
r = requests.get(EBI_SEARCH,
params={"query": "sars-cov-2 spike", "size": 5, "format": "json",
"fields": "id,name,resolution,em_method,organism"},
timeout=30)
r.raise_for_status()
res = r.json()
print(f"Total hits: {res['hitCount']}")
for e in res["entries"]:
f = e["fields"]
name = (f.get("name") or [""])[0][:60]
resol = (f.get("resolution") or ["?"])[0]
print(f" {e['id']}: {name} ({resol} Å)")
Core API
Query 1: Keyword Search (EBI Search WS)
Returns a paged hit list keyed by EMDB ID, with the requested fields per entry.
import requests, pandas as pd
EBI_SEARCH = "https://www.ebi.ac.uk/ebisearch/ws/rest/emdb"
def emdb_search(query, size=20, start=0,
fields="id,name,resolution,em_method,organism"):
r = requests.get(EBI_SEARCH,
params={"query": query, "size": size, "start": start,
"format": "json", "fields": fields},
timeout=30)
r.raise_for_status()
return r.json()
data = emdb_search("ribosome 70S", size=10)
rows = []
for e in data["entries"]:
f = e["fields"]
rows.append({
"emdb_id": e["id"],
"name": (f.get("name") or [""])[0],
"resolution_A": float((f.get("resolution") or [0])[0] or 0) or None,
"em_method": (f.get("em_method") or [""])[0],
"organism": (f.get("organism") or [""])[0],
})
df = pd.DataFrame(rows)
print(f"hitCount={data['hitCount']}; first {len(df)} rows:")
print(df.to_string(index=False))
# Paged retrieval — iterate `start` until exhausting hitCount
def emdb_search_all(query, size=100, max_pages=5,
fields="id,name,resolution,em_method"):
out = []
for page in range(max_pages):
d = emdb_search(query, size=size, start=page * size, fields=fields)
if not d["entries"]:
break
out.extend(d["entries"])
if len(out) >= d["hitCount"]:
break
return out
hits = emdb_search_all("ferritin", size=50, max_pages=2)
print(f"Pulled {len(hits)} ferritin entries")
Query 2: Entry Metadata
The single entry endpoint returns all metadata, the map header, fitted PDB list, and the citation in one document. Read field paths carefully — most are nested.
import requests
EMDB_API = "https://www.ebi.ac.uk/emdb/api"
def emdb_entry(emdb_id):
r = requests.get(f"{EMDB_API}/entry/{emdb_id}", timeout=30)
r.raise_for_status()
return r.json()
e = emdb_entry("EMD-30210") # nsp12-nsp7-nsp8 + Remdesivir (RdRp)
print(f"Title : {e['admin']['title']}")
print(f"Status : {e['admin']['current_status']}")
print(f"Key dates : {e['admin'].get('key_dates')}")
sd = e["structure_determination_list"]["structure_determination"][0]
ip = sd["image_processing"][0]
res = ip["final_reconstruction"]["resolution"]
print(f"Method : {sd['method']}")
print(f"Resolution : {res['valueOf_']} {res['units']} (type: {res['res_type']})")
Query 3: Map Header / Download Info
entry["map"] contains the file name, format, voxel grid, axis order, cell, contour level(s), and recommended display threshold.
import requests
EMDB_API = "https://www.ebi.ac.uk/emdb/api"
e = requests.get(f"{EMDB_API}/entry/EMD-30210", timeout=30).json()
m = e["map"]
print(f"Map file : {m.get('file')}") # e.g. emd_30210.map.gz
print(f"Format : {m.get('format')}") # 'CCP4'
print(f"Dimensions : {m.get('dimensions')}") # {'col': ..., 'row': ..., 'sec': ...}
print(f"Axis order : {m.get('axis_order')}") # {'fast': 'X', 'medium': 'Y', 'slow': 'Z'}
print(f"Contour list : {m.get('contour_list')}") # recommended threshold(s)
# Conventional FTP/HTTPS download URL pattern (mirror at EBI):
EMDB_FTP = "https://ftp.ebi.ac.uk/pub/databases/emdb/structures"
emdb_num = "EMD-30210"
print(f"Download URL : {EMDB_FTP}/{emdb_num}/map/{m['file']}")
Query 4: Fitted PDB Atomic Models
EMDB cross-references the PDB entries that fitted into the map. Each item has the PDB ID and a relationship tag (e.g., FULLOVERLAP).
import requests
EMDB_API = "https://www.ebi.ac.uk/emdb/api"
e = requests.get(f"{EMDB_API}/entry/EMD-30210", timeout=30).json()
pdblist = e.get("crossreferences", {}).get("pdb_list", {})
pdb_refs = pdblist.get("pdb_reference", []) if isinstance(pdblist, dict) else []
print(f"Fitted PDB entries: {len(pdb_refs)}")
for ref in pdb_refs:
in_frame = (ref.get("relationship") or {}).get("in_frame")
print(f" {ref['pdb_id'].upper():6s} relationship={in_frame}")
# 7BV2 relationship=FULLOVERLAP
Query 5: Citation and Publications
Primary citation lives at entry["crossreferences"]["citation_list"]. The shape varies by citation type (journal vs. preprint).
import requests
EMDB_API = "https://www.ebi.ac.uk/emdb/api"
e = requests.get(f"{EMDB_API}/entry/EMD-30210", timeout=30).json()
citations = e.get("crossreferences", {}).get("citation_list", {})
primary = citations.get("primary_citation", {})
ct = primary.get("citation_type") or primary
title = (ct.get("title") or "").strip()
journal = ct.get("journal") or ct.get("book_title")
year = ct.get("year")
authors = [a.get("name") or a.get("name_str")
for a in (ct.get("author") or ct.get("author_order") or [])
if a]
xrefs = ct.get("external_references", []) or ct.get("xref", [])
doi = next((x.get("valueOf_") for x in xrefs if x.get("type") == "DOI"), None)
pmid = next((x.get("valueOf_") for x in xrefs if x.get("type") == "PUBMED"), None)
print(f"Title : {title[:80]}")
print(f"Journal : {journal} ({year})")
print(f"Authors : {', '.join(a for a in authors[:5] if a)}{'...' if len(authors) > 5 else ''}")
print(f"DOI : {doi}")
print(f"PMID