
when working with the human genome, it is useful to confirm all your information using the many public APIs that researchers trust.
These are the APIs i use to build custom scripts and programs that help me get information quickly and reliably.
worked example throughout is
ABCC8 c.3989-9G>A.
the picture, if this is all new: your dna is a book about 3 billion letters long, written in a four-letter alphabet, A C G T. every cell carries a copy. a "variant" is a typo, one letter swapped for another, and some typos cause disease. to fix a typo you have to know exactly which letter, and "the 9th letter before chapter 33" only helps if everyone agrees where the chapters start. they don't always. so the whole job below is turning a vague, edition-dependent address into one exact physical address, then checking it from a few directions so you know it's right before you go cutting.
clinvar is a big public logbook of known typos and whether each one is harmless or causes disease. you look yours up and it hands back the typo's exact physical address on the chromosome plus what's known about it. the address style you want is called SPDI, and it is exact on purpose: which chromosome, which position, the letter that's there, the letter it became. one thing that trips everyone up: the gene is written along the chromosome backwards, on the opposite strand, so the same typo reads as G>A when you read the gene but C>T when you read the chromosome. same event, seen from two directions.
pull the record (id 9088 = VCV000009088):
eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=clinvar&id=9088&retmode=json
take the SPDI, NC_000011.10:17397054:C:T. it is 0-based, so that position is
chr11:17,397,055 1-based. it reads C>T, not G>A, because ABCC8 is on the minus
strand. also grab the classification (pathogenic) and the transcript clinvar anchored to,
NM_000352.6.
docs: e-utilities · clinvar help · spdi / variation services
before the cell uses a gene it cuts out chunks and splices the rest together. the kept chunks are exons, the tossed ones are introns. "c.3989-9" means "9 letters before exon 33 starts," which only means something if you and the cell agree on which chunk is exon 33, and there is more than one way to slice a gene. MANE Select is the single slicing that the two big databases, refseq and ensembl, agreed to treat as canonical. lock onto it and your exon numbers match everyone else's. use a different one and "exon 33" might be a different piece of dna and your whole design slides sideways. worth knowing here: this typo sits in an intron right at the seam where it meets an exon. the cell finds that seam by a two-letter flag, AG, and this typo makes a second fake AG next door, so the cell splices in the wrong place and the protein comes out broken. that's the disease.
rest.ensembl.org/lookup/id/ENST00000389817?expand=1
ENST00000389817 is NM_000352.6, and it is MANE Select. the response gives
the strand (minus) and every exon's coordinates, which places c.3989-9 nine bases into
intron 32, in front of the exon 33 acceptor AG. some genes also carry a MANE Plus Clinical isoform
that numbers exons differently, so check for one.
docs: ensembl rest · lookup endpoint · mane
ucsc just hands you the actual letters of the genome at any address you ask for. this is you reading the neighborhood with your own eyes instead of trusting a diagram someone drew. two counting traps live here. one: computers usually count starting at 0 and stop one short of the end, humans count from 1 and include both ends, and mixing the two is the classic off-by-one. two: because the gene runs backwards, the letters ucsc gives you (always the forward strand) have to be flipped and mirror-imaged, called reverse-complement, to read the way the gene reads.
quick vocab, because the next parts need it. to edit one spot, the machine uses a 20-letter "guide," a homing address that matches your target so the machine knows where to land. and right next to that stretch there has to be a short landing pad, three letters, called a PAM, or the machine can't grip. so a workable edit is a 20-letter window that both contains your typo and has a valid landing pad next to it.
api.genome.ucsc.edu/getData/sequence?genome=hg38;chrom=chr11;start=17397020;end=17397075
that returns the raw dna for the window. reverse-complement it (minus strand) and read off the
protospacer AGCCCAGCCCCCAGCACCAT at chr11:17,397,041-060, PAM
CGC at 17,397,038-040, target A at protospacer position 6 =
chr11:17,397,055. that last number has to equal the clinvar coordinate from step 1.
docs: ucsc rest api
your 20-letter guide is an address, but the genome is 3 billion letters and your address might partly match other spots too. the machine tolerates a few wrong letters when it homes in, so a guide can weakly grab similar-but-wrong places and edit them by accident. those are "off-targets," and they're the main safety worry. crispor takes your sequence, hunts the whole genome for every near-match, and scores how specific your guide is (few off-targets is good) and how efficient it is likely to be. you want high specificity so you're editing your spot and basically nowhere else.
the first three apis answer instantly. crispor actually runs a compute job (searching 3 billion letters takes a minute), so it is three moves. first, submit the sequence and set the PAM to match your enzyme (NGG for classic SpCas9, NG for SpCas9-NG):
crispor.gi.ucsc.edu/crispor.py?seq=YOUR_SEQUENCE&org=hg38&pam=NGG
the page that comes back contains a batchId, a ticket for the job. the job sits in a
queue and runs, and while it runs the download url below returns a server error, that is normal, keep
polling. once it finishes, download the scored guides as a tsv:
crispor.gi.ucsc.edu/crispor.py?batchId=YOUR_BATCHID&download=guides&format=tsv
columns are guideId, targetSeq, mitSpecScore, cfdSpecScore, offtargetCount,
targetGenomeGeneLocus then efficiency scores. off-targets get their own file with
&download=offtargets. one note: the public server is meant for light,
interactive use, and the authors ask you to run the standalone command-line or docker version for
real batch work. that version is crispor.py hg38 input.fa guides.tsv, with
-p/--pam and -o/--offtargets FILE, and you can pass the org as
noGenome to get efficiency scores only, which skips the genome download and the
off-target search.
docs: crispor · crispor source / cli
the target coordinate you rebuilt in ucsc has to equal the position clinvar gave you. here both are
chr11:17,397,055. if they do not match, stop and find out why before you order anything.
stdlib only, no keys, no pip. change the three config values at the top for your own variant. this is the exact script i ran to write this post, it prints the coordinate, the strand, the reference window, and the scored guides:
#!/usr/bin/env python3
# confirm a variant's genome coordinate and pull crispr guides for it, using only
# public web apis and the python standard library. no api keys, no pip installs.
# change the three CONFIG values to point at your own variant.
import json, re, time, urllib.parse, urllib.request, urllib.error
# ---- config: the only three things you change per variant ----
CLINVAR_ID = "9088" # numeric id of the clinvar record (VCV000009088)
TRANSCRIPT = "ENST00000389817" # ensembl transcript (= refseq NM_000352.6, MANE Select)
CHROM = "chr11" # ucsc chromosome name for this gene
def get_json(url): # fetch a url and parse the json body
with urllib.request.urlopen(url, timeout=30) as r:
return json.load(r)
def get_text(url, timeout=90): # fetch a url as plain text
with urllib.request.urlopen(url, timeout=timeout) as r:
return r.read().decode("utf-8", "replace")
# 1. CLINVAR: turn the variant into one exact, absolute coordinate.
# esummary hands back a record; the field we want is the canonical SPDI,
# which is chromosome:0-based-position:reference-letter:new-letter.
rec = get_json("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi"
f"?db=clinvar&id={CLINVAR_ID}&retmode=json")["result"][CLINVAR_ID]
spdi = rec["variation_set"][0]["canonical_spdi"] # e.g. NC_000011.10:17397054:C:T
_acc, pos0, ref, alt = spdi.split(":")
pos1 = int(pos0) + 1 # SPDI is 0-based; +1 to human 1-based
verdict = rec["germline_classification"]["description"]
print(f"[clinvar] {spdi} -> {CHROM}:{pos1} {ref}>{alt} ({verdict})")
# 2. ENSEMBL: confirm the transcript and get its strand + exon layout, so the
# c. numbering actually lines up. strand -1 means the gene reads backwards.
tx = get_json(f"https://rest.ensembl.org/lookup/id/{TRANSCRIPT}"
"?expand=1;content-type=application/json")
strand = tx["strand"]
print(f"[ensembl] {tx['display_name']} strand={strand} exons={len(tx['Exon'])}")
# 3. UCSC: pull the actual reference letters around the variant and orient them.
# the api is 0-based half-open. if the gene is on the minus strand, flip the
# letters (reverse-complement) so they read the way the gene / guide reads.
start0, end0 = pos1 - 61, pos1 + 39 # 0-based window around the variant
plus = get_json("https://api.genome.ucsc.edu/getData/sequence"
f"?genome=hg38;chrom={CHROM};start={start0};end={end0}")["dna"].upper()
def revcomp(s): return s.translate(str.maketrans("ACGT", "TGCA"))[::-1]
oriented = plus if strand == 1 else revcomp(plus)
print(f"[ucsc] + strand: {plus}")
# 4. CRISPOR: score guides in this window for genome-wide off-targets. unlike the
# first three (instant answers), this runs a compute job, so it is three moves:
# submit -> get a ticket (batchId) -> poll -> download results as tsv.
pam = "NGG" # match your enzyme (NG for SpCas9-NG, etc.)
sub = get_text("https://crispor.gi.ucsc.edu/crispor.py"
f"?seq={urllib.parse.quote(plus)}&org=hg38&pam={pam}")
batch = re.search(r"batchId=([A-Za-z0-9]+)", sub).group(1)
print(f"[crispor] batchId={batch} (waiting on the genome-wide off-target search)")
dl = f"https://crispor.gi.ucsc.edu/crispor.py?batchId={batch}&download=guides&format=tsv"
tsv = ""
for _ in range(30): # poll up to ~10 minutes
try:
tsv = get_text(dl)
except urllib.error.HTTPError: # job not finished -> server 500s; keep waiting
time.sleep(20); continue
if tsv.lstrip().startswith(("#guideId", "guideId")):
break
time.sleep(20)
rows = [r for r in tsv.splitlines() if r and not r.startswith("#")]
print(f"[crispor] {len(rows)} guides scored")
print(f"[crispor] columns: guideId targetSeq mitSpecScore cfdSpecScore offtargetCount locus ...")
print(f"[crispor] first guide: {rows[0].split(chr(9))[:4]}")
running it on the example prints the clinvar coordinate as pathogenic, ensembl strand -1 with 39 exons, the ucsc reference window, and roughly 20 crispor-scored guides. the whole point: several independent sources agree on one coordinate before you design anything, and crispor tells you whether your guide hits that spot and mostly nowhere else.
last updated 07/08/26