REST API
Call the REST API from any language: post a sequence to POST /v1/tasks/<task>/predict with an API key, get back typed JSON. This page walks the contract, then the integration kit — a single-file Python client and runnable recipes that let you reproduce every call in a few minutes.
Base URL: https://api.genomicintelligence.ai
1. The contract
Every endpoint shares one shape:
- Six tasks, one operation each:
POST /v1/tasks/promoter/predict,/splice/predict,/enhancer/predict,/chromatin/predict,/annotation/predict,/expression/predict— plus two workflows,POST /v1/workflows/find-genes-and-predict-expressionandPOST /v1/workflows/genomic-variant-interpretation. - API key on every
/v1/*route, sent asAuthorization: Bearer gi_…. Public (no key):/health,/docs,/redoc,/v1/openapi.json, andGET /v1/tasks/{task}/models— capability discovery needs no key, so you can size a request against a model'sbio_specbefore onboarding. It is metered per source IP rather than per partner, so a burst of discovery calls can429. - One success envelope —
{data, meta}on every predict and workflow 2xx, including the202.datais task-specific;metais uniform across tasks. The read-only endpoints are deliberately bare:GET /v1/tasks/{task}/modelsreturns{task, default_model, models}andGET /v1/tasks/jobsreturns{jobs, count}.metadescribes the computation that produced a result —model,cold_start, timings,job_id— so an endpoint that runs no inference has nothing to put there. - One error envelope —
{error: {code, message, request_id, details?}}. Switch onerror.code, never the message. - Sync by default, async on request —
Prefer: respond-asyncis a declared header parameter on all six predict operations and the composite: the call returns202with ajob_id, and you pollGET /v1/tasks/jobs/{job_id}. - Per-task bodies. Each task has its own request schema, its own minimum
sequencelength, and its own closedoptionsobject — there is no sharedPredictRequestany more. The URLs are unchanged, so client code needs no new URL construction, but a typed SDK generated from the old document must be regenerated. See Tasks and Limits.
The live OpenAPI schema (browse it in ReDoc) is the machine-readable source of truth for every field.
Pin the contract revision. The document carries a top-level x-contract-revision — a monotonic integer that changes whenever anything partner-visible in it changes, and only then. Record the value you built against and compare it on a schedule; you get a one-integer answer to "has the contract moved?" without diffing the document. A version bump alone never moves it.
The six-operation document, the typed options, the per-task minimums, and the published composite are rolling out to api.genomicintelligence.ai now. If a schema you fetched still reports a single templated POST /v1/tasks/{task}/predict, you have the previous version — the request URLs and response bodies are identical either way.
2. Get a key
Email contact@genomicintelligence.ai for a gi_… API key. The same key works for the REST API and the MCP server.
Verify it in one call:
curl -sS https://api.genomicintelligence.ai/health
# {"status":"healthy","version":"YYYY.MM.DD.iter (commit)"}
3. Get the kit
Browse the code in your browser — the Python client
and recipes render with syntax highlighting — or
download the integration kit
(.tar.gz) to run it locally. Fetch it inline:
curl -L https://docs.genomicintelligence.ai/integration-kit.tar.gz | tar -xz
cd integration-kit/
Inside:
| Path | What it is |
|---|---|
client/gi_client.py | Single-file client. Depends only on requests. Drop it into your project. |
client/quickstart.py | Runs every task end to end on real bundled sequences. |
client/sequences/ | Real FASTA fixtures, one per task. |
recipes/ | Five self-contained integration patterns (see Recipes). |
4. Run the quickstart
This hits every task with a real biological sequence and prints the result — the fastest way to see the API work:
cd integration-kit/client
pip install -r requirements.txt
export GI_API_KEY=gi_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
python quickstart.py
5. Call a task
Drop gi_client.py into your project and call a task. Every prediction returns the full {data, meta} body:
import os
from gi_client import Client
client = Client(api_key=os.environ["GI_API_KEY"])
body = client.predict("promoter", sequence="ACGT" * 500, sequence_name="demo")
print(body["meta"]["model"], body["meta"]["inference_time_ms"], "ms")
print(body["data"]["regions"])
Two things to get right on any task:
- Mind the per-task minimum. Promoter needs ≥ 300 bp, splice 100, enhancer 50, chromatin 200, annotation 1,000, expression 9,198 — measured after whitespace is stripped, and enforced before any model loads. Under it (or over 500,000 bp) is
422 validation_failed, not413. Being above the floor is not the same as being in regime: if your sequence is shorter than the chosen model'scontext_window_bp, it is scored against a padded window. Limits has the table. optionsis closed. Each task accepts a fixed set of keys — an unknown one is422 validation_failedwithloc["body","options","<key>"]. In particular, choose a model with the top-levelmodel=argument, neveroptions={"model": …}. The per-task keys are in Tasks.
The expression task requires an experimental-context description and a TSS position. Every expression model takes at least 9,198 bp with the TSS at least 4,599 bp from each end. Longer TSS-centred sequence is better, and the model list says how much: build a gene-sense sequence with the TSS at that offset, and pass tss_index, the 0-based offset of the TSS into the whitespace-stripped sequence. Do not hard-code a model id or a flank; read both from the list, so a new default or a longer-window model needs no change on your side. Mind the strand when you fetch:
listing = client.list_models("expression")
model = listing["default_model"] # or the model your user picked; resolve once per run
# One fetch that serves every listed model: the largest recommended flank (the
# server ignores flank beyond what a model reads), never below the 4,599 bp
# per-side floor, and with 2F within the request cap.
flank = max([4599] + [m["bio_spec"].get("recommended_flank_bp") or 0 for m in listing["models"]])
flank = min(flank, min(m["bio_spec"]["request_max_bp"] for m in listing["models"]) // 2)
# tss = 0-based genomic position of the TSS base. fetch() and revcomp() are your own.
if strand == "+":
locus = fetch(chrom, tss - flank, tss + flank) # [tss - F, tss + F)
else:
locus = revcomp(fetch(chrom, tss - flank + 1, tss + flank + 1)) # (tss - F, tss + F], then reverse-complemented
# Either way the TSS now sits at offset `flank` of the gene-sense sequence.
body = client.predict(
"expression",
sequence=locus, # 2 * flank bp; at least 9,198, up to 500,000
sequence_name="HBB",
model=model, # explicit, so a run never straddles a default change
tss_index=flank, # required unless len(sequence) == 9198
options={"description": "polyA plus RNA-seq; Homo sapiens K562"},
)
print(body["data"]["model"], body["data"]["prediction"]["expression_log_tpm"], "log(TPM+1)")
# Confirm the server read the region you meant; its width depends on the model.
print(body["meta"]["task_specific_counts"]["scored_window"])
The same body works with any listed model: pass another id as model and the server cuts what that model reads around tss_index. Near a chromosome end, use the largest symmetric flank that fits, down to 4,599 bp. A submission of exactly 9,198 bp centred on the TSS needs no tss_index (it defaults to 4599), but it is the floor, not a target: a model that reads more flank than that scores less than it was trained on.
Sequences shorter than 9,198 bp, or with the TSS closer than 4,599 bp to either end, are rejected with 422 validation_failed; there is no padding or truncation fallback. A window dominated by N or low-complexity repeats can also be refused, because each model needs a minimum of real sequence on each side of the TSS. A tss_index computed against line-wrapped FASTA characters rather than the stripped nucleotide string will be off by its newline count, which can silently score the wrong region; always check the echoed scored_window. Full rules: Limits.
Per-task inputs, options, and outputs are on the Tasks page.
Asynchronous jobs
For long inputs, submit asynchronously and poll. The client handles both steps — submit returns a job_id, and wait_for_job polls until the job is done:
job_id = client.submit_async(
"annotation", sequence=long_sequence, sequence_name="chr8:1-120000",
)
result = client.wait_for_job(job_id, on_progress=print)
print(len(result["data"].get("transcripts", [])), "transcripts")
Prefer: respond-async is a declared header parameter on all six predict operations and on both workflows — required on genomic-variant-interpretation, which has no synchronous mode, and required for an annotation JSON response above 200,000 bp; optional everywhere else. The 202 body is the same {data, meta} envelope as a sync 200, with data = {job_id, status: "accepted", links}; the id is also on the Content-Location and X-Job-Id response headers. Polling GET /v1/tasks/jobs/{job_id} returns 202 with data = {job_id, status, progress} while the job runs, then 200 with the result. Async is JSON-only — pairing it with a text ?format= is a 400.
Use async above the per-task size thresholds in Limits; below them, sync is simplest.
Find genes, then predict their expression
POST /v1/workflows/find-genes-and-predict-expression chains annotation and expression server-side: it finds the genes in a region, cuts a window around each TSS, and scores each gene with the API's default expression model, using as much flank around each TSS as the submitted region holds, so longer regions give better predictions, so you never compute a tss_index.
body = client.find_genes_and_predict_expression(
sequence=region, # 1,000–500,000 bp, plus strand
sequence_name="chr8:127,680,000-127,800,000",
description="polyA plus RNA-seq; Homo sapiens K562",
async_=True, # required above 50,000 bp
)
result = client.wait_for_job(body["data"]["job_id"])
print(result["meta"]["task_specific_counts"]) # {genes_found, genes_predicted, genes_skipped}
for gene in result["data"]["expression_predictions"]:
print(gene.get("skip_reason") or gene["prediction"]["expression_log_tpm"])
Submitted synchronously above 50,000 bp it returns 413 sync_too_large with details = {sequence_length, threshold}; resend the identical body with async_=True. POST /v1/tasks/annotation/predict behaves the same way above 200,000 bp, but only for a JSON response: ?format=bed and ?format=gff3 have no cap and stay synchronous at any accepted length, because async delivery is JSON-only and pairing Prefer: respond-async with a text format is a 400. Those two operations are the contract's only sync caps. Full contract: Tasks.
Interpret the variants in a VCF
POST /v1/workflows/genomic-variant-interpretation is the one operation that takes no DNA sequence. Point it at a VCF in your own object storage; it scores the variants near a transcription start site and writes an annotated copy beside it, named after the input and the job — read the exact name from data.output.uri. The JSON is a receipt — where the file is and how the run went — so you never parse a VCF to drive the workflow.
POST /v1/workflows/genomic-variant-interpretation
Authorization: Bearer gi_...
Prefer: respond-async
{
"input": { "type": "s3_object", "uri": "s3://your-bucket/cases/CASE-00123/sample.vcf.gz" },
"client_ref": "CASE-00123",
"options": { "genome_build": "GRCh37" }
}
Prefer: respond-async is required here — a case runs for hours, so there is no synchronous mode and a request without the header is 400. Poll GET /v1/tasks/jobs/{job_id} as above. Resubmitting the same case returns the job you already have rather than starting a second run. Full contract, including the annotation format and why meta.model may be absent: Tasks.
Error handling
Every non-2xx raises GIError, carrying the envelope's fields. Switch on code, not HTTP status:
from gi_client import Client, GIError
try:
client.predict("promoter", sequence="ACGT" * 500, sequence_name="demo")
except GIError as exc:
print(exc.code, exc.message, exc.request_id)
Every error.code and how to handle it is in Errors.
6. Recipes
Self-contained scripts that each show one pattern. Read them all in the browser, or link straight to the raw source:
| Intent | Recipe |
|---|---|
| Verify connectivity and key | 01_health_probe.py |
| Promoters across a gene list | 02_promoters_for_gene_list.py |
| Async annotation with polling | 03_async_annotation_polling.py |
| Rate-limit-aware retry | 04_ratelimit_aware_retry.py |
| Typed error handling | 05_typed_error_handling.py |
Client reference
gi_client.py exposes:
Client(api_key, base_url=…, timeout=…)— session configured with your key.predict(task, sequence, sequence_name, model=None, options=None, tss_index=None)— synchronous; returns{data, meta}.modelis top-level, not anoptionskey.submit_async(...)— same arguments, submitted withPrefer: respond-async; returns ajob_id.find_genes_and_predict_expression(sequence, description, sequence_name=…, options=None, async_=False)— the composite workflow.wait_for_job(job_id, poll_interval=2.0, on_progress=None)— poll until terminal.list_models(task),list_jobs(limit=…),health()— supporting calls.list_modelsreturns the flat{task, default_model, models}object, each model carryingbio_spec.request_max_bp/context_window_bp/trained_window_bp(and, for expression,recommended_flank_bp).TASK_MIN_BP,MAX_BP,FIND_GENES_SYNC_LIMIT_BP— local mirrors of the published bounds, for pre-flight checks. The authority is the served schema, not these constants.GIError— raised on any non-2xx, carryingcode,message,request_id,details.
Output formats
Predictions return JSON in the {data, meta} envelope by default. Tasks that also emit text tracks (BED, bedGraph, GFF3) expose them via the Accept header or a ?format= query parameter; the Tasks page lists the formats each one supports. An unsupported value is 415 unsupported_format — never a silent fallback to JSON — and text formats are synchronous-only, so combining one with Prefer: respond-async is a 400.
GFF3 source (column 2)
In every GFF3 this API generates, column 2 is the id of the model that
produced the feature — the same value as data.model — so a saved track
records its own provenance:
chr1 g0-splice-bigbird splice_donor 68 73 0.9949 . . ID=SD_1
:::note Changed in contract revision 4
Splice GFF3 previously emitted the literal gpu_service here. Saved files
produced before revision 4 carry the old value; nothing else about the line
changed. If you parse column 2, read it as the model id.
:::
The one exception is annotation's g0-annotation, whose upstream pipeline
emits its own canonical GFF3. That document is passed through verbatim, so
its column 2 reads GENATATOR-PIPELINE — a four-stage pipeline rather than a
single model, which a single model id would misdescribe.
Next steps
- Tasks: per-task input sizes, options, outputs, and strand-sensitivity.
- Errors: every
error.codeand how to handle it. - Limits: per-task length caps, rate quotas, async TTL.
- MCP server: the same six tasks as agent tools.
- OpenAPI schema · ReDoc: the full machine-readable contract.