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 scores exactly one 9,198 bp TSS-centered window. Submit that window on its own and you're done:
body = client.predict(
"expression",
sequence=tss_window, # exactly 9,198 bp centered on the TSS
sequence_name="HBB",
options={"description": "polyA plus RNA-seq; Homo sapiens K562"},
)
print(body["data"]["prediction"]["expression_log_tpm"], "log(TPM+1)")
Or hand over a whole locus (up to 500,000 bp) and let the server cut the window, by passing tss_index — the 0-based offset of the TSS into the whitespace-stripped sequence:
body = client.predict(
"expression",
sequence=locus, # 9,198–500,000 bp, gene-sense strand
sequence_name="HBB locus",
tss_index=tss_offset, # required unless len(sequence) == 9198
options={"description": "polyA plus RNA-seq; Homo sapiens K562"},
)
# Confirm the server scored the window you meant.
print(body["meta"]["task_specific_counts"]["scored_window"])
Sequences shorter than 9,198 bp are rejected with 422 validation_failed — there is no padding or truncation fallback. 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 window; 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, centres a 9,198 bp window on each TSS, and scores expression for each — 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.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.