# 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](#3-get-the-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](tasks.md#options) and [Limits](reference/limits.md#input-length-per-task).

The live [OpenAPI schema](https://api.genomicintelligence.ai/v1/openapi.json) (browse it in [ReDoc](https://api.genomicintelligence.ai/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](mailto:contact@genomicintelligence.ai) for a `gi_…` API key. The same key works for the REST API and the [MCP server](mcp.md).

Verify it in one call:

```bash
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](/integration-kit/client)
and [recipes](/integration-kit/recipes) render with syntax highlighting — or
**[download the integration kit](https://docs.genomicintelligence.ai/integration-kit.tar.gz)**
(`.tar.gz`) to run it locally. Fetch it inline:

```bash
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](#6-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:

```bash
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:

```python
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](reference/limits.md#the-floor-is-admission-control-not-regime) 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](tasks.md#options).

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:

```python
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**:

```python
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](reference/limits.md#expression-the-9198-bp-window-and-tss_index).

Per-task inputs, options, and outputs are on the [Tasks](tasks.md) 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:

```python
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](tasks.md#genomic-variant-interpretation), which has no synchronous mode, and required for an [annotation](tasks.md#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](reference/limits.md); 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`.

```python
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](tasks.md#find-genes-and-predict-expression).

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

```http
POST /v1/workflows/genomic-variant-interpretation
Authorization: Bearer gi_...
Prefer: respond-async
```

```json
{
  "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](tasks.md#genomic-variant-interpretation).

### Error handling

Every non-2xx raises `GIError`, carrying the envelope's fields. Switch on `code`, not HTTP status:

```python
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](reference/errors.md).

## 6. Recipes

Self-contained scripts that each show one pattern. [Read them all in the
browser](/integration-kit/recipes), or link straight to the raw source:

| Intent | Recipe |
|---|---|
| Verify connectivity and key | [`01_health_probe.py`](https://docs.genomicintelligence.ai/recipes/01_health_probe.py) |
| Promoters across a gene list | [`02_promoters_for_gene_list.py`](https://docs.genomicintelligence.ai/recipes/02_promoters_for_gene_list.py) |
| Async annotation with polling | [`03_async_annotation_polling.py`](https://docs.genomicintelligence.ai/recipes/03_async_annotation_polling.py) |
| Rate-limit-aware retry | [`04_ratelimit_aware_retry.py`](https://docs.genomicintelligence.ai/recipes/04_ratelimit_aware_retry.py) |
| Typed error handling | [`05_typed_error_handling.py`](https://docs.genomicintelligence.ai/recipes/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}`. `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](tasks.md) 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
```

> 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](tasks.md): per-task input sizes, options, outputs, and strand-sensitivity.
- [Errors](reference/errors.md): every `error.code` and how to handle it.
- [Limits](reference/limits.md): per-task length caps, rate quotas, async TTL.
- [MCP server](mcp.md): the same six tasks as agent tools.
- [OpenAPI schema](https://api.genomicintelligence.ai/v1/openapi.json) · [ReDoc](https://api.genomicintelligence.ai/redoc): the full machine-readable contract.
