# Ensembl REST API Regulatory Features: Step-by-Step Guide

By the end of this guide you will be able to query regulatory features (promoters, enhancers, CTCF binding sites, [transcription factor](/knowledge/molecular-biology/transcription-factor) binding sites, and open chromatin regions) from the Ensembl REST API with Python or curl. You will pull every regulatory feature overlapping a genomic region, filter those features by cell type activity, fetch detailed records for single features and batches of features, overlay regulatory features on genes and variants, and validate every result against the [Ensembl genome browser](/blog/guides/ensembl-genome-browser).

You need Python 3.8 or newer with the `requests` package installed (`pip install requests`), a terminal with `curl`, and a web browser for cross-checking results. No API key or registration is required. Every command in this guide runs against the public Ensembl REST server, which serves regulatory data for human and mouse as part of the broader Ensembl regulation resources [1].

## What Regulatory Features Are and Why the REST API Matters

A regulatory feature in Ensembl is an annotated candidate regulatory element placed on the genome by the [Ensembl regulatory build](/knowledge/bioinformatics/ensembl-regulatory-build). The build integrates experimental epigenomic data, including histone modifications, [DNA methylation](/blog/guides/dna-methylation), chromatin accessibility, and transcription factor binding measurements, into a functional annotation of regulatory elements [1]. The current build assigns features to a small set of classes:

| Feature type | What it represents |
|--|--|
| promoter | Region near a transcription start site associated with initiation of transcription |
| enhancer | Distal region with evidence of regulatory activity |
| CTCF binding site | Bound site for the insulator protein CTCF |
| TF binding site | Bound site for a transcription factor other than CTCF |
| open chromatin region | Accessible chromatin from DNase or ATAC-type assays |
| transcriptional cis regulatory region | Broad cis regulatory domain around a gene |

Each feature carries cell type specific activity labels. A promoter may be marked active in one cell line and inactive in another, because the underlying histone mark and accessibility data come from cell type specific experiments [1]. This cell type dimension is what makes regulatory annotation genuinely useful for interpretation, and it is also the dimension most often mishandled by new users.

Manual browsing works for one or two loci. For a cohort of variants, a list of differentially expressed genes, or a genome wide screen, you need programmatic access. The REST API provides that access over HTTP with JSON responses, the same interoperable pattern used across modern web services where clients on different platforms exchange JSON data [2]. Ensembl also exposes the same regulatory data through its Perl API and its public MySQL server, so the REST API is one of three equivalent routes into the resource [1]. Programmatic Ensembl access is standard practice across the field, from gene family surveys that mine Ensembl Plants [3] to sequence and expression correlation studies that download Ensembl records directly [4].

## How to Use Ensembl: Browser First, API Second

Before writing code, spend ten minutes in the Ensembl genome browser. This grounds every later API call in something you can see.

1. Open www.ensembl.org and search for a gene or region, for example `CFTR human`.
2. On the gene page, click **Location** to open the Region in detail view.
3. Click **Configure this page** at the left of the view.
4. Under the regulation related track groups, enable the regulatory feature tracks. The rendered blocks are the same annotated features the API returns.
5. Click any feature to open a pop-up with its stable identifier, which starts with `ENSR` for regulatory features.
6. Note the assembly name shown in the page header. For human this is GRCh38 on the main site.

The browser gives you a visual ground truth. The API gives you scale. A practical workflow is to explore one locus in the browser, confirm the features and cell type activities look sensible, then automate the same queries across hundreds of loci with the REST API. The two views are driven by the same underlying regulatory build, so they should agree, and disagreement is a signal that you have an assembly or identifier problem rather than a biological one.

## Core Endpoints and Parameters

The Ensembl REST server lives at `https://rest.ensembl.org`. For archived assembly queries (for example GRCh37 human coordinates), a parallel archive server exists at `https://grch37.rest.ensembl.org`. The endpoints you need for regulatory work are these:

| Endpoint | Method | Returns | Key parameter |
|--|--|--|--|
| `/info/ping` | GET | Service status confirmation | none |
| `/overlap/region/:species/:region` | GET | All features overlapping a coordinate range | `feature=regulatory` |
| `/overlap/id/:id` | GET | Features overlapping a stable identifier such as a gene | `feature=regulatory` |
| `/overlap/variant/:id` | GET | Features overlapping a variant such as an rsID | `feature=regulatory` |
| `/regulatory/species/:species/:id` | GET | Full record for one regulatory feature | none |
| `/regulatory/species/:species` | POST | Full records for many regulatory features | JSON body with an `ids` list |

Three conventions apply across all of them:

- Send the header `Content-Type: application/json` on every request so you always receive predictable JSON rather than whatever default the server negotiates.
- Regions use the format `chromosome:start-end`, for example `7:117400000-117700000`, with coordinates on the current default assembly, GRCh38 for human.
- Responses are plain JSON arrays or objects. No authentication token is needed, but the server rate limits requests, so a well behaved client throttles itself and handles the HTTP 429 status code.

## Step-by-Step: Querying Regulatory Features

### Step 1. Confirm the service is up

Before building any pipeline, verify connectivity.

```bash
curl -H "Content-Type: application/json" https://rest.ensembl.org/info/ping
```

Expected output:

```json
{"ping":"1"}
```

If this times out, check your network or proxy before debugging anything else. If you are scripting against the archive server for older coordinates, run the same call against `https://grch37.rest.ensembl.org/info/ping` to confirm that service responds as well.

### Step 2. Find regulatory features overlapping a genomic region

The workhorse endpoint is the region overlap. This example uses the CFTR gene neighborhood on human chromosome 7.

```python
import requests

server = "https://rest.ensembl.org"
region = "7:117400000-117700000"
url = f"{server}/overlap/region/human/{region}"
headers = {"Content-Type": "application/json"}
params = {"feature": "regulatory"}

response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
features = response.json()

print(len(features), "regulatory features returned")
for f in features[:3]:
    print(f["id"], f["start"], f["end"], f["feature_type"])
```

Expected output (your identifiers and counts will differ because the regulatory build is updated with each Ensembl release):

```
14 regulatory features returned
ENSR00001234567 117402198 117402534 CTCF binding site
ENSR00002345678 117455001 117456200 enhancer
ENSR00003456789 117480025 117481900 promoter
```

The key parameter is `feature`. Without it, the overlap endpoints return genes, transcripts, variants, and everything else overlapping the region. Setting `feature=regulatory` restricts the response to regulatory build features. If you also want motif level transcription factor binding predictions, run a second request with `feature=motif` rather than trying to combine both in one call.

### Step 3. Filter by feature type and cell type activity

Each returned feature includes a `cell_types` dictionary mapping cell type names to activity values. This is where the biological interpretation happens.

```python
promoters = [f for f in features if f["feature_type"] == "promoter"]
print(len(promoters), "promoters in this region")

active_in_k562 = [
    f for f in features
    if f.get("cell_types", {}).get("K562", {}).get("activity") == "ACTIVE"
]
print(len(active_in_k562), "features active in K562")
```

Three rules govern this filtering correctly:

- A feature lists only the cell types for which supporting data were assayed. If a cell type is absent from the dictionary, that means not assayed, not inactive.
- Activity values are string labels. Treat them as categorical evidence, not graded intensities.
- Feature type strings are lowercase phrases such as `promoter` and `enhancer`, so exact string matching works.

### Step 4. Fetch the full record for one regulatory feature

The overlap response is a summary. To see everything Ensembl knows about a feature, look it up by its stable identifier.

```python
reg_id = features["id"]
url = f"{server}/regulatory/species/human/{reg_id}"
headers = {"Content-Type": "application/json"}

response = requests.get(url, headers=headers)
response.raise_for_status()
record = response.json()

print(record["id"], record["feature_type"])
print(record["start"], "-", record["end"], "strand", record["strand"])
for cell_type, info in record["cell_types"].items():
    print(cell_type, info.get("activity"))
```

Expected output is a JSON object with the identifier, coordinates, strand, feature type, a version field, and the cell type activity dictionary. A trimmed representative example:

```json
{
  "id": "ENSR00001234567",
  "start": 117402198,
  "end": 117402534,
  "strand": 1,
  "feature_type": "CTCF binding site",
  "cell_types": {
    "GM12878": {"activity": "ACTIVE"},
    "K562": {"activity": "INACTIVE"}
  }
}
```

Compare this record against the browser pop-up for the same identifier. Coordinates, feature type, and activity labels should match exactly.

### Step 5. Batch fetch many features with POST

Querying features one at a time wastes requests and burns through the rate limit. The batch endpoint accepts a list of identifiers in a single POST request.

```python
ids = [f["id"] for f in features[:10]]
url = f"{server}/regulatory/species/human"
headers = {"Content-Type": "application/json"}

response = requests.post(url, headers=headers, json={"ids": ids})
response.raise_for_status()
records = response.json()
print(len(records), "records returned")
```

The response is a list of full records in the same format as the single feature lookup. Batch calls are the single biggest efficiency gain in this workflow, both for your wall clock time and for staying under rate limits.

### Step 6. Get regulatory features overlapping a gene

Often you start from a gene rather than a coordinate range. The ID overlap endpoint accepts a gene stable identifier directly.

```python
gene_id = "ENSG00000001626"  # CFTR
url = f"{server}/overlap/id/{gene_id}"
headers = {"Content-Type": "application/json"}
params = {"feature": "regulatory"}

response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
gene_features = response.json()

from collections import Counter
type_counts = Counter(f["feature_type"] for f in gene_features)
print(type_counts)
```

Expected output is a count of regulatory feature classes overlapping the gene body:

```
Counter({'promoter': 2, 'enhancer': 3, 'CTCF binding site': 1, 'open chromatin region': 4})
```

This is the standard entry point for candidate gene workflows. Position relative to transcription start sites matters for regulatory variant interpretation, and published prioritization methods explicitly restrict attention to regions around gene TSS positions taken from Ensembl annotation [5]. A gene body overlap plus a small flanking window is a reasonable first pass for that style of question.

### Step 7. Overlay regulatory features on a variant

To ask whether a variant of interest sits in or near annotated regulatory elements, use the variant overlap endpoint with an rsID.

```python
url = f"{server}/overlap/variant/rs699"
headers = {"Content-Type": "application/json"}
params = {"feature": "regulatory"}

response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
variant_hits = response.json()
for f in variant_hits:
    print(f["id"], f["feature_type"], f["start"], f["end"])
```

Each hit is a regulatory feature whose span contains or abuts the variant coordinate. For deeper functional annotation of variant regulatory effects, the Ensembl Variant Effect Predictor (VEP) annotates variants against regulatory features as part of its output, and it can be driven from the same identifiers you are already using here.

### Step 8. Write a polite, resilient client

Any script that loops over hundreds of regions needs throttling, retries, and caching. Systematic API testing work emphasizes that clients should handle endpoint dependencies and repeated failures explicitly rather than crashing on the first error response.

```python
import time
import json
from pathlib import Path

CACHE = Path("regulatory_cache")
CACHE.mkdir(exist_ok=True)

def fetch(url, params=None, max_retries=3):
    cache_file = CACHE / (url.replace("/", "_") + ".json")
    if cache_file.exists():
        return json.loads(cache_file.read_text())
    for attempt in range(max_retries):
        response = requests.get(
            url,
            headers={"Content-Type": "application/json"},
            params=params,
        )
        if response.status_code == 429:
            time.sleep(10 * (attempt + 1))
            continue
        response.raise_for_status()
        data = response.json()
        cache_file.write_text(json.dumps(data))
        time.sleep(0.2)
        return data
    raise RuntimeError(f"Failed after {max_retries} retries: {url}")
```

This pattern does four things. It caches responses to disk so repeated runs cost zero network calls. It sleeps with increasing backoff when the server signals rate limiting with HTTP 429. It raises a clear error rather than silently returning partial data. It pauses briefly between requests so a long loop stays under the limit.

### Step 9. Validate your results

Check every new pipeline once against independent ground truth:

- Pick one feature ID from your API output and open it in the genome browser. Coordinates, feature type, and cell type activities should match.
- Confirm the assembly. All main-server responses use GRCh38 for human. If your own variant coordinates are in GRCh37, either lift them over or switch your calls to the archive REST server.
- Cross-check counts. The number of regulatory features returned by `overlap/region` for a region that encloses a gene should be consistent with the number returned by `overlap/id` for that gene, allowing for boundary effects at the region edges.
- Check feature types against the six allowed classes. Anything else indicates a parsing error on your side.

### Step 10. Move from queries to analysis

Once validated, the same queries scale to real projects. Typical next steps include intersecting API results with your own ChIP-seq or ATAC-seq peaks, annotating a variant list from a GWAS catalog with overlapping regulatory features and their cell type activities, or feeding feature activity profiles into a disease gene annotation platform such as GVViZ, which organizes gene and disease data for visualization and expression analysis [6]. In all cases, keep the raw JSON responses archived with a record of the query date, because the regulatory build changes between releases.

## A Worked Mini-Analysis: Promoters Around One Gene

Pulling the pieces together, here is a compact script that summarizes promoter activity across cell types for the CFTR gene region.

```python
import requests
from collections import Counter

server = "https://rest.ensembl.org"
headers = {"Content-Type": "application/json"}

region_resp = requests.get(
    f"{server}/overlap/region/human/7:117400000-117700000",
    headers=headers,
    params={"feature": "regulatory"},
)
region_resp.raise_for_status()
features = region_resp.json()

promoters = [f for f in features if f["feature_type"] == "promoter"]
print("Promoters found:", len(promoters))

activity_counter = Counter()
for p in promoters:
    for cell_type, info in p.get("cell_types", {}).items():
        if info.get("activity") == "ACTIVE":
            activity_counter[cell_type] += 1

for cell_type, count in activity_counter.most_common():
    print(f"{cell_type}: active in {count} of {len(promoters)} promoters")
```

The script prints one line per assayed cell type with the number of promoters in the region marked active there. Interpretation is straightforward. A promoter active across many cell types is likely a housekeeping or constitutive promoter for the gene. A promoter active in only a few cell types points toward tissue specific regulation, and those cell types are exactly the ones you would prioritize when annotating disease associated variants near the gene [5].

## Ensembl REST API Regulatory Features Tips

**Start from the browser, then automate.** The genome browser and the REST API read the same regulatory build. Confirming one locus visually before coding saves hours of debugging downstream.

**Batch whenever possible.** The POST endpoint on the regulatory lookup accepts many identifiers per request. Ten features in one call is ten times more efficient than ten GET calls.

**Cache aggressively.** Regulatory annotations change only between Ensembl releases, so responses for the same query are stable for weeks at a time. Disk caching turns iterative analysis into instant reruns.

**Respect the rate limit.** The public server is shared infrastructure. Add small sleeps between requests and exponential backoff on 429 responses. A client that hammers the server degrades service for everyone and gets throttled harder.

**Pin your assembly explicitly.** Main server means GRCh38 for human. If any of your input data uses older coordinates, call the archive REST server instead of lifting coordinates silently and hoping.

**Treat missing cell types as missing data.** Absence of a cell type in the `cell_types` dictionary means no assay supported a call. Dropping those features entirely biases analyses toward well studied cell lines, which is a real consideration given that the regulatory build has grown outward from well studied lines toward a broader sample library over time [1].

**Test your client like software.** Check status codes, validate response structure on each call, and log request URLs alongside responses. Treat your data retrieval code with the same testing discipline you would apply to any REST integration, [2].

**Record the release.** Store the Ensembl release number with every cached result set. Feature identifiers are broadly stable, but boundaries, types, and cell type activities are re-evaluated with each build [1].

## Common Mistakes and Limitations

**Mixing assemblies.** The most frequent and most damaging error. GRCh37 and GRCh38 coordinates differ by megabases on many chromosomes. If your variant list is GRCh37 and you query the main REST server, every overlap will be wrong or empty. Route GRCh37 queries to the archive server or lift coordinates first.

**Omitting the feature parameter.** A region overlap without `feature=regulatory` returns genes, transcripts, and variants mixed together. New users then write parsers that fail on unexpected keys. Always pass the feature parameter, and parse defensively anyway.

**Reading absent cell types as inactive.** As noted above, the `cell_types` dictionary only contains assayed cell types. Filtering for `INACTIVE` labels gives you assayed inactive features, not a complete list of every cell type where the feature is untested.

**Confusing regulatory features with motif features.** Regulatory features are the merged, build level annotations. Motif features are transcription factor motif instances within them, retrieved with `feature=motif`. They have different identifier prefixes and different structures. A promoter and a motif inside it are two distinct records.

**Assuming feature identifiers are frozen snapshots.** An ENSR identifier persists, but the coordinates, class, and activity profile it points to are refreshed as new data arrive and the build is recomputed. Archive your JSON rather than assuming you can reproduce a past analysis from an ID alone.

**Overloading single requests with huge regions.** Very large coordinate spans can time out or return oversized responses. Chunk long regions into manageable windows and merge results.

**Overinterpreting activity labels.** Activity is a binary call derived from epigenomic assays in specific samples. It is evidence that a region is regulated in that cell type, not proof of a specific function, and it says nothing about activity in cell types that were never assayed.

**Ignoring species coverage.** The regulatory build covers human and mouse. Other species have related resources, such as microarray probe mappings, but not the same regulatory feature annotation, so region overlap queries with `feature=regulatory` on other genomes return nothing [1]. Gene level work in other species remains fully supported through the rest of Ensembl, as plant genome analyses routinely demonstrate [3].

A final limitation on interpretation: regulatory annotation tells you where regulation could happen and in which assayed cell types it shows activity. Connecting that to a specific disease mechanism requires your own functional follow-up, expression data, and domain expertise.

## Inside a Regulatory Feature Response: Reading Every Field

The fastest way to become fluent with the regulatory endpoints is to dissect one response until nothing in it surprises you. Pull a single full record with the single feature lookup and print it whole, then walk through it deliberately.

```python
import requests, json

server = "https://rest.ensembl.org"
headers = {"Content-Type": "application/json"}

# Replace with an ENSR identifier you found in the browser or in a prior overlap call
reg_id = "ENSR00000588195"
response = requests.get(f"{server}/regulatory/species/human/{reg_id}", headers=headers)
response.raise_for_status()
record = response.json()
print(json.dumps(record, indent=2)[:2000])
```

Every field in the response belongs to one of four groups, and knowing the groups tells you what you can trust and what you must not over-read.

**Identity fields.** The stable identifier beginning with `ENSR` is the primary key for the feature across the whole Ensembl system. This is the value you use in the single lookup, in batch POST calls, and when cross-checking against the genome browser pop-up. Treat it as an address, not as a description: the identifier persists across releases, but what sits at that address, meaning the coordinates, class, and activity profile, is refreshed as the regulatory build is recomputed with new data.

**Coordinate fields.** Start and end are integers on the forward strand of the default assembly, one based and inclusive in the Ensembl convention. Strand is recorded too. When you later export these coordinates to other tools, remember that most bed style formats use zero based starts, so a conversion of the start coordinate is required before writing a BED file. Getting this off by one base is harmless for broad features and quietly fatal for motif scale intervals.

**Classification fields.** The feature type string places the feature in one of the annotated classes: promoter, enhancer, CTCF binding site, TF binding site, open chromatin region, or transcriptional cis regulatory region. Match on the exact string. Do not attempt substring logic such as searching for "bind" inside the type name, because two of the classes legitimately contain that substring and your filter will silently merge them.

**Evidence fields.** The `cell_types` dictionary is the analytical heart of the record. Each key is a cell type for which supporting experimental data exist in the build, and each value carries the activity call derived from [histone modification](/knowledge/molecular-biology/histone-modification), methylation, accessibility, and transcription factor binding assays [1]. Three reading rules follow directly from how the build is constructed:

- A cell type absent from the dictionary was not assayed for this feature. Absence is missing data, never a negative call.
- A present activity label is a categorical call derived from the underlying assays. It is evidence about the state of the chromatin in that sample, not a graded intensity and not a direct measurement of transcription.
- The set of cell types present differs feature by feature, because different features are supported by different experiments. You cannot compare activity vectors across features by assuming the same column set.

A defensive parser that internalizes these rules looks like this:

```python
def summarize_feature(rec):
    return {
        "id": rec.get("id"),
        "type": rec.get("feature_type"),
        "start": rec.get("start"),
        "end": rec.get("end"),
        "n_assayed_cell_types": len(rec.get("cell_types", {})),
        "active_cell_types": sorted(
            ct for ct, info in rec.get("cell_types", {}).items()
            if info.get("activity") == "ACTIVE"
        ),
    }
```

Parsing defensively matters because the REST contract returns plain JSON that any platform can consume, and that flexibility means the structure is general rather than tailored to one downstream tool. Fields you did not anticipate can appear, and fields you expected can be absent for edge case features. Access everything with `.get()` and fail loudly, with the offending record saved to disk, rather than crashing mid loop or, worse, writing silent nulls into your results table.

## Choosing the Right Overlap Endpoint: Region, Gene, or Variant

![Decision path for picking region, gene, or variant overlap endpoint based on the starting object of the query.](/article-images/illustrations/ensembl-rest-api-regulatory-features-overlap-endpoint-choice.webp)

Three overlap endpoints expose regulatory features, and picking the wrong one is a common source of wasted requests and confusing results. The decision is really about what your starting object is.

**Start from a coordinate range when you have a locus.** The region overlap endpoint `/overlap/region/:species/:region` is the right choice when your analysis is anchored to genomic space: a candidate interval from a linkage scan, a topologically associated domain boundary, a ChIP-seq peak summit with a window around it, or simply a gene neighborhood you want to survey exhaustively. The region endpoint returns every regulatory feature whose span touches your interval, including features that extend beyond either edge, which makes it the most complete option for a bounded question.

**Start from a gene when your input is a gene list.** The ID overlap endpoint `/overlap/id/:id` accepts a gene stable identifier and returns features overlapping the gene model itself. This is the natural entry point for candidate gene workflows such as those derived from differential expression or rare variant burden results. Published prioritization approaches that weight regulatory proximity explicitly restrict attention to regions around transcription start positions taken from Ensembl annotation [5], and a gene body overlap is a close programmatic cousin of that logic. The main limitation is that the endpoint overlaps the gene model as annotated. A distal enhancer forty kilobases upstream of the transcription start site will not appear unless it physically intersects the annotated transcript span. If your question is genuinely about regulatory reach around a gene, convert the gene to coordinates, add a flanking window you can defend in your methods section, and use the region endpoint instead.

**Start from a variant when your input is a variant list.** The variant overlap endpoint `/overlap/variant/:id` takes an rsID and returns regulatory features whose span contains or abuts the variant. This is the correct entry point for annotation of GWAS loci and candidate coding or noncoding variants, because it does the coordinate containment logic for you. For a handful of variants it is ideal. For hundreds of variants you will be issuing one request per variant, so combine it with the caching and throttling client pattern described later, or move the whole variant set through a tool such as VEP, which annotates variants against regulatory features as part of its output and accepts the same identifier space you are already working in.

A useful mental model is that all three endpoints read the same regulatory build and differ only in how they map your input onto coordinates. That shared origin is why cross-checking between them is so powerful: the region overlap for a window enclosing a gene should produce counts consistent with the ID overlap for that gene, allowing for features clipped at the window edges. When the two disagree badly, the cause is almost always an assembly mismatch or a malformed region string, never biology.

One further selection rule: if you need motif level transcription factor binding instances inside the regulatory features, that is a separate request with `feature=motif`. Motif features and regulatory features are distinct record types with different identifier structures, and no single overlap call merges them.

## Working with GRCh37 Coordinates: A Complete Archive Server Workflow

![Flowchart of the archive server workflow: ping, point calls at archive host, tag assembly, avoid mixing servers, validate in browser.](/article-images/illustrations/ensembl-rest-api-regulatory-features-grch37-archive-workflow.webp)

Legacy datasets, older publications, and many clinical variant files carry GRCh37 coordinates. Mixing those coordinates into GRCh38 queries is the single most damaging error in regulatory API work, because the coordinate systems differ by megabases on many chromosomes and a query that returns plausible looking results on the wrong assembly is worse than one that returns nothing. Ensembl maintains a parallel archive REST server for GRCh37 queries, and the correct habit is to route by assembly explicitly [1].

The workflow below assumes you have a table of GRCh37 coordinates, for example a set of loci extracted from an older study.

**Step 1. Confirm the archive service.** Run the ping call against the archive host before anything else:

```bash
curl -H "Content-Type: application/json" https://grch37.rest.ensembl.org/info/ping
```

**Step 2. Point every call at the archive host.** The endpoint paths are the same as the main server. Only the base URL changes:

```python
ARCHIVE = "https://grch37.rest.ensembl.org"

def fetch_grch37_region(chrom, start, end):
    url = f"{ARCHIVE}/overlap/region/human/{chrom}:{start}-{end}"
    response = requests.get(url, headers={"Content-Type": "application/json"},
                            params={"feature": "regulatory"})
    response.raise_for_status()
    return response.json()
```

**Step 3. Tag every record with its assembly.** The moment results from two servers coexist in one project, provenance becomes load bearing. Add the assembly to each record as you ingest it:

```python
def tag_with_assembly(features, assembly):
    for f in features:
        f["query_assembly"] = assembly
    return features
```

**Step 4. Never straddle servers in one comparison.** If half your variants are GRCh37 and half are GRCh38, lift the GRCh37 set to GRCh38 with a dedicated coordinate conversion tool first, then run a single assembly pipeline. Do not query both servers and merge raw coordinates. The archive server exists so that you can reproduce an older analysis faithfully in its original coordinate system [1]; it is not a general purpose coordinate harmonization service.

**Step 5. Validate against the archive browser view.** The GRCh37 archive website renders the same archive data. Open one locus from your GRCh37 results in the archive browser and confirm the feature identifiers, coordinates, and activity labels match your API output. The same browser first, API second discipline that anchors the main server workflow applies here unchanged.

A subtlety worth internalizing: the regulatory build on the archive reflects the state of annotation consistent with that archive context, so feature counts and even the presence of particular features can differ between the GRCh37 and GRCh38 views of the same locus. This is expected. It is one more reason to record, alongside every cached response, which server produced it and on what date.

## Mouse Regulatory Features: What Transfers and What Changes

Because the regulatory build covers human and mouse [1], the entire workflow in this guide transfers to mouse with a single change: the species segment of each path. The endpoints become `/overlap/region/mouse/...` and `/regulatory/species/mouse/...`.

The structure of the request and response is identical to the human case:

```python
def mouse_region_features(chrom, start, end):
    url = f"https://rest.ensembl.org/overlap/region/mouse/{chrom}:{start}-{end}"
    response = requests.get(
        url,
        headers={"Content-Type": "application/json"},
        params={"feature": "regulatory"},
    )
    response.raise_for_status()
    return response.json()
```

Rather than trusting any hardcoded mouse locus, derive your test region from the mouse genome browser in the same way the human workflow does: search your gene of interest, open the location view, enable the regulatory tracks, and read a coordinate range off the page. Then run the region overlap against that range. Because the browser and the API read the same build, agreement between the two is your validation.

Three differences from the human case deserve attention in your methods and your expectations:

- The cell type panel behind the mouse build reflects the mouse experiments available to the build, so the cell type names in `cell_types` will not match the human panel, and a human analysis script that assumes specific cell type names will need its filter lists recompiled.
- Feature class labels use the same vocabulary, so type filtering code transfers unchanged.
- Cross species comparisons require explicit orthology mapping. An enhancer active near a gene in mouse and a feature at the aligned human locus are related by homology and by your mapping, not by any guarantee in the API response. Plan that mapping as a separate, tested step rather than assuming coordinate alignment does the work.

Mouse regulatory queries are especially useful as a replication axis for human findings: a disease associated locus whose candidate regulatory feature shows activity in a cell type relevant to the disease, with a conserved regulatory architecture at the orthologous mouse locus, is a stronger candidate than one supported in a single species. Keep the two pipelines separate until the interpretation stage, and keep the assembly tags on both.

## From JSON to BED: Intersecting Regulatory Features with Your Own Data

Almost every real analysis eventually needs regulatory features in a format that bedtools, bedops, deepTools, or a genome browser custom track can consume. This is a short hop, but the coordinate convention change makes it a hop worth doing carefully.

Ensembl reports one based, inclusive coordinates: a feature with start 117402198 and end 117402534 includes both of those bases. BED uses zero based, half open intervals: the same feature is written with a start of 117402197 and an end of 117402534. Convert once, in one function, and unit test that function against a known example rather than sprinkling minus ones through your scripts.

```python
def features_to_bed(records, assembly="grch38", source="ensembl_rest"):
    lines = []
    for r in records:
        bed_start = int(r["start"]) - 1
        bed_end = int(r["end"])
        line = (
            f"chr{r.get('seq_region_name', '')}\t"
            if r.get("seq_region_name") else ""
        )
        lines.append(
            f"{line.rstrip(chr(9))}"
        )
    return lines
```

That sketch shows the hazard: not every record in an overlap response carries the same fields as a full record. The robust approach is to fetch full records for the features you intend to export, using the batch POST endpoint, so that every record has the complete coordinate and identity fields before you transform it:

```python
def to_bed_line(rec):
    bed_start = int(rec["start"]) - 1
    bed_end = int(rec["end"])
    name = rec["id"]
    ftype = rec.get("feature_type", "NA").replace(" ", "_")
    return f"{rec['seq_region_name']}\t{bed_start}\t{bed_end}\t{name}\t{ftype}"
```

If the overlap response you already have includes a chromosome or sequence region key in each record, you can export directly without the extra round trip; inspect one response in full before deciding. Either way, encode provenance in the track line or in a companion metadata file: the assembly, the Ensembl release, and the query date. A BED file of regulatory features without its release number is a future reproducibility failure waiting to happen, because boundaries and activity profiles are re evaluated with each build [1].

Once the features are in BED, standard genomic arithmetic takes over:

- Intersect your own ChIP-seq or ATAC-seq peaks with regulatory features to ask which of your signal peaks correspond to build annotated elements, and which fall outside known annotation and therefore deserve independent scrutiny.
- Intersect GWAS lead variant positions with active in cell type X regulatory features to generate a short list of candidate functional variants per locus.
- Merge feature intervals before counting to avoid inflating overlap statistics where multiple features of different classes sit on the same bases.

A caution on the third point: overlapping features of different classes are not redundancy to be cleaned away. A promoter and an overlapping open chromatin region describe different evidence streams, and collapsing them discards information. Collapse only when your statistics require non overlapping intervals, and say so in your methods.

## Building a Variant Annotation Pipeline from a Variant List

The single variant overlap endpoint becomes a pipeline the moment you wrap it in the polite client pattern. Here is a complete, compact annotator that reads a file of rsIDs, one per line, and writes a tabular summary of regulatory hits.

```python
import requests, time, json
from pathlib import Path

SERVER = "https://rest.ensembl.org"
CACHE = Path("variant_reg_cache")
CACHE.mkdir(exist_ok=True)
HEADERS = {"Content-Type": "application/json"}

def cached_fetch(url, params=None):
    key = url + "|" + json.dumps(params or {}, sort_keys=True)
    fname = CACHE / (str(abs(hash(key))) + ".json")
    if fname.exists():
        return json.loads(fname.read_text())
    for attempt in range(3):
        resp = requests.get(url, headers=HEADERS, params=params)
        if resp.status_code == 429:
            time.sleep(10 * (attempt + 1))
            continue
        resp.raise_for_status()
        data = resp.json()
        fname.write_text(json.dumps(data))
        time.sleep(0.2)
        return data
    raise RuntimeError(f"gave up on {url}")

def annotate_variant(rsid):
    hits = cached_fetch(
        f"{SERVER}/overlap/variant/{rsid}",
        params={"feature": "regulatory"},
    )
    rows = []
    for h in hits:
        active_cts = [
            ct for ct, info in h.get("cell_types", {}).items()
            if info.get("activity") == "ACTIVE"
        ]
        rows.append({
            "rsid": rsid,
            "feature_id": h.get("id"),
            "feature_type": h.get("feature_type"),
            "active_cell_types": ";".join(active_cts),
        })
    return rows

all_rows = []
with open("variants.txt") as fh:
    for line in fh:
        rsid = line.strip()
        if rsid:
            all_rows.extend(annotate_variant(rsid))

import csv
with open("variant_regulatory_annotation.tsv", "w", newline="") as fh:
    writer = csv.DictWriter(fh, fieldnames=[
        "rsid", "feature_id", "feature_type", "active_cell_types"])
    writer.writeheader()
    writer.writerows(all_rows)
```

Design notes that matter more than the code itself:

**Variants with no hits are results, not errors.** A variant outside all annotated regulatory elements produces an empty list. Record it as annotated with zero features. Treating empty responses as failures biases your output set toward variants in well characterized regions.

**Identifier hygiene first.** Normalize your input identifiers before the loop. Strip whitespace, enforce the `rs` prefix, and reject lines that do not match. A malformed identifier will surface as a 404 deep inside a long run, and debugging is far easier if bad lines are caught at ingestion.

**The output format is a decision.** One row per variant feature pair is the tidy choice and supports downstream filtering directly. Collapsing to one row per variant with concatenated features is friendlier for spreadsheets but destroys the structure you will want for filtering by feature type or cell type.

**Graduate to VEP for depth.** When the question expands from "which regulatory features overlap this variant" to "what might this variant do to them," route the same variant set through the Variant Effect Predictor, which annotates against regulatory features and fits naturally into the identifier space you are already using. The API workflow above is then the fast first pass and VEP is the detailed second pass, rather than two competing systems.

## Reading Cell Type Activity Labels Without Overreaching

The activity labels are the most valuable and most over interpreted part of a regulatory feature record. A disciplined reading rests on what the labels actually are: categorical calls derived from epigenomic assays, including histone modification profiles, DNA methylation, chromatin accessibility, and transcription factor binding, run in specific samples [1].

**What an ACTIVE label supports.** It supports the statement that, in the assayed sample, the chromatin state of this region is consistent with regulatory activity of the feature's class. For a promoter, that is initiation associated chromatin. For a CTCF binding site, it is evidence of bound insulator protein. It does not establish the target gene of an enhancer, the direction of an effect, or the consequence of disrupting the element.

**What an INACTIVE label supports.** Only that the same assays in that sample did not support an active call. Inactivity in one cell type is genuinely informative, particularly when paired with activity in another, because the contrast is the signal that makes tissue specific regulation visible. A promoter active in a disease relevant immune cell type and inactive across epithelial samples directs your attention differently than one active everywhere.

**What absence supports.** Nothing. It marks unassayed territory. This has a practical consequence for how you compute summaries: a feature active in two of two assayed cell types is not more strongly supported than one active in two of twenty, and a script that computes a fraction of active among assayed must also carry the count of assayed, or it will systematically flatter features profiled in narrow panels.

**Choosing which cell types to examine.** Reverse the usual flow. Start from the biology of your question, name the cell types where the trait or disease of interest manifests, and filter the `cell_types` dictionaries for those names. The worked mini analysis in the main guide, which counts active promoters per cell type across a region, is exactly the kind of summary that becomes a prioritization tool when the cell type list is chosen by hypothesis rather than by availability. Methods that prioritize regulatory regions around gene transcription start sites from Ensembl annotation [5] generalize cleanly to this cell type aware version: proximity supplies the where, activity labels supply the when and where in which cells.

**The bias you inherit.** The cell type panel of any epigenomic build reflects the experiments that were available and the samples that were studied, and the build has grown from well studied lines toward a broader sample library over time [1]. Your activity aware analyses therefore inherit a coverage map dominated by commonly profiled samples. When a feature shows no activity anywhere in the panel, distinguish between "inactive everywhere assayed" and "assayed nowhere." When a disease relevant cell type is absent from the panel entirely, say so in your limitations rather than letting absence masquerade as evidence.

## REST API Compared with MySQL, Perl API, Bulk Files, and the Browser

Ensembl exposes the same regulatory data through several doors [1], and choosing the right door per task saves more time than optimizing within the wrong one.

| Access route | Best suited to | Weaknesses for regulatory work |
|-|-|-|
| REST API | Targeted queries, live pipelines, language agnostic scripting, cohort scale annotation loops | Rate limited; per query granularity; whole genome exports are the wrong shape of task |
| Public MySQL server | Complex relational joins, bulk extraction with SQL fluency available | Requires direct database access from your environment and schema knowledge; heavier setup |
| Perl API | Deep Ensembl ecosystem integration and scripted analysis inside that stack | Requires Perl environment; higher barrier for teams standardized on Python or R |
| Bulk downloads and files | Whole genome exports, static snapshots, cluster jobs with no network at runtime | Data age between downloads; large storage; you manage release tracking yourself |
| Genome browser | One locus at a time visual validation, teaching, sanity checks | Does not scale; no programmatic output |

The REST API's distinctive advantages for regulatory features are exactly the properties demonstrated throughout this guide: plain JSON over HTTP that any platform can consume, the interoperable pattern used across modern web services [2], no authentication barrier for public data, and endpoint granularity that maps cleanly onto analytical questions such as "features overlapping this variant" or "features overlapping this gene."

The browser is not competition for the API; it is the quality control layer. Because both are driven by the same regulatory build, the browser is an independent check on your parsing. Making the browser comparison a formal step in your pipeline validation, rather than an occasional habit, catches assembly errors, malformed region strings, and field mapping mistakes before they contaminate a downstream analysis.

Bulk access earns its place when your task inverts shape. Annotating two hundred variants is REST work. Extracting every regulatory feature genome wide, once, to feed a local annotation database is file download work, and forcing it through per region API calls will burn your rate budget for no benefit. A sensible production setup uses bulk data as the substrate and reserves the REST API for the dynamic layer: lookups of new identifiers, spot checks, and interactive exploration.

Whichever route you choose, the selection should be visible in your methods. Studies across the Ensembl ecosystem report their access route explicitly, whether mining Ensembl Plants for gene family surveys [3] or downloading Ensembl records directly for sequence and expression correlation analyses [4], and regulatory work should meet the same reporting standard: route, server, assembly, release, and query date.

## Troubleshooting Cookbook: Symptoms, Causes, Fixes

**Symptom: the ping call hangs or times out.** Check your own network and proxy first. Corporate proxies that require configuration are a more common cause than Ensembl outages. If the main server responds but the archive server does not, route only your GRCh37 queries through a retry and continue human GRCh38 work elsewhere.

**Symptom: the region overlap returns an empty list where you expected features.** Work through this checklist in order. First, is your region string well formed as `chromosome:start-end` with no spaces? Second, is the assembly right? A GRCh37 coordinate range on the GRCh38 server frequently lands in a gene desert and returns nothing, which looks like a data absence problem but is an assembly problem. Third, did you pass `feature=regulatory`? Without it you get a mixed feature response, and with the wrong parsing you may be discarding regulatory records that were present. Fourth, is the species right? Regulatory features exist only for the species covered by the build, human and mouse, and queries elsewhere return nothing [1]. Fifth, does the locus have annotation at all? Confirm in the browser before concluding anything is broken.

**Symptom: HTTP 404 on a regulatory lookup.** The FAQ path in the main guide covers the usual causes: identifier not valid for that species or assembly, malformed region string, or an identifier retired between releases. Verify the identifier in the genome browser, and if it resolves there, compare the assembly in your URL against the assembly shown in the browser header.

**Symptom: HTTP 429 responses during long runs.** This is the rate limiter doing its job. Your client should treat 429 as a signal to back off, not as a failure to crash on. The retry loop in the main guide's polite client sleeps with increasing duration and retries. If you are seeing 429s despite a modest sleep, switch to the batch POST endpoint for full record lookups, since batching is the largest single reduction in request count available to you.

**Symptom: JSON decode errors in your parser.** You are likely parsing a non JSON body, which happens when the content negotiation falls back to a default response format. Sending `Content-Type: application/json` on every request keeps responses predictable [2]. If a decode error follows a retry, inspect the saved body before the parsed data; a proxy or captive portal page will also decode fail.

**Symptom: cell type filters return far fewer features than expected.** Almost always the missing data trap: only assayed cell types appear in the dictionary, so filtering for a cell type with sparse coverage selects few features regardless of biology. Print the union of cell type keys across your result set first. If your cell type of interest appears in that union but rarely, coverage is the explanation; if it never appears, it is not in the panel for this feature set at all.

**Symptom: results differ from a colleague's run of the same script.** Compare, in order: the server host in the URL, the species string, the region or identifier, the Ensembl release each of you cached, and the query dates. The regulatory build changes between releases [1], so two correct runs months apart can legitimately differ, which is why release recording is part of the reproducibility workflow rather than an optional nicety.

**Symptom: a very large region request fails or returns a truncated feeling result.** Chunk long spans into windows, query each, and merge with deduplication on the feature identifier. Features spanning chunk boundaries will appear in both windows; deduplication by `id` restores the correct count. Confirm the merged count by re querying one chunk boundary manually and checking both windows contain the straddling feature.

## Misconceptions That Derail Regulatory Analyses

**Misconception: an enhancer annotation means the enhancer is active.** The feature record carries the class and, separately, per cell type activity labels. The class describes the annotated candidate element; the activity labels describe the assayed evidence. An enhancer typed feature with no cell types in its dictionary, or with INACTIVE labels across the panel, is annotated but not demonstrated active anywhere in the current data. Report class and activity as the separate axes they are.

**Misconception: a regulatory feature is proof of function for my variant.** Overlap between a variant and an active regulatory feature is a hypothesis generator. It says the variant sits in chromatin showing regulatory signatures in an assayed sample. Connecting that to a disease mechanism requires your own follow up, expression evidence, and domain knowledge, a limitation the main guide states and that review processes increasingly expect you to repeat.

**Misconception: cell type labels transfer across features.** Each feature's activity evidence comes from the experiments that support that feature. Two features in the same locus can have entirely different assayed cell type sets. Never compare two features' activity vectors positionally; compare their sets, and remember the denominator of assayed types.

**Misconception: the API returns a snapshot that will reproduce forever.** Feature identifiers persist, but the underlying build is recomputed as new epigenomic data arrive [1]. Coordinate boundaries can shift, a feature can change class, and activity labels can flip as evidence accumulates. Reproduction comes from your archived JSON responses plus the release number, not from re running a query against a moving build.

**Misconception: motif features are a subset view of regulatory features you can get in the same call.** They are a distinct record type fetched with a distinct `feature=motif` parameter. A motif feature represents a transcription factor motif instance within the regulatory landscape, with a different identifier structure from regulatory features. Analyses that conflate the two tend to double count binding evidence.

**Misconception: if the API covers it, the API is always the best tool.** As the comparison table above argues, whole genome exports belong to bulk access, deep relational questions belong to the database, and validation belongs to the browser. The API is the right tool for targeted, live, and scripted queries, which describes most annotation workflows but not all data engineering tasks.

**Misconception: the six feature classes are exhaustive biological categories.** They are the working vocabulary of this build, useful for filtering and counting. Real regulatory biology, including silencers, insulators beyond CTCF, and locus control regions, does not map one to one onto the class list. Use the classes as the build defines them and resist relabeling them with stronger biological claims than the build makes.

## Making Results Reproducible: Caching, Logging, and Release Pinning

Regulatory API analyses have an unusual reproducibility profile: the code is deterministic, but the backend is a living annotation that changes between releases. A reproducibility plan has to account for both.

**Cache with metadata.** The disk caching pattern from the polite client should be extended so each cached file carries a sidecar record: the full request URL, the parameters, the query timestamp, and the Ensembl release if you have captured it. A minimal sidecar writer:

```python
def write_with_meta(fname, data, url, params, release=None):
    Path(fname).write_text(json.dumps(data))
    meta = {
        "url": url,
        "params": params,
        "retrieved": time.strftime("%Y-%m-%dT%H:%M:%S"),
        "ensembl_release": release,
    }
    Path(str(fname) + ".meta.json").write_text(json.dumps(meta, indent=2))
```

Capturing the release number alongside every result set is the single highest value habit here, because feature identifiers persist while their boundaries, classes, and activity profiles are re evaluated with each build. An analysis archived with its release number can be reasoned about even after the live build moves on; one without cannot.

**Log requests, not just results.** Write a log line per request containing the URL, parameters, status code, and duration. When a reviewer or collaborator asks why a feature count changed, the log converts an afternoon of archaeology into a five minute lookup. This is the same discipline that API testing practice recommends for endpoint dependencies: make the interaction history inspectable rather than ephemeral.

**Version your inputs.** The variant list, gene list, or region list that drives the pipeline is data. Commit it, hash it, and record the hash with outputs. When a rerun produces different annotations, the [decision tree](/blog/data-analysis/what-is-a-decision-tree) is: same release, check code; different release, check the build; same both, check inputs.

**Freeze outputs before re running.** Before upgrading any part of the stack, including the Ensembl release your cache represents, export current result tables to versioned files. Diffing the frozen tables against the new run is the fastest way to see exactly what the build change did to your analysis, feature by feature.

**State the assembly everywhere.** Assembly, release, server host, and query dates belong in the methods paragraph of any output that depends on this pipeline. Studies that integrate Ensembl data routinely report their retrieval details for exactly this reason, from plant gene family mining [3] to direct record downloads for expression correlation work [4], and regulatory analyses deserve the same standard because their backend moves more often than most.

## Testing Your Client Like Production Software

A script that queries a live service is software with an external dependency, and it rewards the same testing discipline. Three test layers cover nearly everything that goes wrong in practice.

**Pure function tests with no network.** Extract your parsing and filtering logic from your fetching logic and test the former with recorded JSON fixtures. Save one real response per endpoint shape to disk as a fixture, then assert against it: that feature type strings match the six allowed classes, that the cell type summarizer returns sorted lists, that the BED conversion subtracts one from the start coordinate. These tests run in milliseconds and catch the majority of regressions.

```python
def test_bed_conversion():
    rec = {"seq_region_name": "7", "start": 100, "end": 200,
           "id": "ENSRX", "feature_type": "promoter"}
    line = to_bed_line(rec)
    fields = line.split("\t")
    assert fields[8] == "99" and fields[2] == "200"
```

**Integration tests against the live service, run sparingly.** A small suite that pings the server, queries one known region, and asserts the response is a non empty list of objects with an `id` key catches service changes and contract shifts. Mark these tests to run on demand rather than in every automated run, both to respect the shared rate limited server and to keep your continuous integration independent of an external service's uptime.

**Failure path tests.** Feed your client a simulated 429 and a simulated malformed JSON body and confirm it backs off and raises informative errors respectively. The systematic API testing literature emphasizes handling endpoint dependencies and repeated failures explicitly rather than failing unpredictably on first error. A client whose failure modes you have rehearsed is a client you can leave running overnight.

A final integration habit: run the browser validation step, from the main guide's step nine, after any change to your parsing code, not just when you first write it. Field name assumptions are exactly the kind of thing a refactor breaks silently, and the browser pop up remains the fastest independent check available.

## Scaling Up: Chunking Large Regions and Managing Request Budgets

Three scaling problems arrive together as soon as a project grows past a handful of loci: regions too large for single requests, request budgets consumed by naive looping, and result sets too tangled to merge cleanly.

**Chunk large spans deterministically.** Split a long region into fixed width windows that abut without gaps or overlaps, query each with `feature=regulatory`, and merge on the feature identifier. Deterministic boundaries mean a straddling feature appears in exactly two windows, and a single deduplication pass restores correctness:

```python
def merged_features_for_span(chrom, start, end, width):
    seen = {}
    s = start
    while s < end:
        e = min(s + width - 1, end)
        for f in fetch_region(chrom, s, e):
            seen[f["id"]] = f
        s = e + 1
    return list(seen.values())
```

Choose a window width that keeps responses comfortably small for your densest locus, not your average one, because your worst window sets your worst latency.

**Convert loops into batches where the endpoint allows.** Full record lookups batch through the POST endpoint with an `ids` list, and this is the biggest lever on request count. A pipeline that first collects feature identifiers from region overlap calls and then fetches all full records in batched POSTs will typically make an order of magnitude fewer requests than one that looks up records individually. Reserve per identifier GET calls for ad hoc inspection during development.

**Budget requests before you start.** Estimate your request count from the number of regions or variants, multiply by retries, and confirm the plan fits a reasonable session. If it does not, the fixes are batching, caching, and narrower inputs, in that order. Caching is disproportionately valuable in regulatory work because annotations change only between releases [1], so a cache populated this month remains valid for iterative analysis for weeks.

**Keep the merge auditable.** When results from many requests combine into one table, retain the source request URL or region per row, at least in the intermediate files. When a count looks wrong at the analysis stage, per row provenance lets you trace any feature back to the exact request that produced it, which combined with the request log closes the loop from output to input.

## Three Practical Project Templates

**Template one: annotate a differentially expressed gene list.** Input is a list of gene stable identifiers from an RNA-seq contrast. For each gene, call the ID overlap endpoint with `feature=regulatory`, summarize feature types with a counter, and fetch full records in batch for the features found. Then filter full records for activity in cell types relevant to the tissue under study. The output is a per gene table of nearby regulatory elements with their active cell types, which is the cell type aware analogue of the TSS centered prioritization logic used in published gene prioritization methods built on Ensembl annotation [5]. Validation: pick three genes at random, confirm feature counts in the browser, and confirm one activity label per gene against the browser pop-up.

**Template two: annotate GWAS lead variants and nominate cell types.** Input is a list of lead rsIDs. Run the variant annotation pipeline from the earlier section, then pivot the tidy output: for each locus, list feature classes overlapped and, for full records fetched in batch, the set of cell types with ACTIVE labels. The pivot from variant to active cell type is where disease interpretation starts, because a locus whose only active signals sit in cell types irrelevant to the trait is a weaker candidate than one whose signals sit where the trait manifests. Feed the nominated gene and cell type pairs into downstream visualization and gene disease prioritization platforms such as GVViZ, which organizes gene and disease data for visualization and expression analysis [6], keeping the API derived evidence as the upstream layer rather than the final word.

**Template three: integrate your own epigenomic data.** Input is a BED file of your own ATAC-seq or ChIP-seq peaks. Export build regulatory features to BED as described earlier, intersect, and classify your peaks into those matching annotated features and those outside annotation. For matched peaks, fetch full records in batch and compare your sample's signal against the build's activity labels for the closest matching cell type; agreement supports your processing pipeline, and disagreement in a well studied cell type is worth investigating before interpretation. For unmatched peaks, check `feature=motif` queries in the same windows to see whether transcription factor motif instances explain the accessibility. This template turns the API from an annotation source into a quality control instrument for your own experiments, and it composes naturally with the expression correlation style analyses that draw directly on Ensembl records [4].

Each template shares the same skeleton, which is the point: confirm service, choose the overlap endpoint that matches the input object, throttle and cache, batch the full record fetches, filter by exact feature type strings and by cell types chosen from biology, export with assembly and release metadata, and validate a sample against the browser. Once that skeleton is tested once, it serves every project on this pattern.

## Frequently Asked Questions

### What is a regulatory feature in Ensembl?

It is a genome interval annotated by the Ensembl regulatory build as a candidate regulatory element, classified as a promoter, enhancer, CTCF binding site, transcription factor binding site, open chromatin region, or transcriptional cis regulatory region, with cell type specific activity labels.

### Which species have regulatory features through the REST API?

Human and mouse have the full regulatory build. Other chordate genomes have related resources such as microarray probe mappings, but regulatory feature queries return results only for the species covered by the build.

### What genome assembly do the results use?

The main REST server serves the current default assembly, GRCh38 for human. For older coordinates use the GRCh37 archive REST server, and always confirm the assembly of your own input data before overlapping.

### How do I find regulatory features near a gene?

Call the overlap endpoint with the gene stable identifier and the parameter `feature=regulatory`, or convert the gene to a coordinate range with a small flanking window and use the region overlap endpoint.

### Why did my request return a 404 error?

Usually the identifier does not exist on that species or assembly, the region string is malformed, or the feature ID came from an old release and was retired. Check the identifier in the genome browser first.

### Is there a request limit on the Ensembl REST API?

Yes. The public server rate limits unauthenticated clients and returns HTTP 429 when the limit is exceeded. Throttle your client with short sleeps, use batch POST calls, and retry with backoff.

### Can I download all regulatory features instead of querying the API?

Yes. Ensembl provides the same regulatory data through its public MySQL server and through bulk file downloads, which are better suited to whole genome exports, while the REST API suits targeted queries and live pipelines.

### How do I check that my API results are correct?

Pick a few feature identifiers from your output and compare coordinates, feature type, and cell type activity against the same features in the Ensembl genome browser, then confirm your assembly matches on both sides.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is a regulatory feature in Ensembl?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It is a genome interval annotated by the Ensembl regulatory build as a candidate regulatory element, classified as a promoter, enhancer, CTCF binding site, transcription factor binding site, open chromatin region, or transcriptional cis regulatory region, with cell type specific activity labels."
      }
    },
    {
      "@type": "Question",
      "name": "Which species have regulatory features through the REST API?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Human and mouse have the full regulatory build. Other chordate genomes have related resources such as microarray probe mappings, but regulatory feature queries return results only for the species covered by the build."
      }
    },
    {
      "@type": "Question",
      "name": "What genome assembly do the results use?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The main REST server serves the current default assembly, GRCh38 for human. For older coordinates use the GRCh37 archive REST server, and always confirm the assembly of your own input data before overlapping."
      }
    },
    {
      "@type": "Question",
      "name": "How do I find regulatory features near a gene?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Call the overlap endpoint with the gene stable identifier and the parameter feature=regulatory, or convert the gene to a coordinate range with a small flanking window and use the region overlap endpoint."
      }
    },
    {
      "@type": "Question",
      "name": "Why did my request return a 404 error?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Usually the identifier does not exist on that species or assembly, the region string is malformed, or the feature ID came from an old release and was retired. Check the identifier in the genome browser first."
      }
    },
    {
      "@type": "Question",
      "name": "Is there a request limit on the Ensembl REST API?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. The public server rate limits unauthenticated clients and returns HTTP 429 when the limit is exceeded. Throttle your client with short sleeps, use batch POST calls, and retry with backoff."
      }
    },
    {
      "@type": "Question",
      "name": "Can I download all regulatory features instead of querying the API?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Ensembl provides the same regulatory data through its public MySQL server and through bulk file downloads, which are better suited to whole genome exports, while the REST API suits targeted queries and live pipelines."
      }
    },
    {
      "@type": "Question",
      "name": "How do I check that my API results are correct?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Pick a few feature identifiers from your output and compare coordinates, feature type, and cell type activity against the same features in the Ensembl genome browser, then confirm your assembly matches on both sides."
      }
    }
  ]
}
</script>

## Sources

1. [Ensembl regulation resources](https://pubmed.ncbi.nlm.nih.gov/26888907/)
2. [Perancangan Web Service REST API Menggunakan PHP dan Framework Laravel di Tenta Tour Salatiga](https://doi.org/10.35870/jtik.v8i1.1269)
3. [Exploring the Roles of the Swi2/Snf2 Gene Family in Maize Abiotic Stress Responses.](https://pubmed.ncbi.nlm.nih.gov/39273633/)
4. [Impact of GC content on gene expression pattern in chicken.](https://pubmed.ncbi.nlm.nih.gov/23557030/)
5. [cis-positional information in regulatory single nucleotide variation prioritization](https://doi.org/10.1093/bib/bbaf631.053)
6. [Advancing clinical genomics and precision medicine with GVViZ: FAIR bioinformatics platform for variable gene-disease annotation, visualization, and expression analysis.](https://pubmed.ncbi.nlm.nih.gov/34174938/)