Skip to content
getgeolens.com

OGC API & Standards Endpoints

GeoLens implements several OGC and STAC standards alongside its REST API. This page covers the standards-based endpoints. Reach for the auto-generated reference for per-route schemas and request/response bodies; GET /api/conformance on your own instance is the authoritative conformance-class list.

Replace https://geolens.example.com with your GeoLens instance’s URL in every example below. Authentication follows the same rules as the rest of the API. See API Authentication for X-Api-Key, JWT, and OAuth options.

Provides the OGC API root, conformance declaration, and OpenAPI definition. Every OGC API client begins here.

Endpoints:

  • GET /api/: landing document with links to conformance, collections, and OpenAPI.
  • GET /api/conformance: list of conformance class URIs (Records, Features, CQL2, etc.).
  • GET /api/openapi.json: full OpenAPI definition.

Curl:

Terminal window
curl https://geolens.example.com/api/
curl https://geolens.example.com/api/conformance

Keep the trailing slash on the landing page URL. A bare /api answers with a 301 instead of the landing document. The redirect carries no CORS header, so a browser client drops it rather than following it. GeoLens 1.14.1 and later emit a relative Location: /api/, which resolves correctly behind a TLS edge; older instances built an absolute URL from the container port and could send desktop clients to an unreachable address.

For the full schema of every endpoint, see the Endpoints by Tag section in the auto-generated reference.

The catalog itself is exposed as an OGC API Records collection at collections/datasets. Use this to list, filter, and discover datasets programmatically.

Endpoints:

  • GET /api/collections/datasets/items: list catalog records (paginated, CQL2-filterable).

Curl:

Terminal window
curl https://geolens.example.com/api/collections/datasets/items

CQL2 filter (one example):

Terminal window
curl 'https://geolens.example.com/api/collections/datasets/items?filter=title%20LIKE%20%27%25hydrology%25%27&filter-lang=cql2-text'

The properties you can filter on are listed at /api/collections/datasets/queryables. The CQL2 grammar itself is advertised in /api/conformance (the cql2-text, cql2-json, basic-cql2, advanced-comparison-operators, and basic-spatial-functions conformance classes). The same grammar works on per-dataset feature collections; see Filtering with CQL2.

QGIS MetaSearch (built-in plugin):

1. Web > MetaSearch > MetaSearch (built-in, no install)
2. Services tab > New
3. Name: GeoLens
URL: https://geolens.example.com/api/
Catalog Type: OGC API - Records
4. Save -> Search tab to query records.

GDAL ogr2ogr / ogrinfo (OAPIF driver):

Terminal window
ogrinfo OAPIF:https://geolens.example.com/api/

Per-dataset feature access for vector layers. Useful for exporting subsets to GeoPackage, Shapefile, or any GDAL-supported format.

Use Features when the result is small or bounded, when the client needs attributes, or when it loads by viewport or filter. Use vector tiles when the dataset is large and users pan across all of it; the server then cuts tiles per request and the client holds only what is on screen. A viewport loader that pages this endpoint by bbox and follows next is in the examples gallery: maplibre/features-viewport.html.

Endpoints:

  • GET /api/collections/{dataset_id}/items: feature items for a single dataset, filterable by bbox and by a CQL2 filter (filter=, filter-lang=, filter-crs=), which is evaluated server-side and composed with bbox by AND — see Filtering with CQL2. datetime is accepted but ignored here: per-dataset feature tables hold user-uploaded data with no standard temporal column, so OGC API Features Core’s ignore-provision applies and the response is the unfiltered set.
  • GET /api/collections/{dataset_id}/queryables: the properties a CQL2 filter on that dataset may reference, as a JSON Schema document derived from the live table schema. The schema sets additionalProperties: false, so a filter referencing a property not listed there is rejected with HTTP 400. Raster collections answer 404 — they have no feature items.

Curl:

Terminal window
curl https://geolens.example.com/api/collections/{dataset_id}/items

Paging. Every response carries numberMatched (what the query found) and numberReturned (what this page holds). When they differ you are holding a partial result. The loop condition is the rel="next" link, not the counts: follow it until it stops appearing. Paging is keyset-based (after_gid=), so rows do not shift under a reader mid-scan. limit is clamped silently to the instance’s ogc_items_max_page_size (default 1000; see Settings -> Network), and the self link echoes the value that was applied.

Since 1.18.0, numberMatched on a filtered request (bbox, a property filter, or CQL2) is exact only up to 20,000 matching rows; past that, counting would cost a full scan on every page, so the response reports the planner’s row estimate instead. A response carrying an estimate sets an X-GeoLens-Number-Matched: estimated header — check for it if your client displays or compares the count. The estimate never drops below the rows already counted, so rel="next" stays reliable even when the number beside it is approximate; keep following the link rather than computing pages from the total yourself.

Since 1.16, per-dataset feature collections take the same CQL2 filter parameter the catalog collection has always taken (OGC API Features Part 3). filter-lang selects the encoding: cql2-text (the default) or cql2-json. A filter combines with bbox and with the property-filter extension (bare column=value query parameters) by AND.

Curl:

Terminal window
curl 'https://geolens.example.com/api/collections/{dataset_id}/items?filter=population%20%3E%2010000&filter-lang=cql2-text'

The properties a filter can reference are published per collection at /api/collections/{dataset_id}/queryables, derived live from the dataset’s table schema with additionalProperties: false — a filter naming any other property is rejected rather than silently ignored. filter-crs is accepted only as CRS84 (http://www.opengis.net/def/crs/OGC/1.3/CRS84), the CRS every GeoLens feature response uses; any other value answers 400.

The conformance document declares OGC API Features Part 3 (queryables, filter, features-filter) plus CQL2 cql2-text, cql2-json, basic-cql2, advanced-comparison-operators, and basic-spatial-functions. That set is what desktop clients check before pushing filter expressions to the server — attribute filters push down on any current QGIS, and QGIS 3.44 or later also pushes explicit spatial predicates (S_INTERSECTS and friends). Ordinary map panning uses the Core bbox parameter on every version and involves no CQL2. See Use GeoLens from QGIS.

QGIS Add Layer > OGC API Features:

1. Layer > Add Layer > Add WFS / OGC API Features Layer...
2. New connection: Name: GeoLens
URL: https://geolens.example.com/api/
Version: OGC API - Features
3. Private instance: under Authentication > Configurations, click +,
choose method "API Header", add header X-Api-Key = <your-api-key>,
save, and select that configuration on the connection.
4. Connect -> pick collection -> Add.

The authentication configuration lives in QGIS’s encrypted auth database, not in the .qgz, and the same configuration can be reused on the XYZ raster tile connection below (vector tiles authorize by signed parameters instead). The catalog itself appears in the collection list as datasets (the Records collection); add it like any other layer to browse records in the attribute table. GeoLens advertises the OGC API Features Part 3 filter and queryables conformance classes alongside the CQL2 classes since 1.16, so QGIS pushes a Build query filter down to the server as /collections/{dataset_id}/items?filter=... instead of downloading every feature and filtering client-side; QGIS builds the filter UI from the per-dataset /queryables document, and QGIS 3.44 and later also pushes explicit spatial predicates in a filter expression server-side — panning itself always uses the Core bbox parameter (see Use GeoLens from QGIS). A longer walkthrough with screenshots and a ready-made project is at qgis/README.md in the examples repo.

ArcGIS Pro (New OGC API Server connection):

1. Insert ribbon > Connections > Server > New OGC API Server
2. Server URL: https://geolens.example.com/api/?api_key=<your-api-key>
3. OK — the connection appears in the Catalog pane under Servers.
4. Expand the connection and drag a collection onto the map.

ArcGIS Pro speaks OGC API Features natively (Pro 2.8 or newer). The ?api_key= query parameter rides along on every request the connection makes; omit it entirely for anonymous access to public datasets. In the ArcGIS Maps SDK for JavaScript, OGCFeatureLayer takes the same landing page URL plus a collectionId; see arcgis-js/features.html for a runnable page that shows the request interceptor for sending X-Api-Key.

GDAL export to GeoPackage:

Terminal window
ogr2ogr -f GPKG out.gpkg \
--config GDAL_HTTP_HEADERS "X-Api-Key: <your-api-key>" \
"OAPIF:https://geolens.example.com/api/" \
{dataset_id}

GDAL sends custom headers through the GDAL_HTTP_HEADERS config option, so ogr2ogr does not need the query lane (point GDAL_HTTP_HEADER_FILE at a 0600 file instead if you would rather keep the key off the command line). Use ?api_key= only where a tool genuinely cannot set a header, such as ArcGIS Pro’s connection dialog or a desktop XYZ URL template. That query lane is deprecated for every client that can set a header because a key in a URL lands in access and proxy logs.

GeoLens exposes raster collections (and their items) under a STAC 1.0 catalog rooted at /api/stac/. Use any STAC client to search and ingest assets. A browser-side search that draws the matching footprints and tiles is in the examples gallery: stac/browse.html.

Endpoints:

  • GET /api/stac/: STAC root catalog.
  • GET /api/stac/collections: list of STAC collections.
  • GET /api/stac/search and POST /api/stac/search: full STAC search (bbox, datetime, collections, intersects, ids). GET takes the parameters as a query string; POST takes the same fields as a JSON body. datetime is an RFC 3339 instant (2026-08-12T15:53:30Z) or an interval start/end, with .. for an open end (2026-01-01T00:00:00Z/..); bare dates are rejected with 400 (pystac-client accepts bare dates and normalizes them before sending).

Curl:

Terminal window
curl https://geolens.example.com/api/stac/
curl 'https://geolens.example.com/api/stac/search?bbox=-122.5,37.5,-122.0,38.0&limit=10'

pystac-client:

from pystac_client import Client
client = Client.open("https://geolens.example.com/api/stac/")
search = client.search(
collections=["my-raster-collection"],
bbox=[-122.5, 37.5, -122.0, 38.0],
datetime="2024-01-01T00:00:00Z/2024-12-31T23:59:59Z",
)
for item in search.items():
print(item.id, item.assets["raster_tiles"].href)

Every raster item carries a raster_tiles asset whose href is an absolute XYZ tile template ending in /raster-tiles/{dataset_id}/tiles/{z}/{x}/{y}.png?v=<n>&pv=<n>; pass it through to a map client as is, including the v and pv cache-key parameters. That works unchanged for a public item. For a private item (one an authenticated search returned) the template carries no credential, so either send X-Api-Key with each tile request or mint a signed tile_url from GET /api/tiles/token/{dataset_id}/ (see Tile endpoints). On local storage an uploaded raster has only raster_tiles: a local-storage path has no authorized public URL, so the entry is omitted rather than published dead. An item imported by reference from another STAC catalog is the exception: from v1.16.1 it also carries that catalog’s data asset, the origin COG where it already lives, untouched. On S3-backed instances a published dataset also advertises thumbnail, overview, and its primary data asset — data (the COG) for an ordinary raster, or vrt for a VRT mosaic — each as a presigned URL valid for an hour.

The catalog also serializes itself as DCAT JSON-LD. These feeds are for harvesters rather than map clients: data.gov-style aggregators, EU open-data portals, and anything else that ingests RDF catalog records. Three profiles are served, each as a whole-catalog feed and as a per-dataset record.

Endpoints:

  • GET /api/datasets/dcat/: W3C DCAT 3 — the vendor-neutral baseline.
  • GET /api/datasets/dcat-us/3.0/: DCAT-US 3.0, the US federal profile, serialized against the GSA schema that data.gov harvesters expect.
  • GET /api/datasets/geodcat-ap/: GeoDCAT-AP 2.0.0, the EU/INSPIRE geospatial profile. It carries the ISO 19115 metadata GeoLens already stores — lineage, access and use constraints, responsible-party roles, maintenance frequency, reference system, and spatial/temporal extent.
  • GET /api/datasets/{dataset_id}/dcat/ (likewise .../dcat-us/3.0/ and .../geodcat-ap/): the same three profiles for one dataset.

Curl:

Terminal window
curl https://geolens.example.com/api/datasets/dcat/
curl https://geolens.example.com/api/datasets/geodcat-ap/

All six routes answer application/ld+json. There is no content negotiation to Turtle or RDF/XML — asking for those in Accept still returns JSON-LD. On the DCAT 3 routes Accept-Language selects among a record’s stored translations; the DCAT-US and GeoDCAT-AP serializers emit whatever language the record holds and report it in Content-Language — except the GeoDCAT-AP whole-catalog feed, which sends no such header.

The feeds respect dataset visibility, so an anonymous harvester sees the public, published catalog and a credentialed one sees everything that caller can read. Catalog feeds page with limit and offset; limit defaults to its own maximum of 10000. Each catalog response reports its own coverage in X-GeoLens-Source-Dataset-Count, -Serialized-Dataset-Count, -Excluded-Dataset-Count, and -Metadata-Fallback-Dataset-Count. A record with no description is serialized with its title in place of one rather than dropped from the feed; that is what the fallback count tracks, and a per-dataset response names the substituted terms in X-GeoLens-Metadata-Fallback-Fields.

Validation. Append validation/ to any of the six routes for a metadata-quality report — valid, error_count, and errors[] entries carrying path, schema_path, validator, and message. A catalog report repeats the coverage counts above and takes no paging of its own, so it covers the same first 10000 visible datasets; a per-dataset report adds uses_metadata_fallback and metadata_fallback_fields. DCAT-US is checked against the vendored GSA JSON Schema; DCAT 3 and GeoDCAT-AP publish no machine-consumable JSON Schema, so those two reports are structural and required-field checks in the same shape. A validation route is scoped exactly like the feed it validates and never reports on records the caller cannot see.

Terminal window
curl https://geolens.example.com/api/datasets/dcat-us/3.0/validation/

DCAT-US needs a contact. contactPoint is mandatory in DCAT-US 3.0 and GeoLens will not invent one. When a record has no usable contact and no DCAT_CONTACT_EMAIL is set, the DCAT-US routes answer RFC 7807 503 rather than publish a feed missing a required field — the validation route still answers 200 and reports the gap, which is where to look when the export fails. Point DCAT_CONTACT_EMAIL at a monitored organization mailbox to supply a catalog-level fallback. DCAT 3 and GeoDCAT-AP carry no such requirement and serve either way.

GeoLens serves vector and raster tiles. Public, published datasets serve tiles to anyone with no token. Private datasets serve them via signed access tokens, which are not generic API keys: they are HMAC-signed, scoped to a single dataset, and time-limited. They exist so that a map client whose tile URL template cannot carry a header can still read a private layer without exposing a permanent credential to the browser. The examples gallery has runnable pages for both.

Vector tile (MVT) URL shape:

https://geolens.example.com/api/tiles/{table_path}/{z}/{x}/{y}.pbf?sig={sig}&exp={exp}&scope={scope}

{table_path} is the schema-qualified table name (data. plus the table_name that GET /api/datasets/{id} reports, e.g. data.my_table), and sig, exp, and scope are the HMAC-signed parameters returned by GET /api/tiles/token/{dataset_id}/. Omit all three for a public dataset.

Raster tile URL shape:

https://geolens.example.com/raster-tiles/{dataset_id}/tiles/{z}/{x}/{y}.png?v=<n>&pv=<n>

Raster tiles live at the site root, not under /api/. Read the template from the collection’s rel="tiles" link on GET /api/collections/{dataset_id} (or from the STAC item’s raster_tiles asset) rather than building it by hand: the ?v= and ?pv= cache-key parameters matter, because a raster replace bumps the first and a publication or visibility change bumps the second, and a hand-built URL keeps serving the old cache entry. For a private raster the token response’s tile_url is the same template with sig, exp, scope, and those parameters already appended. A tile outside the dataset’s footprint answers 204 (draw nothing); a dataset that does not exist answers 404 from v1.14.0 (older releases answered 204 for both). The /api/tiles/raster-proxy/... route in the auto-generated reference is what nginx forwards /raster-tiles/ to, and the fallback for a deployment without nginx; prefer the /raster-tiles/ template.

Obtaining a tile token.

  • GET /api/tiles/token/{dataset_id}/: mint a token for one dataset. Vector datasets return kind: "vector" with sig, exp, scope, and expires_in; raster datasets return kind: "raster" with those plus a tile_url (origin-relative — prefix your instance’s origin; it already carries sig/exp/scope, the pv= publication counter, and the v= cache version when one is set) and the layer’s bounds, minzoom, maxzoom, tile_size, and format.
  • POST /api/tiles/tokens/: the batch form, up to 50 dataset IDs in one request. Per-dataset failures come back as {"error": ...} entries without failing the batch.

Minting is authorized by dataset visibility. A public, published dataset mints anonymously; a private one answers an anonymous mint with 401, so a static page holding no credential cannot mint its own token for private data. Something server-side has to hold the key and hand tokens down. The endpoints accept the same authentication as the rest of the API (X-Api-Key, JWT, OAuth-issued JWT). Tile tokens themselves are not substitutes for those credentials. They are derived, scoped, and expire.

exp is always a 15-minute boundary, usually the next one. When that boundary is under a minute away the mint skips to the following one, so a fresh token lasts from 60 seconds to less than 16 minutes. Read expires_in off the response rather than assuming a fixed TTL, and re-mint before it passes: map clients keep requesting whatever template you handed them.

Expiry is not the only thing that ends a template. From 1.19.0, unpublishing a dataset or making it private retires the signed templates already issued for it, on the raster, vector and cluster routes alike, whatever time was left on them. scope folds in a publication counter that every publication-status or visibility change rolls, in either direction, so an outstanding signature stops verifying. Only those two transitions take access away, because a public, published dataset needs no signature in the first place. The change takes hold within a minute, the lifetime of the application’s dataset metadata cache. Ordinary edits, reuploads and replaces retire nothing.

One caveat applies to raster tiles, the only kind the bundled nginx and a CDN in front of it cache. A .png URL someone copied while the dataset was public can still read the entry that URL already populated, until the cache’s own lifetime elapses, because a cache answers it without asking the application. The pv parameter is part of that cache key, so a template emitted after the transition carries a different value and cannot reach an entry stored before it. A CDN configured to strip query strings collapses the versions onto one entry, as it already does for v.

Embed tokens are not tile tokens. X-Embed-Token is minted per map by an authenticated owner, and the tile routes read it from the request header only, so it cannot ride along in a URL template in place of a tile token.

QGIS Add XYZ Tiles (raster):

QGIS’s XYZ Tiles connection is raster-only. Point it at the .png template:

1. Browser panel > XYZ Tiles > Right-click -> New Connection...
2. Name: GeoLens - <dataset>
URL: https://geolens.example.com/raster-tiles/{dataset_id}/tiles/{z}/{x}/{y}.png?v=<n>&pv=<n>
3. Private dataset: select the API Header authentication configuration
(X-Api-Key) described under OGC API Features above, or paste the
token response's tile_url (origin prefixed) as the URL.
4. Click OK, then drag the connection onto the canvas.

QGIS Add Vector Tile Layer (MVT):

1. Layer > Add Layer > Add Vector Tile Layer...
2. New > New Generic Connection...
Name: GeoLens - <dataset>
URL: https://geolens.example.com/api/tiles/{table_path}/{z}/{x}/{y}.pbf
3. Private dataset: append ?sig=<sig>&exp=<exp>&scope=<scope> from
GET /api/tiles/token/{dataset_id}/ to the URL. An API Header
configuration does not work here — a non-public vector tile
answers 403 "Signature required for non-public tiles" whenever
the signed parameters are absent, whoever is asking.
4. Click OK, then add the connection.

A pasted token is a session credential: it expires within 16 minutes, and it is retired early if the dataset is unpublished or made private. Either suits a quick share and not a project you open next week. For a working raster session use the authentication configuration instead; a private vector tile layer has no header-based alternative, so re-mint and re-paste the signed parameters when they lapse.

MapLibre GL JS (vector tile source):

The MVT layer name inside each tile is the same schema-qualified {table_path} as in the URL — use it as the source-layer:

map.addSource('geolens-roads', {
type: 'vector',
tiles: [
'https://geolens.example.com/api/tiles/data.roads/{z}/{x}/{y}.pbf?sig=<sig>&exp=<exp>&scope=<scope>',
],
});
map.addLayer({
id: 'roads-line',
type: 'line',
source: 'geolens-roads',
'source-layer': 'data.roads',
paint: { 'line-color': '#3b6fd4', 'line-width': 1.5 },
});

Because tile tokens expire, a long-lived custom MapLibre app should mint a fresh token via GET /api/tiles/token/{dataset_id}/ (or the batch POST) and rebuild the source URL when one lapses. For a zero-code alternative, GeoLens’s built-in share embeds handle tokens for you; see Map Builder.

A runnable version of this snippet with error handling is maplibre/vector-tiles.html in the examples gallery; the raster-tile pages there cover MapLibre, Leaflet, OpenLayers, and the ArcGIS Maps SDK for JavaScript. The ArcGIS WebTileLayer takes the same raster template with {level}/{col}/{row} in place of {z}/{x}/{y}; see arcgis-js/imagery.html.