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.
OGC API - Common
Section titled “OGC API - Common”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:
curl https://geolens.example.com/api/curl https://geolens.example.com/api/conformanceKeep 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.
OGC API - Records
Section titled “OGC API - Records”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:
curl https://geolens.example.com/api/collections/datasets/itemsCQL2 filter (one example):
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 > New3. Name: GeoLens URL: https://geolens.example.com/api/ Catalog Type: OGC API - Records4. Save -> Search tab to query records.GDAL ogr2ogr / ogrinfo (OAPIF driver):
ogrinfo OAPIF:https://geolens.example.com/api/OGC API - Features
Section titled “OGC API - Features”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 bybboxand by a CQL2filter(filter=,filter-lang=,filter-crs=), which is evaluated server-side and composed withbboxby AND — see Filtering with CQL2.datetimeis 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 CQL2filteron that dataset may reference, as a JSON Schema document derived from the live table schema. The schema setsadditionalProperties: 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:
curl https://geolens.example.com/api/collections/{dataset_id}/itemsPaging. 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.
Filtering with CQL2
Section titled “Filtering with CQL2”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:
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 - Features3. 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 Server2. 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:
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.
STAC 1.0
Section titled “STAC 1.0”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/searchandPOST /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.datetimeis an RFC 3339 instant (2026-08-12T15:53:30Z) or an intervalstart/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:
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.
DCAT catalog feeds
Section titled “DCAT catalog feeds”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:
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.
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.
Tile endpoints
Section titled “Tile endpoints”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 returnkind: "vector"withsig,exp,scope, andexpires_in; raster datasets returnkind: "raster"with those plus atile_url(origin-relative — prefix your instance’s origin; it already carriessig/exp/scope, thepv=publication counter, and thev=cache version when one is set) and the layer’sbounds,minzoom,maxzoom,tile_size, andformat.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}.pbf3. 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.