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 requestPrefer: 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 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:

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.
  • 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.