# STAC catalog API

> Browse and search the full catalog with any STAC client — collections, item search, pagination, and asset access.

The catalog is a standards-compliant **STAC API** (SpatioTemporal Asset
Catalog). Anything that speaks STAC works against it out of the box:
`pystac-client`, [STAC Browser](https://radiantearth.github.io/stac-browser/#/external/api.darly.io/),
[STAC Map](https://developmentseed.org/stac-map/?href=https%3A%2F%2Fapi.darly.io),
the QGIS STAC plugin, GDAL. No API key needed to browse — anonymous callers
get 30 requests/min per IP, keys raise that to your plan's limit.

```text
https://api.darly.io
```

## Collections and Items

A **Collection** is one coherent elevation dataset in Darly. It is either a
global product, such as Copernicus DEM, or a specific selected dataset from a
national (or other regional) lidar programme — for example, the USGS 3DEP
seamless 1 m DTM. It is not a catch-all for every dataset an agency publishes.
Collection metadata describes the source, coverage, resolution, and available
surface models.

An **Item** is one spatial piece of that Collection, usually a source raster
tile. Its geometry and bounding box show the area it covers; its properties
carry the relevant acquisition and projection metadata; and its assets link to
the terrain (`dtm`) and/or surface (`dsm`) raster for that area. Search for
Items when you need data covering a place or time; use Collection metadata to
choose the dataset first.

## Endpoints

| Endpoint | Methods | Purpose |
|---|---|---|
| `/` | `GET` | Landing page + conformance links |
| `/collections` | `GET` | List all collections |
| `/collections/{id}` | `GET` | One collection's metadata |
| `/collections/{id}/items` | `GET` | Items in a collection (paginated) |
| `/collections/{id}/items/{item_id}` | `GET` | One item with full asset links |
| `/search` | `GET`, `POST` | Cross-collection spatial/temporal search |

`POST` is accepted only on `/search`. Collection IDs and coverage are listed
in [coverage & datasets](/docs/coverage).

## Using an API key

Browsing works without a key, so attaching one is optional — but it does two
things for STAC:

- **Raises your rate limit** from the anonymous 30 req/min per IP to your
  plan's limit.
- **Unlocks download links.** On paid plans, the single-item endpoint replaces
  Darly-hosted asset `href`s with `/dl/...` links (see [items and
  assets](#items-and-assets)); anonymous callers never see them.

Send the key the same way as everywhere else — the `X-API-Key` header is
preferred. It attaches to *any* STAC call; the search below just shows the
common case:

```bash cURL
curl -X POST "https://api.darly.io/search" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $DARLY_API_KEY" \
  -d '{"collections": ["usa-usgs-3dep-1m-seamless"], "limit": 10}'
```

```python pystac-client
from pystac_client import Client

# headers ride on every request the client makes — search, item fetch, paging.
catalog = Client.open(
    "https://api.darly.io",
    headers={"X-API-Key": DARLY_API_KEY},
)
```

```javascript JavaScript
const resp = await fetch("https://api.darly.io/search", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": DARLY_API_KEY,
  },
  body: JSON.stringify({ collections: ["usa-usgs-3dep-1m-seamless"], limit: 10 }),
})
```

For tools whose STAC field only takes a URL (some GIS clients, `/vsicurl`),
append `?api_key=…` to the request instead of setting a header — but a key in a
URL can leak into logs, and it won't ride along as clients follow `next` links.
`Authorization: Bearer` works too. All three forms, the security trade-offs, and
how quotas attach to your account are covered in
[authentication](/docs/authentication).

## Searching

```bash cURL
curl -X POST "https://api.darly.io/search" \
  -H "Content-Type: application/json" \
  -d '{
    "collections": ["usa-usgs-3dep-1m-seamless"],
    "bbox": [-122.52, 37.70, -122.35, 37.83],
    "limit": 10
  }'
```

```python Python
from pystac_client import Client

catalog = Client.open("https://api.darly.io")
search = catalog.search(
    collections=["usa-usgs-3dep-1m-seamless"],
    bbox=[-122.52, 37.70, -122.35, 37.83],
    max_items=10,
)
for item in search.items():
    print(item.id, item.assets["dtm"].href)
```

```javascript JavaScript
const resp = await fetch("https://api.darly.io/search", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    collections: ["usa-usgs-3dep-1m-seamless"],
    bbox: [-122.52, 37.70, -122.35, 37.83],
    limit: 10,
  }),
})
const { features, links } = await resp.json()
```

Standard STAC search parameters are supported: `collections`, `bbox`,
`intersects` (GeoJSON geometry), `datetime` ranges, `ids`, and `limit`.

## Pagination

Page size defaults to **10 items** and is capped at **100**. Responses carry
a `rel: next` link; follow it until absent (`pystac-client` does this for
you):

```json Response
{
  "type": "FeatureCollection",
  "features": ["…"],
  "links": [
    { "rel": "next", "href": "https://api.darly.io/search?token=…" }
  ]
}
```

Prefer narrow `bbox`/`datetime` filters over crawling: search and item-list
requests consume rate-limit budget proportional to their page size (one unit
per 10 items requested — see [limits](/docs/limits)), so whole-catalog dumps
are slow by design.

## Items and assets

### Item properties

Darly uses the following properties consistently across elevation Items:

| Property | Meaning |
|---|---|
| `datetime` | Representative acquisition timestamp. For an Item with a temporal interval, this is always the elapsed-time midpoint between `start_datetime` and `end_datetime`. |
| `start_datetime`, `end_datetime` | Inclusive source acquisition bounds. Both are present together or both are absent. |
| `created`, `updated` | Creation and last-update timestamps for the STAC metadata. |
| `vertical_datum` | Canonical vertical reference for elevation values, such as `NAVD88`, `CGVD2013`, or `EGM2008`. |
| `vertical_datum_epsg` | EPSG identifier for the vertical CRS when one is available. |
| `elevation_unit` | Elevation unit; currently `m` for every collection. |
| `checksum:sha256` | Checksum used to track changes to the upstream source record. |

For interval Items, the midpoint helps clients that display or sort Items by one date. It does not
mean that the whole raster was acquired at that instant: use `start_datetime`
and `end_datetime` when the complete acquisition period matters. An Item that
represents a true instant keeps its source-provided `datetime` and has no
interval fields.

Elevation rasters use consistent asset keys across all collections —
`dtm` (terrain) and/or `dsm` (surface) — with projection metadata
(`proj:code`, `proj:transform`, `proj:shape`) on each asset, so GDAL and
rasterio can read windows without opening the file blind.

Asset `href`s come in two flavors:

- **Source-hosted** (USGS, NRCan, AWS Open Data, OpenGeoHub): direct public
  HTTPS URLs — fetch them however you like, they never touch your Darly
  quotas.
- **Darly-hosted** (Mexico COGs re-hosted on our storage): on paid plans, the
  single-item endpoint (`/collections/{id}/items/{item_id}`) replaces these
  with `/dl/...` download links — see [COG downloads](/docs/downloads).
  Multi-item responses (search, item lists) omit Darly-hosted assets; fetch
  the individual item to get its download link.

## Errors

- Unknown collections — and collections your plan doesn't include — return
  the same `404` `{"detail": "Not found"}`.
- `POST` anywhere but `/search` → `405`.
- Over the per-minute limit → `429` with `X-RateLimit-*` headers; see
  [rate limits & quotas](/docs/limits).
