---
title: "Score the variants in a VCF and return an annotated copy"
method: POST
path: "/v1/workflows/genomic-variant-interpretation"
tags: ["Workflows"]
---

# Score the variants in a VCF and return an annotated copy

`POST /v1/workflows/genomic-variant-interpretation`

## Request

- Base URL: `https://api.genomicintelligence.ai`
- URL: `https://api.genomicintelligence.ai/v1/workflows/genomic-variant-interpretation`
- Auth: HTTP bearer

Score every variant in a VCF and write back an annotated copy.

Give it the ``s3://`` URI of a VCF. Each variant near a transcription
start site is scored in each requested tissue, and **every input record is
written to the output** — those that could not be scored are flagged in
place rather than dropped, so the output is a superset of the input and
counts reconcile.

**The annotated copy is written beside the input, named after it and
after the job** — ``sample.vcf.gz`` becomes
``sample.annotated.<job>.vcf.gz`` in the same prefix, where ``<job>`` is
the first eight characters of the ``job_id``. Read the exact name from
``data.output.uri`` rather than constructing it. This service needs
``s3:GetObject`` on the object you name and ``s3:PutObject`` on its
prefix, and nothing else. No listing is done, so a prefix holding a raw
and a filtered VCF is fine: name either, and each gets its own output.

**Submitting the same case twice gives you the same job.** The ``job_id``
is derived from the request — caller, ``input.uri``, the input object's
ETag, ``client_ref`` and ``options`` — so a retry after an ambiguous
failure, or a restarted integration, resolves to the run already under way
(or already finished, still inside the 24-hour window) rather than
starting a second multi-hour pass over the same object. There is no
``Idempotency-Key`` to send. Two consequences: a **failed** job re-runs on
resubmission, because de-duplicating a failure would strand the case for
the rest of the day; and to deliberately re-run a case that **succeeded**,
change ``client_ref``. Replacing the VCF under an unchanged path also
yields a new job, because its ETag is part of the derivation.

**This workflow is under development.** Your file is read, every record
is matched against the GENCODE v50lift37 MANE Select start sites, every
record is written back, and the counts are computed from it — but the
per-tissue values in an annotated file are not final and are not yet to
be read as results. A run is model output when, and only when, the
response carries ``meta.model`` and the annotated VCF carries a
``##GEXModel`` header line; both are omitted rather than naming a model
that did not produce the values, so their presence is the signal.

The result of this operation is a receipt, not a payload: it names the
object that was written and reports how the run went. The predictions live
in that object. A caller never has to parse a VCF to orchestrate the
workflow, and the annotation format inside it can change without breaking
anything built against this envelope.

**Async only.** A case takes hours, far past any proxy timeout, so there
is no synchronous mode and no callback: submit, then poll. Results are
retained for 24 hours; the annotated object is not on that clock.

## Headers

- `Prefer` 'respond-async', required

## Request body

- GenomicVariantInterpretationRequest: Body for ``POST /v1/workflows/genomic-variant-interpretation``.
  - `input` S3ObjectInput, required: The VCF to read, named directly by the caller.
    - `type` 's3_object', required
    - `uri` string, required: Full ``s3://bucket/key`` URI of the VCF to read, e.g. ``s3://bucket/cases/CASE-00123/sample.vcf.gz``. The key must end in ``.vcf``, ``.vcf.gz`` or ``.vcf.bgz``; nothing else in the prefix is listed, opened or read. **The annotated copy is written beside it, named after it and after the job** — ``sample.vcf.gz`` yields ``sample.annotated.<job>.vcf.gz`` in the same prefix, where ``<job>`` is the first eight characters of the ``job_id``. Read the exact name from ``data.output.uri`` rather than constructing it. Two VCFs in one prefix therefore produce two distinct outputs, and so do two different analyses of the SAME VCF — running one file against several panels is the ordinary case, and without the job in the name the second run would overwrite the first while the first receipt still pointed at it. This service must be granted ``s3:GetObject`` on the object and ``s3:PutObject`` on its prefix; ``s3:ListBucket`` is not required.
  - `client_ref` string, nullable: Opaque caller string, never parsed, echoed back in the result. It carries the caller's own case identity so this API does not have to model a case. **It does participate in this submission's identity**: the job id is derived from the request, so changing ``client_ref`` is how you deliberately re-run a case that already succeeded. Hashing is not parsing.
  - `options` GenomicVariantInterpretationOptions, required: ``options`` for ``POST /v1/workflows/genomic-variant-interpretation``. Everything the analysis needs from the caller's own case metadata arrives here as a typed field, so the service never parses a partner's metadata file and never depends on its format.
    - `genome_build` 'GRCh37', required: Assembly the VCF's coordinates refer to. **Required** — a VCF header declares it only sometimes, and guessing it silently misplaces every window. ``GRCh37`` is the only build with a published start-site catalogue; the enum will gain a value when another is published, which is additive.
    - `window` integer: Maximum distance in bp from a transcription start site for a variant to be scored.
    - `tissues` string[]: Cell-type contexts to score, one model run each. The expression model is conditioned on these strings and the exact wording moves the prediction. Defaults to ``['heart', 'liver', 'brain']``.
    - `genes` string[], nullable: Restrict scoring to these genes — a panel. Omit the field entirely to score every gene near a variant; an empty list is a ``422``, because a panel of nothing scores nothing and would otherwise return a green receipt with zero annotations that looks like a coverage result. Accepts HGNC symbols (``BRCA1``) or Ensembl gene ids with or without a version (``ENSG00000141510``, ``ENSG00000141510.17``); symbols are case-insensitive. An identifier that matches no MANE Select gene is a ``400`` naming every unresolved one, rather than being dropped — a panel silently missing a gene returns a result that looks complete and is not. Omit it to score every gene near a variant. Supplying one is the difference between tens of thousands of scored windows and a handful, so a case restricted to a panel finishes in a fraction of the time. Records near a start site that is **not** in the panel are written through with ``GEX_NOT_RUN_REASON=NO_REQUESTED_GENE_IN_WINDOW``, which is distinct from ``OUTSIDE_TSS_WINDOW``: the first says you did not ask, the second says there was nothing there.

## Response `200`

Never returned by this operation, which only ever accepts (202). Declared because ``GET /v1/tasks/jobs/{job_id}`` replays this envelope when the run completes, and a generated client needs the shape.

- GenomicVariantInterpretationResponse: Success envelope for ``POST /v1/workflows/genomic-variant-interpretation``.
  - `data` GenomicVariantInterpretationData, required: ``data`` for ``POST /v1/workflows/genomic-variant-interpretation``. A receipt, not a payload. The predictions live in the annotated VCF at ``output.uri``; this envelope says where that object is and how the run went, so a caller can orchestrate the workflow without parsing a VCF.
    - `task` 'genomic_variant_interpretation', required
    - `client_ref` string, nullable: Echo of the request's ``client_ref``, verbatim.
    - `status` 'ok', required: Present so the payload reads as a receipt. A failed run is a failed job: it comes back as the natural 4xx/5xx with the error envelope, never as a 200 saying ``failed``.
    - `output` VariantObjectRef, required: One S3 object: the case's input VCF, or the annotated copy of it.
      - `type` 's3_object', required
      - `uri` string, required: Fully qualified ``s3://`` object URI. For an output, the name is derived from the input's and the job's — read it from here rather than constructing it.
    - `counts` GenomicVariantInterpretationCounts, required: ``data.counts`` for the variant workflow. Invariant: ``annotated + not_run == variants_in``. Every input record is written to the output, whether or not it could be scored.
      - `variants_in` integer, required: Records read from the input VCF.
      - `annotated` integer, required: Records carrying a prediction in the output.
      - `not_run` integer, required: Records written through unscored — typically because no transcription start site falls within ``options.window``, or because it belongs to a gene outside ``options.genes``. See ``not_run_reasons`` for the breakdown.
      - `not_run_reasons` object: Why records were not scored, by reason: ``OUTSIDE_TSS_WINDOW``, ``NO_TSS_ON_CONTIG``, ``NO_SUPPORTED_ALT``, ``NO_REQUESTED_GENE_IN_WINDOW``. Sums to ``not_run``. A reason with no records is omitted.
      - `annotated_alleles` integer: ALT alleles carrying a prediction. Exceeds ``annotated`` where a record is multiallelic.
      - `associations` integer: Allele-to-start-site pairs scored — **the unit of model work**, not the record count. One allele near three start sites is three.
      - `skipped_alleles` integer: ALT alleles ineligible for scoring (symbolic, breakends, the spanning deletion ``*``), listed per record in ``GEX_SKIPPED_ALT``. A record can be annotated for its other alleles and still have some counted here.
  - `meta` GenomicVariantInterpretationMeta, required: ``meta`` for the variant workflow. Carries the provenance a result needs to stay interpretable months later: which assembly, which models, and the options actually applied — which may differ from those requested only by defaulting.
    - `job_id` string, required
    - `request_id` string, required: Correlation id for this request, mirrored on the X-Request-Id response header.
    - `task` 'genomic_variant_interpretation', required
    - `model` string, nullable: The model that produced these values, and the marker that they are model output at all. **Absent while this workflow is under development**, as is the annotated VCF's ``##GEXModel`` header line: the field is omitted rather than naming a model that did not produce the values. Read the per-tissue log2 fold changes as results only when both are present.
    - `genome_build` string, required
    - `gene_annotation` GeneAnnotationProvenance, required: The gene annotation the run was scored against.
      - `build` string, required: GENCODE release, e.g. ``v50lift37``.
      - `md5` string, required: MD5 of the GENCODE annotation the catalogue was built from.
      - `transcripts` string: Which transcripts carry a start site. One per gene today, so a gene with an alternative promoter is scored at its MANE start only — stated rather than implied by the release.
      - `start_sites` integer, required: Start sites in the catalogue.
    - `options_applied` GenomicVariantInterpretationOptionsApplied, required: What the run actually used — which differs from the request only by defaulting, and is the only way a caller can confirm which tissues were scored.
      - `window` integer, required: TSS proximity window in bp.
      - `tissues` string[], required: Cell-type contexts scored, in order.
      - `genes` string[], nullable: The panel as resolved to Ensembl gene ids, or null when the run was not restricted. Resolved rather than echoed, so a caller can see what their symbols became.
    - `input` VariantObjectRef, required: One S3 object: the case's input VCF, or the annotated copy of it.
      - `type` 's3_object', required
      - `uri` string, required: Fully qualified ``s3://`` object URI. For an output, the name is derived from the input's and the job's — read it from here rather than constructing it.

## Other responses

- `202`: Submission accepted. **This operation is async-only** — a case runs for hours, so ``Prefer: respond-async`` is required and a request without it is refused with ``400``. Poll ``GET /v1/tasks/jobs/{job_id}``.
- `400`: Refused before any work started, always with ``error.code`` ``bad_request``. The cases, distinguishable today only by ``error.message``: * ``Prefer: respond-async`` was not sent (this operation has no synchronous mode). * ``input.uri`` is not an ``s3://`` URI naming a bucket and a key. * ``input.uri`` names a prefix rather than an object (it ends in ``/``), or its key does not end in ``.vcf``, ``.vcf.gz`` or ``.vcf.bgz``. * the object cannot be read — it does not exist, or this service has not been granted ``s3:GetObject`` on it. * the input VCF is empty, or larger than 2 GiB. * ``options.genes`` names an identifier that resolves to no MANE Select gene; the message names every unresolved one. * ``options.genome_build`` has no published start-site catalogue. The input is validated **at submission**, so a typo costs a second rather than a poll cycle through a failed job.
- `401`: Missing or invalid bearer key (`unauthorized`).
- `403`: Key disabled or insufficient tier (`forbidden`).
- `404`: Unknown task, unknown job, or a job owned by another key (`not_found`).
- `409`: Conflict (`conflict`).
- `410`: Async job result expired out of the job store (`job_expired`).
- `413`: Either the 16 MiB request-body hard cap was exceeded (`payload_too_large`, enforced before parsing), or the body is over the endpoint's synchronous-delivery cap (`sync_too_large`; `error.details` carries `{sequence_length, threshold}` — retry the same request with `Prefer: respond-async`).
- `415`: The requested output format is not one this task can render (`unsupported_format`). The message lists the supported formats; the server never silently falls back to JSON.
- `422`: Validation failed (`validation_failed`) — sequence length outside the task's bounds, a non-ACGTN character, an unknown key under `options` (every options object is closed), or a sequence below the *selected task's published `minLength`/`maxLength` bounds.
- `429`: Rate or concurrency cap reached (`too_many_requests`). `Retry-After` and the `RateLimit-*` headers say when to retry.
- `500`: Server error (`internal_error`).
- `503`: ``service_unavailable``. On the POST it means the transcription-start-site catalogue for the requested build is missing or unreadable **on our side** — the request was correct, so retry rather than change it. On the poll it means the input object could not be downloaded, or the annotated copy could not be written, after the job was accepted. It is never ``model_loading`` or ``insufficient_memory``.

---

[API](https://skmtc.dev/genomicintelligence/apis/genomic-intelligence-api.md) · [All operations](https://skmtc.dev/genomicintelligence/apis/genomic-intelligence-api/llms.txt) · [OpenAPI document](https://skmtc.dev/genomicintelligence/apis/genomic-intelligence-api/revisions/72436b86d4fe?raw)
