Short answer
OGC API - Features is a RESTful standard for publishing vector data over HTTP. Instead of downloading a whole shapefile or GeoPackage, a client requests individual features or filtered subsets through predictable URLs like /collections/wells/items?bbox=-5,48,-1,51&limit=100, and the server replies with GeoJSON. It is the modern, JSON-first successor to WFS, built on OpenAPI and ordinary web conventions so that a feature collection behaves like any other web resource.
For a team moving from one-off GIS exports toward a maintained data service, the value is that the same endpoint serves a desktop GIS, a web map, and a scripted pipeline without exporting three copies of the data.
The resource model you actually call
The standard (formally OGC API - Features - Part 1: Core, which is also published as ISO 19168-1) defines a small, fixed set of paths. Every compliant server exposes them, which is what makes the API predictable:
/— the landing page, with links to everything below./conformance— the list of conformance classes the server implements (Core, GeoJSON, HTML, CRS, Filtering, and so on). Read this first; it tells you what the server can actually do./api— the OpenAPI 3.0 definition describing every operation and parameter./collections— the catalogue of available feature collections (your layers)./collections/{collectionId}— metadata for one collection, including its spatial and temporal extent and supported CRS list./collections/{collectionId}/items— the features, returned as a GeoJSONFeatureCollection./collections/{collectionId}/items/{featureId}— a single feature as a GeoJSONFeature.
The default response media type for items is application/geo+json. Most servers also offer text/html for human browsing and sometimes vendor formats. Content negotiation works either through the Accept header or an f=json / f=html query parameter.
Why it replaced WFS in practice
WFS (1.x and 2.0) is XML-centric: you fetch a verbose GetCapabilities document, build GML or WFS request payloads, and parse XML responses. OGC API - Features keeps the same conceptual model — collections of features with filters — but expresses it in JSON and clean URLs. You can paste an items URL into a browser, follow rel="next" links to page through results, and feed responses straight into any GeoJSON-aware library. The OGC explicitly designed it as the modern web API counterpart to WFS, not a replacement for the underlying data concepts.
Querying: bbox, datetime, properties, and CQL2
The Core conformance class gives you three filters on the items endpoint out of the box.
Bounding box. bbox=minx,miny,maxx,maxy returns features whose geometry intersects that rectangle. By default the coordinates are interpreted in CRS84 (WGS84 longitude, latitude order). A four-value bbox is 2D; a six-value bbox adds min/max elevation:
GET /collections/seismic_lines/items?bbox=-10.5,46.0,-4.0,49.5&limit=500
Temporal. datetime accepts an instant or an interval with a slash, and open-ended intervals use ..:
GET /collections/surveys/items?datetime=2023-01-01T00:00:00Z/..
Property equality. Any queryable attribute can be used directly as a parameter, e.g. ?status=active.
For anything richer, Part 3: Filtering adds CQL2 (Common Query Language 2). You send a filter expression and declare its language with filter-lang=cql2-text:
GET /collections/wells/items?filter-lang=cql2-text&filter=depth_m > 200 AND lithology = 'basalt'
CQL2 also defines spatial predicates such as S_INTERSECTS, S_WITHIN, and S_DWITHIN, and temporal predicates like T_AFTER. A spatial filter looks like:
filter=S_INTERSECTS(geom, BBOX(-6,47,-2,50))
Always confirm the /conformance response lists the CQL2 classes before relying on these — a server that only implements Core will silently ignore an unsupported filter parameter, which is a common source of "my filter isn't working" confusion.
Pagination done correctly
The limit parameter sets page size. Servers enforce a maximum (commonly 1000, sometimes 10000), so requesting limit=1000000 does not return everything. The correct way to walk a large collection is link following, not offset arithmetic: each page's response includes a links array with a rel="next" entry whose href is the exact URL of the following page. Implementations may use an offset parameter or an opaque cursor token internally — your client should treat the next href as a black box and just follow it until no next link is returned.
import requests
url = "https://demo.example.org/collections/wells/items?limit=1000"
features = []
while url:
r = requests.get(url, headers={"Accept": "application/geo+json"}).json()
features.extend(r["features"])
url = next((l["href"] for l in r.get("links", []) if l["rel"] == "next"), None)
CRS handling
Core returns geometries in CRS84 (http://www.opengis.net/def/crs/OGC/1.3/CRS84). The optional CRS conformance class lets you request other coordinate reference systems. The collection metadata lists supported CRS URIs in its crs array; you then request a CRS with the crs query parameter and tell the server which CRS your bbox is in with bbox-crs. CRS identifiers are full URIs, e.g. EPSG:3857 is http://www.opengis.net/def/crs/EPSG/0/3857. If you need projected coordinates from a server that only advertises Core, you must reproject client-side — ST_Transform in PostGIS or ogr2ogr -t_srs after download.
Worked example: publishing with pygeoapi
The most widely used open-source implementation is pygeoapi (a Python reference implementation). To serve a PostGIS table:
- Install and point an environment variable at a config file:
pip install pygeoapi
export PYGEOAPI_CONFIG=pygeoapi-config.yml
export PYGEOAPI_OPENAPI=openapi.yml
pygeoapi openapi generate $PYGEOAPI_CONFIG --output-file $PYGEOAPI_OPENAPI
pygeoapi serve
- Define one collection per layer under
resourcesin the YAML, with a PostGIS provider:
resources:
wells:
type: collection
title: Borehole locations
extents:
spatial:
bbox: [-11, 45, 2, 52]
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
providers:
- type: feature
name: PostgreSQL
data:
host: localhost
dbname: geo
user: reader
password: ${PG_PASSWORD}
table: wells
id_field: well_id
geom_field: geom
- Browse
http://localhost:5000/collections/wells/itemsand confirm GeoJSON comes back. Other backends (GeoPackage, Elasticsearch, OGR, CSV) swap in by changing the providername.
QGIS connects to any of this natively: Layer ▸ Add Layer ▸ Add WFS / OGC API - Features Layer, paste the landing page URL, and it discovers the collections. GDAL exposes the OAPIF driver, so ogr2ogr out.gpkg "OAPIF:https://demo.example.org" wells downloads a layer to a local file.
Validating a server
Before trusting an endpoint as a product:
- Fetch
/conformanceand confirm it actually lists the classes you need (GeoJSON, CRS, Filtering/CQL2). Do not assume. - Run the official OGC Team Engine / ETS for OGC API - Features against the landing page to check compliance against the abstract test suite.
- Page through a collection to the end and verify the feature count matches the source table (
SELECT count(*)in PostGIS). - Test a known feature by
idand compare its attributes and coordinate values against the source. - Check that geometries come back in the CRS you expect — a CRS84 vs EPSG:4326 axis-order mistake shows up as swapped latitude/longitude.
Common pitfalls and why they happen
- Axis order surprises. CRS84 is longitude-latitude; EPSG:4326 is officially latitude-longitude. Clients that assume the wrong order plot points in the ocean. This happens because the GeoJSON requirement (lon, lat) and the EPSG authority definition disagree, and OGC API - Features standardises on CRS84 to avoid it.
- Filters silently ignored. A
filterquery sent to a Core-only server returns the full unfiltered set with HTTP 200. Always gate on the conformance list. - Treating
limitas "give me everything." Server caps mean you get one page and assume that is the whole dataset. Follownextlinks. - No spatial index behind the API. A PostGIS table without a GiST index makes every bbox query a sequential scan. Add
CREATE INDEX ON wells USING GIST (geom);before exposing the collection.
Bathyl perspective
We treat an OGC API - Features endpoint as the publishing layer over a properly modelled spatial database, not as a wrapper around whatever files are in a folder. The database holds the authoritative geometry with a spatial index and explicit CRS; the API exposes a stable, documented contract that a client's analyst can query for years without us re-exporting anything.
Related reading
- Designing a Geological Data API
- Spatial Data Catalogs for Project Teams
- From PDF Report to Spatial System
- Spatial data products