Skip to main content
.md

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-expression and POST /v1/workflows/genomic-variant-interpretation.
  • API key on every /v1/* route, sent as Authorization: Bearer gi_…. Public (no key): /health, /docs, /redoc, /v1/openapi.json, and GET /v1/tasks/{task}/models — capability discovery needs no key, so you can size a request against a model's bio_spec before onboarding. It is metered per source IP rather than per partner, so a burst of discovery calls can 429.
  • One success envelope — {data, meta} on every predict and workflow 2xx, including the 202. data is task-specific; meta is uniform across tasks. The read-only endpoints are deliberately bare: GET /v1/tasks/{task}/models returns {task, default_model, models} and GET /v1/tasks/jobs returns {jobs, count}. meta describes 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 on error.code, never the message.
  • Sync by default, async on request — Prefer: respond-async is a declared header parameter on all six predict operations and the composite: the call returns 202 with a job_id, and you poll GET /v1/tasks/jobs/{job_id}.
  • Per-task bodies. Each task has its own request schema, its own minimum sequence length, and its own closed options object — there is no shared PredictRequest any 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.

note

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:

PathWhat it is
client/gi_client.pySingle-file client. Depends only on requests. Drop it into your project.
client/quickstart.pyRuns 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, not 413. Being above the floor is not the same as being in regime: if your sequence is shorter than the chosen model's context_window_bp, it is scored against a padded window. Limits has the table.
  • options is closed. Each task accepts a fixed set of keys — an unknown one is 422 validation_failed with loc ["body","options","<key>"]. In particular, choose a model with the top-level model= argument, never options={"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:

IntentRecipe
Verify connectivity and key01_health_probe.py
Promoters across a gene list02_promoters_for_gene_list.py
Async annotation with polling03_async_annotation_polling.py
Rate-limit-aware retry04_ratelimit_aware_retry.py
Typed error handling05_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}. model is top-level, not an options key.
  • submit_async(...) — same arguments, submitted with Prefer: respond-async; returns a job_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_models returns the flat {task, default_model, models} object, each model carrying bio_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, carrying code, 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.code and 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.