norway-open-data-sdk
v0.9.0
Published
One TypeScript interface for Norway's open public APIs.
Maintainers
Keywords
Readme
Norway Open Data SDK
One typed TypeScript interface for Norwegian public data.
Norway Open Data SDK provides a consistent, runtime-validated client for official Norwegian APIs from Brønnøysundregistrene, SSB PxWeb and SSB Klass, FHI, Kartverket/Geonorge and its Hydrographic Service, Entur, MET Norway and its Frost archive, Data.norge, Norges Bank, Stortinget, Statens vegvesen, NVE, BarentsWatch, Fiskeridirektoratet, Miljødirektoratet and NIBIO. It also wraps the documented third-party public electricity API from Hva koster strømmen?.
- 15 public-sector organizations through 21 official API integrations, plus 1 third-party derived API
- 24 service namespaces
- 135 supported namespace methods in the 0.9.0 surface
- Runtime-validated responses
- Cross-provider profiles that answer one question from several agencies
- Official weather warnings, nowcasts, tides, sun times and climate history alongside forecasts
- Auto-paginating async iterators for list endpoints
- Live AIS position streaming as a plain
AsyncIterable, with cancellation - Reusable OAuth2 client-credentials support with single-flight token refresh
- ESM, CommonJS and TypeScript support
- Runs on Node.js, Deno, Bun and edge runtimes from one web-standard build
- Deterministic offline tests plus representative opt-in live contract probes
- Enforced statement, branch, function and line coverage gates
Requests go directly from your Node.js application to the source API. The SDK has no hosted backend, database, account system or scraping layer.
Installation
Runs on Node.js 22+, Deno, Bun and edge runtimes such as Cloudflare Workers. TypeScript declarations are included. See Runtime support for what a host runtime must provide.
Version 0.9.0 is the current release. Install it with:
npm install norway-open-data-sdkContributors can also build and test a local tarball:
git clone https://github.com/iamkm1/Norway-Open-Data.git
cd Norway-Open-Data
corepack pnpm install
corepack pnpm build
corepack pnpm packInstall the generated tarball from another project:
npm install /path/to/Norway-Open-Data/norway-open-data-sdk-0.9.0.tgzQuick start
import { NorwayOpenData, type Company, type NorwegianAddress } from "norway-open-data-sdk";
const norway = new NorwayOpenData({
applicationName: "my-application",
contactEmail: "[email protected]",
});
const company = await norway.companies.get("923609016"); // OpenDataResponse<Company>
console.log(company.data.name);
const addresses = await norway.addresses.search({
query: "Haraldsgata 100, Haugesund",
limit: 1,
});
console.log(addresses.data.items[0]); // NorwegianAddress
const profile = await norway.profiles.company("923609016");
console.log(profile.data.location);Result types follow each provider's own domain language rather than one shared prefix: companies
return Company, Kartverket lookups return NorwegianAddress, and every entity type is exported
from the package root, so editor auto-import finds the exact name.
Anonymous methods work without configuration. Entur and NVDB require applicationName; MET
requires applicationName and contactEmail; NVE HydAPI requires the caller's own API key.
Supported providers
| Source | Namespace | Capabilities | Access |
| ------------------------------- | --------------------- | ------------------------------------------------------------------------------------ | ------------------------------ |
| Brønnøysundregistrene | companies | Organizations, search and sub-entities | Anonymous |
| Statistics Norway / SSB | statistics, klass | PxWeb statistics; Klass classifications, codes and mappings | Anonymous |
| FHI | health | Health-register statistics with preserved suppression flags | Anonymous |
| Kartverket | addresses, places | Addresses, place names, nearby search and the municipality containing a point | Anonymous |
| Kartverket Hydrographic Service | tides | Tide tables, observed and forecast water levels, surge and reference levels | Anonymous |
| Geonorge / Kartverket | geodata | Geospatial dataset/service catalogue and typed discovery | Anonymous; experimental |
| Miljødirektoratet | environment | Selected Naturbase protected, NiN and intervention-free layers | Anonymous; experimental |
| NIBIO | land | Generalized AR50 land-resource classification | Anonymous; experimental |
| Entur | transport | Autocomplete, departures and journeys | Identification required |
| MET Norway | weather | Forecasts, official warnings, nowcast, sun and moon, ocean and air-quality forecasts | Identification + contact email |
| MET Norway Frost | climate | Weather stations, historical and latest observations, climate normals | Free client ID; experimental |
| Data.norge | catalog | Datasets, data services and publishers | Anonymous |
| Norges Bank | currency | Exchange rates, policy rate and NOWA | Anonymous |
| Stortinget | parliament | Representatives, parties, cases, votes, questions and meetings | Anonymous |
| Statens vegvesen / NVDB | roads | Road metadata, objects and network segments | Identification required |
| NVE | energy, hazards | Energy data, warnings and hydrology | Anonymous; API key for HydAPI |
| BarentsWatch AIS | ais | Vessel positions, tracks, area search and live AIS streaming | OAuth2 AIS-client |
| BarentsWatch | marine | Wave and sea-current point forecasts | OAuth2 API-client |
| Fiskeridirektoratet | fisheries | Fishing-vessel register and public aquaculture register | Anonymous |
| Hva koster strømmen? | electricity | Hourly spot prices for all five bidding zones | Anonymous third-party API |
profiles composes several providers into one answer: company combines Brønnøysundregistrene
with a Kartverket address match, address combines Kartverket, MET Norway, NVE and NVDB,
municipality combines SSB, FHI, Brønnøysundregistrene and NVE, and vessel combines BarentsWatch
AIS with the Fiskeridirektoratet vessel register, MET Norway and Kartverket.
natureAtLocation combines Naturbase, NIBIO and Kartverket with per-component provenance and
partial-failure handling. conditionsAtLocation combines MET Norway, NVE, Kartverket, its
Hydrographic Service and, when configured, Frost into one answer about the weather, sea and
daylight at a point.
BarentsWatch has two distinct service contracts in the SDK, and they need separate credentials. BarentsWatch issues two kinds of self-registered client, and neither one's secret works for the other service:
ais→ an AIS-client, whose tokens carry theaisscope, configured undercredentials["barentswatch-ais"].marine→ an API-client, whose tokens carry theapiscope, configured undercredentials.barentswatch.
Register each at BarentsWatch MyPage.
Fiskeridirektoratet likewise has two independent registers behind one namespace. Both are open and
need no credentials, but they are separate services with separate contracts and paging models, so
they keep separate provider descriptors (fiskeridir-vessels and fiskeridir-aqua) and separate
record types. A fishing vessel and an aquaculture site are not forced into a shared model.
SSB has two distinct service contracts in the SDK:
statistics→ statistical values, table metadata and JSON-stat datasets from PxWeb.klass→ official classifications, versions, codes, code changes and correspondence mappings.
They belong to one public-sector organization, but use separate provider descriptors because their endpoints, limits, response formats and cache policies differ.
MET Norway and Kartverket are likewise one organization each with two service contracts:
weather→ api.met.no products under one identification requirement (met);climate→ the Frost observation archive, which needs its own free client ID (met-frost).addressesandplaces→ Kartverket's open Geonorge and Kommuneinfo APIs (kartverket);tides→ the Hydrographic Service's water-level API, with its own terms and mandatory credit (kartverket-tides).
Hva koster strømmen? is an independent third-party service, not a government or official data provider. Its documentation says the API derives EUR electricity prices from ENTSO-E and converts them to NOK using the latest Norges Bank exchange rate.
See the complete capability matrix for every namespace, method, access requirement and known limitation.
MCP server
Looking for AI-client integration? See Norway Open Data MCP, a local stdio MCP server built on this SDK.
npx -y norway-open-data-mcpThe MCP server exposes curated, read-only tools for AI clients while this SDK continues to provide the underlying Norwegian public-data integrations, validation, retries, caching and typed responses.
They are separate packages in separate repositories, and the dependency runs one way:
norway-open-data-mcp depends on norway-open-data-sdk, never the reverse. If you are writing
TypeScript or JavaScript, install this SDK and call it directly — the MCP server is not required and
adds nothing to a normal application. Install it only when you want an AI client to reach this data
through the Model Context Protocol.
Norway Open Data ecosystem
- Norway Open Data SDK — typed TypeScript access to Norwegian public APIs.
- Norway Open Data MCP — curated MCP tools for AI clients, built on top of this SDK.
Common examples
The following examples use the configured norway client from the quick start.
Company lookup
const company = await norway.companies.get("923609016");
console.log(company.data.name);Catalogue search
const results = await norway.catalog.search({
query: "public transport",
type: ["dataset"],
size: 5,
});
console.log(results.data.items);Geospatial catalogue and bounded vector data
The v0.8.0 geospatial namespaces are experimental and anonymous:
const metadata = await norway.geodata.searchDatasets({
query: "verneområder",
limit: 5,
});
const protectedAreas = await norway.environment.searchProtectedAreas({
boundingBox: { south: 61.45, west: 8.25, north: 61.55, east: 8.45 },
limit: 100,
pageSize: 50,
maxPages: 3,
});
const land = await norway.land.getLandResourcesAt({
latitude: 61.494,
longitude: 8.349,
limit: 25,
});
console.log(metadata.data.items);
console.log(protectedAreas.data.features);
console.log(protectedAreas.data.pagination.truncated);
console.log(land.data.features);Caller points and boxes are WGS84; GeoJSON coordinates are [longitude, latitude]. Naturbase and
NIBIO request server-side WGS84 conversion and retain their advertised sourceCrs. Unsupported CRS
metadata is rejected rather than guessed, and there is no client-side reprojection engine. Check
pagination.truncated before treating a feature result as complete.
geodata discovers catalogue metadata, including WFS, WMS, OGC API Features, ArcGIS REST and
download links, but never follows those links. Arbitrary upstream URLs, WMS image rendering,
restricted Geovekst data and nationwide unbounded downloads are outside the SDK.
SSB Klass classifications and historical codes
Klass is SSB's official system for classifications and codelists. The curated klass namespace
keeps it separate from statistical table values:
import { klassClassificationIds } from "norway-open-data-sdk";
const browse = await norway.klass.listClassifications({
language: "en",
page: 0,
pageSize: 10,
});
console.log(browse.data.items);
// Classification 6 is Standard Industrial Classification; version 3218 is SIC 2025.
const industries = await norway.klass.listCodes({
versionId: 3218,
language: "en",
page: 0,
pageSize: 20,
});
// Klass supports code patterns, not server-side code-name search.
const occupations = await norway.klass.searchCodes({
classificationId: klassClassificationIds.occupations, // 7
codePattern: "25*",
date: "2026-01-01",
language: "en",
pageSize: 10,
});
const oslo = await norway.klass.getCode({
classificationId: klassClassificationIds.municipalities, // 131
code: "0301",
date: "2026-01-01",
language: "en",
});Classification IDs are stable exported constants. Version IDs identify dated snapshots: discover
available versions with listVersions() instead of assuming that a documented version remains
current. getCode() also accepts { versionId, code } when an exact snapshot is required. The
documented and tested classifications include municipalities 131, counties 104, industry 6,
occupations 7, education/NUS 36, and country codes 91, 100 and 552.
Historical administrative resolution follows official code-change edges and returns every branch:
const historical = await norway.klass.resolveMunicipalityCode({
code: "0101",
sourceDate: "2019-01-01",
targetDate: "2020-01-01",
language: "en",
});
console.log(historical.data.status); // "replaced"
console.log(historical.data.matches); // [{ code: "3001", name: "Halden", ... }]
console.log(historical.data.predecessors, historical.data.successors);
console.log(historical.data.changes); // dated official graph edges supporting the result
console.log(historical.data.warnings);unchanged, renamed, replaced, merged, split, ambiguous, not_found and
context_required are explicit outcomes. A one-to-many result is never silently reduced to one
successor. A missing mapping is inconclusive, and administrative correspondence does not prove
that statistics are comparable across boundaries or years.
Fixed correspondence tables expose metadata and an SDK-bounded page:
const table = await norway.klass.getCorrespondenceTable({
correspondenceTableId: 2774, // Municipalities 2026 → Municipalities 2024
language: "en",
page: 0,
pageSize: 20,
});
console.log(table.data.sourceVersionName, table.data.targetVersionName, table.data.items);Klass only pages classification browsing/search upstream. Versions, codes, changes and
correspondence maps arrive as complete provider payloads and are paged locally; those results say
pagination.upstreamPaged === false. Page sizes are capped at 100. Norwegian Bokmål (nb),
Norwegian Nynorsk (nn) and English (en) are supported where the selected resource publishes a
translation. All results carry SSB Klass source attribution; optional in-process caching uses a
24-hour provider default.
Cross-provider company profile
const profile = await norway.profiles.company("923609016");
console.log(profile.data.location);Cross-provider address profile
One call answers a location from four providers at once:
const place = await norway.profiles.address("Haraldsgata 100, Haugesund");
console.log(place.data.address.municipalityName); // Kartverket
console.log(place.data.weather?.temperature); // MET Norway
console.log(place.data.hazards); // Exact administrative-area matches from NVE
console.log(place.data.hazardMatches); // Code/name evidence for each match
console.log(place.data.roads); // First-page NVDB bounding-box candidates
console.log(place.data.roadSearch); // Bounds, page size and truncation state
// components is an array with one entry per SDK operation:
for (const component of place.data.components) {
console.log(component.operation, component.status);
// "addresses.search" "available"
// "hazards.getFloodWarnings" "available"
// "weather.current" "omitted" (reason: "not-configured")
}Enrichment degrades gracefully: weather and roads are omitted when the client has no
applicationName/contactEmail, rather than failing the whole call. Optional provider failures
degrade the same way: if MET, NVDB or one Varsom warning feed errors at request time, the profile
still returns with every surviving section, and the failing operation is reported as omitted with
reason provider-error and a sanitized error name and message. Caller cancellation is never
degraded — aborting the request still rejects the whole call. components reports each operation
as available, with its source, retrievedAt and cached values, or omitted, with a
not-configured, missing-coordinate, not-applicable or provider-error reason. A component's retrievedAt is
when that SDK operation resolved, including cache hits, not when its payload was originally fetched
upstream. Sources include provider attribution text, with service-specific wording for each Varsom
warning feed.
roads contains only the first provider page intersecting the WGS84 box in roadSearch; it is not
a geometry-distance result. The default box extends approximately 250 metres from the address to
each side. Check roadSearch.truncated before assuming that all candidates were returned.
Safety: automatic warning discovery checks an explicit municipality by exact code, then exact case-insensitive, Unicode-normalized name. It considers a county only when the warning publishes no municipalities, because a county can be parent context. Forecast-region names are not matched automatically. An empty match is never an all-clear. For any safety decision, consult the current, complete official warnings directly from Varsom/NVE and follow their guidance.
Cross-provider municipality profile
One call answers a municipality from four agencies at once:
const kommune = await norway.profiles.municipality("Haugesund"); // code "1106" also works
console.log(kommune.data.municipality); // { code, name, countyCode } — SSB region register
console.log(kommune.data.population); // SSB-aggregated residents for the two newest years
console.log(kommune.data.lifeExpectancy); // FHI life expectancy, suppression flags preserved
console.log(kommune.data.companies?.registered); // Brønnøysundregistrene organization count
console.log(kommune.data.hazards); // Exact NVE warning matches for the municipality
console.log(kommune.data.components); // Status and source for every operationThe lookup accepts a four-digit municipality code or an exact municipality name; counties and the
whole-country region never resolve, and a duplicated municipality name (Herøy exists twice) requires
SSB's county-qualified label. Population totals are summed by the SDK from SSB's per-sex, per-age
rows — the aggregation is the SDK's, not an SSB-published figure. Life expectancy keeps FHI's
suppression flag with years: null for the smallest municipalities. Every optional section degrades
to a provider-error component if its provider fails, and the hazards safety boundary above
applies here too.
The profile resolves against the current SSB statistics region dimension and has no date parameter,
so it does not guess what a historical municipality code means. Resolve historical input explicitly
with klass.resolveMunicipalityCode() first. Continue automatically only for an unchanged,
renamed or one-to-one replaced result with exactly one match. A merge, split or ambiguous result
requires an application decision; the SDK never combines successor statistics.
Cross-provider nature profile
const nature = await norway.profiles.natureAtLocation({
latitude: 61.494,
longitude: 8.349,
limit: 25,
});
console.log(nature.data.municipality); // context inferred from the nearest official place
console.log(nature.data.protectedAreas); // Naturbase
console.log(nature.data.natureTypes); // selected modern NiN localities
console.log(nature.data.interventionFreeAreas); // January 2023 INON status
console.log(nature.data.landResources); // NIBIO generalized AR50
console.log(nature.data.pagination);
console.log(nature.data.warnings);The five vector lookups and Kartverket place lookup run independently. A provider failure omits only
its section, records a sanitized provider-error component and adds a warning; caller cancellation
still rejects the call. Every successful component keeps its real provider, attribution,
retrievedAt and cache state. The top-level source is a synthetic descriptor for the composed
nature profile, while sources is derived from successful components and preserves their real
provider attribution and licence terms. Distinct descriptors from the same provider remain distinct
when their licence or required attribution differs. The per-dataset limit defaults to 100 and is
capped at 100.
Weather warnings, nowcast and sun times
const warnings = await norway.weather.getWarnings({
latitude: 60.3913,
longitude: 5.3221,
language: "en",
});
for (const warning of warnings.data.warnings) {
console.log(warning.awarenessLevel.color, warning.event, warning.area, warning.expires);
}
const nowcast = await norway.weather.getNowcast({ latitude: 59.9139, longitude: 10.7522 });
console.log(nowcast.data?.radarCoverage); // "ok" unless the radar is degraded
console.log(nowcast.data?.entries.map((entry) => entry.precipitationRate)); // mm/h, 5-minute steps
const sun = await norway.weather.getSunTimes({
latitude: 69.6492,
longitude: 18.9553,
date: "2026-12-21",
});
console.log(sun.data.polarCondition); // "polar-night": no sunrise, explained rather than absentWith a coordinate, MetAlerts itself decides which warning areas contain the point. An empty list
is never an all-clear, and MET Norway's warnings cover weather only: flood, landslide and avalanche
warnings are NVE's and come from hazards. A point outside a product's coverage resolves to
undefined — the nowcast outside the Nordic radar area (which excludes Svalbard), the ocean
forecast inland or offshore, and the experimental air-quality forecast outside Norway. Sun and moon
times use the Europe/Oslo offset in force on the requested date.
Tides and water levels
const table = await norway.tides.getTideTable({
latitude: 60.398,
longitude: 5.32,
from: "2026-10-15T00:00:00Z",
to: "2026-10-17T00:00:00Z",
});
console.log(table.data?.events); // [{ time, value, kind: "high" | "low" }, ...] in cm
const levels = await norway.tides.getWaterLevels({
latitude: 60.398,
longitude: 5.32,
from: "2026-10-15T00:00:00Z",
to: "2026-10-16T00:00:00Z",
referenceLevel: "mean-sea-level",
});
console.log(levels.data?.location); // reference station, factor, delay and surge gauge
for (const series of levels.data?.series ?? []) {
console.log(series.type, series.unit, series.values.length); // observation, prediction, ...
}Every value is a height above the stated reference level — chart datum unless you choose mean sea
level or NN2000 — so always read referenceLevel with it. Away from a permanent gauge, Kartverket
models values from a reference station, and location says which one, with what amplitude factor
and time delay, and which gauge supplied the weather effect. Inland and far-offshore positions
resolve to undefined. A request may span 7 days at 10-minute resolution, 31 days hourly, or 366
days for a tide table; times are in UTC.
Historical weather and climate normals
climate reads MET Norway's Frost archive and needs a free client ID from
Frost:
const withFrost = new NorwayOpenData({
credentials: { "met-frost": { clientId: process.env.MET_FROST_CLIENT_ID! } },
});
const stations = await withFrost.climate.searchStations({
near: { latitude: 59.9139, longitude: 10.7522, maxCount: 1 },
activeOnly: true,
});
const station = stations.data[0];
if (station !== undefined) {
const january = await withFrost.climate.getObservations({
stationIds: [station.id],
elements: ["mean(air_temperature P1D)", "sum(precipitation_amount P1D)"],
from: "2026-01-01T00:00:00Z",
to: "2026-02-01T00:00:00Z",
timeResolutions: ["P1D"],
});
const normals = await withFrost.climate.getClimateNormals({
stationIds: [station.id],
elements: ["mean(air_temperature P1M)"],
period: "1991/2020",
});
console.log(station.name, station.distanceKm, january.data.length, normals.data);
}A window may span 31 days, or 366 days when every requested resolution is daily or coarser. A query Frost holds no data for resolves to an empty array: Frost answers the same way for "no data" and "no such station". Frost's station-holder names are not included. The namespace is experimental until its schemas are verified against the live service with a client ID.
Cross-provider conditions profile
const conditions = await norway.profiles.conditionsAtLocation({
latitude: 60.398,
longitude: 5.32,
hours: 6,
});
console.log(conditions.data.municipality); // the municipality whose boundary contains the point
console.log(conditions.data.forecast); // MET Norway, the next 6 hours
console.log(conditions.data.weatherWarnings); // MET Norway warnings covering the point
console.log(conditions.data.hazards); // NVE warnings matched to the municipality
console.log(conditions.data.tides?.events); // predicted high and low waters
console.log(conditions.data.observations); // nearest Frost station, when configured
console.log(conditions.data.components); // why every section is present or absentThe profile runs 14 operations. Each one records its provider and retrieval time, or why it is
absent: not-configured (MET Norway identification or a Frost client ID is missing),
not-covered (the point is outside that product's coverage), not-applicable (a prerequisite,
such as the municipality, is missing), not-found or provider-error. A missing section never
removes another, and caller cancellation still rejects the whole call. NVE warnings are requested
only once the municipality is known, and a distant weather station or degraded radar produces an
explicit notice in warnings. Neither warning list is ever an all-clear. Experimental.
Health statistics with suppression flags
FHI publishes statistics from Norwegian health registers — cause-of-death rates, abortion statistics, drug sales and municipal public-health indicators — through one open API:
const sources = await norway.health.getSources(); // daar, abr, nokkel, ...
const result = await norway.health.query({
source: "daar",
tableId: 754,
selections: {
DAAR: ["2020"],
KJONN: ["Total"],
HJERTEKAR: ["Total"],
MEASURE_TYPE: ["RATE_NO"],
},
});
console.log(result.data.rows[0]?.value); // age-standardized cardiovascular death rateDiscover what a table accepts with health.getTables(source), health.getTableMetadata(source,
tableId) and health.getTableDimensions(source, tableId); a selection of ["*"] selects every
category of a dimension, including nested child categories.
Small-count health cells are suppressed at the source — anonymized or not computable. The SDK
preserves FHI's markers instead of hiding them: a suppressed row is { value: null, flag: ":" },
and result.data.flags maps every symbol to the provider's own explanation. A flag is
information, not absence: keep flagged observations suppressed in anything you build on top.
Electricity spot prices
This namespace uses the third-party Hva koster strømmen? API. The provider documents the values as ENTSO-E electricity prices in EUR converted to NOK with a Norges Bank exchange rate.
const prices = await norway.electricity.getPrices({ area: "NO1" });
console.log(prices.data[0]);
const now = await norway.electricity.getCurrentPrice({ area: "NO5" });
console.log(now.data?.nokPerKwh);getPrices() returns one entry per elapsed hour in the requested Europe/Oslo calendar day: normally
24, but 23 or 25 across daylight-saving transitions. Normalized endsAt values follow the next
chronological startsAt (or the following local midnight). Use { includeRaw: true } when you also
need the provider-native timestamps.
Paging through large result sets
List endpoints expose auto-paginating async iterators that request each page on demand:
for await (const company of norway.companies.searchAll({ municipalityCode: "1106" })) {
console.log(company.name);
}Bound the walk with maxItems or maxPages:
const iterator = norway.catalog.searchAll({ query: "transport" }, { maxItems: 50 });maxItems must be a non-negative integer. maxPages must be an integer from 1 to 100 and defaults
to 100. NVDB iterators treat continuation markers as opaque and throw ResponseValidationError
before requesting an already-seen marker when a provider repeats a cursor or returns a cycle.
Available on companies.searchAll, catalog.searchAll, parliament.searchCasesAll,
roads.searchRoadObjectsAll, roads.getRoadNetworkAll, fisheries.searchVesselsAll and
fisheries.searchAquacultureSitesAll, plus geodata.searchDatasetsAll and
geodata.searchServicesAll.
The four environment.iterate* methods and land.iterateLandResources use spatial query bounds
instead: limit, pageSize, maxPages and offset. They detect repeated non-empty page contents
as well as cursor cycles, so an ArcGIS/WFS service that ignores its requested offset cannot create an
endless walk. All fourteen iterators are lazy and propagate the caller's AbortSignal.
Neither Fiskeridirektoratet register reports a total count in its response body, so their iterators stop at the first page that comes back short or empty rather than at a known last page.
AIS vessel positions and tracks
const norway = new NorwayOpenData({
credentials: {
"barentswatch-ais": {
clientId: process.env.BARENTSWATCH_AIS_CLIENT_ID!,
clientSecret: process.env.BARENTSWATCH_AIS_CLIENT_SECRET!,
},
},
});
// Every position BarentsWatch recorded for one vessel in the last 24 hours.
const track = await norway.ais.getTrackLast24Hours("257111020");
console.log(track.data.points.length, track.data.from, track.data.to);
// Position and identity together, in one request.
const vessel = await norway.ais.getVesselSnapshot("257111020");
console.log(vessel.data?.name, vessel.data?.callSign);
// A point-in-time snapshot of everything transmitting in a window.
const snapshot = await norway.ais.getLatestPositions({
boundingBox: { south: 58.8, west: 4.5, north: 60.0, east: 6.5 },
});An empty track or an absent snapshot is a normal answer, not an error, and it is not evidence that a vessel does not exist. See AIS coverage limitations.
ais.getCoverageArea() returns the sea area BarentsWatch actually publishes, so you can tell "no
data for this vessel" apart from "this location is outside the feed".
Live AIS streaming and cancellation
ais.streamPositions() returns an AsyncIterable, so a live feed is an ordinary for await loop.
The connection opens on the first iteration, not when the iterator is created.
const controller = new AbortController();
for await (const position of norway.ais.streamPositions({
boundingBox: { south: 58.8, west: 4.5, north: 60.0, east: 6.5 },
signal: controller.signal,
})) {
console.log(position.mmsi, position.latitude, position.longitude);
}Three ways to stop, all of which close the connection:
// 1. Leave the loop. `break`, `return` and `throw` all release the stream.
for await (const position of norway.ais.streamPositions({ boundingBox })) {
if (position.mmsi === target) break;
}
// 2. Abort the signal, from anywhere. The loop then throws ProviderError.
setTimeout(() => controller.abort(), 60_000);
// 3. Stop the iterator explicitly, when you are not using `for await`.
const stream = norway.ais.streamPositions({ boundingBox });
await stream.next();
await stream.return(undefined);streamMessages() yields every selected AIS message kind rather than positions only, as a union
discriminated by kind:
for await (const message of norway.ais.streamMessages({
includeStatic: true,
includeWeather: true,
signal: controller.signal,
})) {
if (message.kind === "static") console.log(message.name, message.callSign);
if (message.kind === "weather") console.log(message.significantWaveHeight);
}Streaming behaviour worth knowing before you rely on it:
- There is no automatic reconnection. When the provider closes the stream, the loop ends normally. Whether reopening is safe depends on whether your application can tolerate the gap, so that decision is left to you.
- Every record is validated independently. An invalid or schema-breaking record raises
ResponseValidationErrorand stops the stream; records are never silently skipped. - Backpressure is preserved. A chunk is read only when you ask for the next record, so a slow consumer slows the transport rather than filling memory.
downsample: trueasks the provider for at most one message per vessel per minute, which reduces volume substantially in busy areas.
Fishing vessels and aquaculture sites
const norway = new NorwayOpenData(); // Both registers are open.
const vessels = await norway.fisheries.searchVessels({ municipalityCode: "1106" });
const vessel = await norway.fisheries.getVessel({ registrationMark: "R-123-H" });
const sites = await norway.fisheries.searchAquacultureSites({ countyCode: "11" });
const site = await norway.fisheries.getAquacultureSite("10029");getVessel accepts the register's own id, a registrationMark, or a radioCallSign. The last two
are searches upstream, so they raise NotFoundError both when nothing matches and when more than one
vessel does — an ambiguous mark never silently resolves to whichever record came first.
A registration mark is accepted in the register's own spelling (R 0062H) or the hyphenated form
(R-62-H). An aquaculture site's capacity is only meaningful alongside capacityUnitType; TN
means tonnes of maximum allowed biomass.
Owner details are published in full for company owners. For a private individual the entry is kept as
{ entityType: "person" } so the ownership structure and owner count stay visible, but the name,
postal code and town are not carried into SDK output — including { includeRaw: true }. See
privacy considerations.
Cross-provider vessel profile
const profile = await norway.profiles.vessel({ mmsi: "257111020" });
console.log(profile.data.ais.status); // "available" | "no-recent-data"
console.log(profile.data.ais.identity?.callSign);
console.log(profile.data.registration?.name); // Fiskeridirektoratet, when it matches
console.log(profile.data.nearestPlace?.name); // Kartverket
console.log(profile.data.weather?.temperature); // MET Norway, when identifiedThe profile asks BarentsWatch for the vessel's latest AIS position and identity and for its recorded track, then uses the radio call sign AIS reported to look the vessel up in the fishing-vessel register, and adds conditions and the nearest official place name at that position.
The call sign is the join key because it is the one identifier both sources publish: AIS is keyed by MMSI, which the register does not hold, and the register is keyed by its own id and registration mark, which AIS does not transmit. When AIS reports no call sign the register lookup is skipped rather than attempted by name, which is not unique and would produce confident wrong matches.
Every section reports why it is present or absent, so absence is never ambiguous:
| components[].reason | Meaning |
| --------------------- | ------------------------------------------------------------------------- |
| not-covered | The provider answered and publishes nothing for this subject |
| not-found | The provider was searched and holds no matching record |
| not-applicable | A prerequisite from an earlier component was absent, so nothing was asked |
| not-configured | The provider needs credentials this client does not have |
| missing-coordinate | No position was available, so location-based sections could not run |
| provider-error | The provider was asked and failed; error carries a sanitized summary |
A partial profile is the normal outcome. One provider failing degrades its own section and leaves
every successful section intact. ais.status of no-recent-data never means the MMSI is unassigned.
Because AIS is the only source that can resolve an MMSI at all, profiles.vessel raises
ConfigurationError without BarentsWatch AIS credentials rather than returning a profile whose every
section failed.
Request cancellation
const controller = new AbortController();
await norway.weather.forecast(
{
latitude: 59.4138,
longitude: 5.268,
},
{
signal: controller.signal,
},
);Raw response access
const rate = await norway.currency.getExchangeRate(
{
from: "EUR",
to: "NOK",
},
{
includeRaw: true,
},
);
console.log(rate.raw);Note that the normalized result names the pair with SDMX terminology: the from currency is
data.baseCurrency and the to currency is data.quoteCurrency, alongside date, value and
an optional unit multiplier for currencies quoted per 100 units.
Configuration
import { NorwayOpenData } from "norway-open-data-sdk";
const norway = new NorwayOpenData({
applicationName: "my-company-my-application",
contactEmail: "[email protected]",
timeoutMs: 10_000,
retries: 2,
cache: { enabled: true, maxEntries: 250 },
fetch: globalThis.fetch,
credentials: {
nve: { apiKey: process.env.NVE_HYDAPI_KEY! },
"barentswatch-ais": {
clientId: process.env.BARENTSWATCH_AIS_CLIENT_ID!,
clientSecret: process.env.BARENTSWATCH_AIS_CLIENT_SECRET!,
},
barentswatch: {
clientId: process.env.BARENTSWATCH_CLIENT_ID!,
clientSecret: process.env.BARENTSWATCH_CLIENT_SECRET!,
},
"met-frost": { clientId: process.env.MET_FROST_CLIENT_ID! },
},
});| Option | Default | Purpose |
| ---------------------------------------------- | ------------------ | ---------------------------------------------------- |
| applicationName | None | Caller identity required by Entur, MET and NVDB |
| contactEmail | None | Contact address required by MET |
| timeoutMs | 10_000 | Per-attempt timeout in milliseconds |
| retries | 2 | Retry attempts after the initial request; range 0–10 |
| fetch | globalThis.fetch | Custom Fetch-compatible implementation |
| cache.enabled | false | Enables the response cache |
| cache.ttlMs | Provider default | Overrides provider-specific TTLs |
| cache.maxEntries | 100 | Maximum entries for the built-in in-memory cache |
| cache.store | In-memory | Custom CacheStore for sharing across instances |
| rateLimit.enabled | true | Enforces each provider's declared request budget |
| credentials.nve.apiKey | None | Free NVE HydAPI key for stations and observations |
| credentials["barentswatch-ais"].clientId | None | BarentsWatch AIS-client id, for the ais scope |
| credentials["barentswatch-ais"].clientSecret | None | BarentsWatch AIS-client secret |
| credentials.barentswatch.clientId | None | BarentsWatch API-client id, for the api scope |
| credentials.barentswatch.clientSecret | None | BarentsWatch API-client secret |
| credentials["met-frost"].clientId | None | Free Frost client ID, for climate |
When required by the selected service, each application must provide its own identity, contact
address and credentials. The SDK contains no shared email, API key or fallback identity. Missing
required values raise ConfigurationError before a request is made.
Every provider method accepts optional signal, includeRaw and bypassCache request options.
Environment variables
The SDK reads no environment variables itself; these are the names used consistently by the examples,
the live probes and the documentation.
Geonorge, Naturbase, NIBIO, profiles.natureAtLocation, tides and places.getMunicipalityAt are
anonymous and add no variables.
| Variable | Used by | Required for |
| ----------------------------------- | --------------------------------- | ---------------------------------------------- |
| NORWAY_OPEN_DATA_APPLICATION_NAME | applicationName | Entur, MET Norway, NVDB |
| NORWAY_OPEN_DATA_CONTACT_EMAIL | contactEmail | MET Norway |
| NVE_HYDAPI_KEY | credentials.nve.apiKey | NVE HydAPI stations and observations |
| BARENTSWATCH_AIS_CLIENT_ID | credentials["barentswatch-ais"] | norway.ais, profiles.vessel |
| BARENTSWATCH_AIS_CLIENT_SECRET | credentials["barentswatch-ais"] | norway.ais, profiles.vessel |
| BARENTSWATCH_CLIENT_ID | credentials.barentswatch | norway.marine |
| BARENTSWATCH_CLIENT_SECRET | credentials.barentswatch | norway.marine |
| MET_FROST_CLIENT_ID | credentials["met-frost"] | norway.climate, observations in a conditions |
| RUN_LIVE_TESTS | Test runner | Enabling the opt-in live probes |
Frost takes its client ID as the HTTP Basic username on its own API requests rather than through an OAuth2 token exchange. Like every other credential, it is sent only to that provider's host and is redacted from errors.
OAuth2 credentials
Providers that use the OAuth2 client-credentials grant are configured with a clientId and
clientSecret under their own provider id. The SDK then handles the tokens:
- A token is acquired on the first request that needs it, never at construction time.
- Tokens are cached in memory per SDK instance and reused across requests.
- A token is refreshed before its stated expiry, with a minute of headroom for clock skew, so a caller does not see an avoidable HTTP 401.
- Concurrent requests share one token request rather than each starting their own.
- A token the provider rejects with HTTP 401 is discarded, so the next call acquires a fresh one instead of replaying a revoked credential.
- One caller aborting its request does not cancel a token request the other callers are waiting on.
- Credentials are provider-scoped: a secret configured for one provider is only ever sent to that provider's own token endpoint.
Tokens are never written to the response cache or to any persistent store, and there is no hosted proxy or token broker. Client secrets, client ids and access tokens are redacted from errors and are never written to the console; the SDK logs nothing.
Runtime support
The SDK uses only web-standard APIs, so one build runs everywhere. CI executes the built package on Node.js 22 and 24, Deno and Bun on every change.
| Runtime | Status | | --------------------------------------- | ---------------------------------------------------------- | | Node.js 22+ | Verified in CI | | Deno 2 | Verified in CI | | Bun | Verified in CI | | Cloudflare Workers and other edge hosts | Supported; the same ESM build is used | | Browsers | Supported where the provider sends permissive CORS headers |
A host runtime must provide:
- a spec-compliant
fetch,Response,HeadersandAbortController structuredClone, used by the response cache- a full-ICU
Intl, used for Europe/Oslo dates and Norwegian locale casing
The package imports no Node built-in and reads no process, Buffer, __dirname or __filename;
pnpm check:portability enforces that against the built output on every verify run. Pass your own
fetch through the fetch option when a runtime needs a custom implementation.
Browser use is bounded by each provider's CORS policy rather than by the SDK. Providers requiring
identification also expect it in a User-Agent, which browsers do not allow applications to set, so
server-side or edge use remains the better fit for those namespaces.
Response format
Every successful operation returns OpenDataResponse<T>:
type OpenDataSource = {
id: string;
name: string;
homepage: string;
documentation: string;
license?: string;
attribution?: string;
};
type OpenDataResponse<T> = {
data: T;
source: OpenDataSource;
sources?: OpenDataSource[];
retrievedAt: string;
cached: boolean;
raw?: unknown;
};data is the typed result; on direct provider calls, source identifies that provider and includes
its licence or attribution when declared. Cross-provider profiles retain a synthetic top-level
source that identifies the SDK composition rather than misidentifying the complete result as one
provider's data. sources lists the distinct real provider descriptors derived from successful
profile components, including separate entries when one provider's feeds have different licence or
required attribution text; data.components retains per-operation provenance. retrievedAt is an
ISO 8601 timestamp; cached reports a memory-cache hit; and raw is included only when requested.
Raw payloads remain runtime-validated and may be allowlisted or sanitized.
The established company, address, municipality and vessel profiles keep their existing synthetic top-level source identities for backward compatibility. The new nature profile follows the same envelope convention. See the API-stability policy.
Error handling
import {
NorwayOpenData,
NotFoundError,
OpenDataError,
ProviderError,
RateLimitError,
} from "norway-open-data-sdk";
const norway = new NorwayOpenData();
try {
await norway.companies.get("000000000");
} catch (error) {
if (error instanceof NotFoundError) {
console.error("Organization not found");
} else if (error instanceof RateLimitError) {
console.error("Retry after seconds:", error.retryAfter);
} else if (error instanceof OpenDataError) {
console.error(error.provider, error.statusCode, error.message);
} else {
throw error;
}
}Exported errors are OpenDataError, ConfigurationError, InputValidationError,
NotFoundError, RateLimitError, ProviderError, RequestTimeoutError and
ResponseValidationError. Retryable provider responses, timeouts and temporary network failures
use bounded retries; validation and other client errors are not retried.
Retry timing
Two separate delays apply, and the SDK never retries sooner than a provider permitted:
- No
Retry-After. The SDK uses its own exponential backoff with full jitter, capped at five seconds per attempt. - A
Retry-Afterof up to one minute. The SDK waits the stated duration in full. That cap on computed backoff does not shorten it. - A
Retry-Afterlonger than one minute. The SDK does not retry at all and does not wait first. It raises immediately, keeping the normal status-to-error mapping:RateLimitErrorfor HTTP 429, and the usualProviderErrorfor a retryable 5xx. Both carryretryAfterin seconds, so you can schedule the work yourself instead of being blocked.
One minute is the longest sliding window any provider budget uses, so it is the longest pause the
SDK's own rate limiter can already impose. Cancelling the caller's signal rejects a waiting retry
immediately.
retryAfter is the stable signal here, not the error class. A 503 that asks you to wait is a
provider outage, not rate limiting, so it stays a ProviderError — branch on retryAfter when you
want "the provider told me how long to wait" regardless of status:
if (error instanceof OpenDataError && error.retryAfter !== undefined) {
scheduleRetryIn(error.retryAfter); // seconds; set for both 429 and 5xx
}Errors expose name, provider, statusCode, retryAfter and cause. There is no code or
retryable property.
Errors across ESM and CommonJS builds
The package ships both an ESM and a CommonJS build. JavaScript class identity is per-bundle, so if a
single process loads both builds — for example your application imports the ESM build while a
dependency requires the CommonJS one — an error thrown by one build will fail instanceof against
the class imported from the other. This is a property of dual-published packages, not a bug you can
configure away.
Prefer one module system throughout the dependency graph, and avoid loading both builds in the same
process. Where an error may cross that boundary, branch on stable data instead of class identity —
every SDK error carries name (for example "RateLimitError"), and provider, statusCode and
retryAfter where they apply:
if (error instanceof OpenDataError || (error as { name?: string }).name === "RateLimitError") {
// Works whichever build constructed the error.
}Caller cancellation also surfaces as ProviderError, with the abort reason attached as cause.
When retry or reporting logic must distinguish a deliberate abort from a provider failure, check
the caller's own signal rather than the error:
try {
await norway.companies.get("923609016", { signal: controller.signal });
} catch (error) {
if (controller.signal.aborted) {
// Cancelled by this application — do not retry or alert.
} else if (error instanceof ProviderError) {
// Genuine provider failure.
}
}Caching
const norway = new NorwayOpenData({
cache: { enabled: true, maxEntries: 250 },
});
const first = await norway.companies.get("923609016");
console.log(first.cached); // false
const second = await norway.companies.get("923609016");
console.log(second.cached); // true
await norway.clearCache();
const afterClear = await norway.companies.get("923609016");
console.log(afterClear.cached); // falseCaching is disabled by default. When enabled, the SDK uses a bounded in-memory TTL/LRU cache with
provider-specific TTLs. Failures are never cached, and { bypassCache: true } skips both reads and
writes.
Sharing a cache between instances
The default cache lives inside one NorwayOpenData instance, so a second instance — another
worker, another process — starts cold. Supply a CacheStore to share responses. Every method may
be synchronous or asynchronous.
import type { CacheStore } from "norway-open-data-sdk";
const store: CacheStore = {
async get(key) {
const raw = await redis.get(key);
return raw === null ? undefined : JSON.parse(raw);
},
async set(key, value, ttlMs) {
await redis.set(key, JSON.stringify(value), { PX: ttlMs });
},
async clear() {
await redis.flushDb();
},
};
const norway = new NorwayOpenData({ cache: { enabled: true, store } });The store receives values that have already passed runtime validation and is expected to return
them unchanged. It owns expiry and eviction, so cache.maxEntries no longer applies. clearCache()
resolves once the store has cleared, and with a shared store it affects every reader.
Request budgets
Each provider declares how often the SDK may call it, and that budget is enforced by default across
every client sharing one NorwayOpenData instance. A request that would exceed the budget waits its
turn rather than being rejected, and waiting never counts against timeoutMs. Cached responses cost
no budget, and cancelling a caller's signal rejects a queued request immediately.
import { providers } from "norway-open-data-sdk";
console.log(providers.ssb.rateLimit?.default);
// { requests: 30, intervalMs: 60_000, basis: "provider-documented", note: "..." }
console.log(providers["ssb-klass"].rateLimit?.default);
// { requests: 30, intervalMs: 60_000, basis: "sdk-courtesy", note: "..." }basis distinguishes a limit the provider publishes from a conservative budget the SDK chose for a
provider that publishes none. SSB documents the first number for PxWeb; it publishes no numeric
Klass-specific limit, so the second is an SDK courtesy policy and must not be described as official.
Budgets are named per operation class, because a provider can publish very different limits for different services. Data.norge allows 10 search requests per minute but 5 resource lookups per second, so those get separate budgets and neither throttles the other:
console.log(providers["data-norge"].rateLimit?.default.requests); // 10 per minute (search)
console.log(providers["data-norge"].rateLimit?.resource.requests); // 5 per second (lookups)Set rateLimit: { enabled: false } only when your traffic is already bounded elsewhere, such as
behind your own scheduler or a shared gateway.
See Architecture for implementation details.
Documentation
flowchart LR
App[Developer application]
Facade[NorwayOpenData facade]
Adapters[Provider adapters]
Core[HTTP, validation, retry and cache]
APIs[Public-sector APIs and documented third-party API]
App --> Facade
Facade --> Adapters
Adapters --> Core
Core --> APIsThe facade creates all service namespaces with one shared configuration and cache.
Provider adapters own request construction, runtime schemas and safe normalization.
The shared core handles timeout, cancellation, retries, errors and caching.
Provider-specific semantics are preserved instead of forced into one universal model.
Hosted API reference — every exported class, method and type
Generate the TypeDoc API reference locally with:
pnpm run docsTypeDoc writes to docs/api; open docs/api/index.html. The same reference is published at
https://iamkm1.github.io/Norway-Open-Data/ and is redeployed on demand through the Pages workflow.
Testing
pnpm test
pnpm test:coverageLive tests are opt-in representative contract probes against the supported public-sector APIs and the third-party electricity endpoint. The Geonorge, Naturbase and NIBIO probes are anonymous, bounded, and use the same opt-in switch:
NORWAY_OPEN_DATA_APPLICATION_NAME=my-application \
[email protected] \
pnpm test:livePowerShell:
$env:NORWAY_OPEN_DATA_APPLICATION_NAME = "my-application"
$env:NORWAY_OPEN_DATA_CONTACT_EMAIL = "[email protected]"
pnpm test:liveSee Testing for live-test coverage, smoke tests and CI behavior.
Contributing
Development requires Node.js 22+ and pnpm 10:
pnpm install
pnpm verifyRead CONTRIBUTING.md before opening a pull request. User-visible changes require a Changeset:
pnpm changesetLicence
The SDK source code is available under the MIT Licence. MIT does not automatically apply to returned data: every provider or dataset retains its own terms, licence, traffic limits and attribution requirements. Users must follow those terms when using or redistributing data.
Hva koster strømmen? is an independent third-party service. Its API page describes ENTSO-E as the electricity-data source and Norges Bank as the exchange-rate source; see PROVIDERS.md for the documented lineage and attribution notes.
Norway Open Data SDK is an independent open-source project. It is not affiliated with, sponsored by or endorsed by Norwegian public authorities.
Personal and restricted data are outside the project scope.
Known limitations
- The SDK needs a spec-compliant
fetchand a full-ICUIntl; runtimes built without full ICU cannot format the Europe/Oslo values the electricity, hazard and profile paths rely on. - Browser use is limited by each provider's CORS policy, which the SDK cannot change.
- Upstream API contracts and response shapes can change independently of the SDK.
- Some providers require caller identification, a contact email or a free API key.
- Address-profile warning matches use exact structured administrative areas but still never constitute an all-clear; use the complete official Varsom/NVE services for safety decisions.
- Address-profile roads are first-page bounding-box candidates, not a circular distance query;
inspect
roadSearchfor the exact bounds and truncation state. - The electricity namespace depends on a third-party derived API rather than an official government endpoint.
- The optional cache is in-process by default; sharing or persisting it across instances requires
supplying your own
CacheStore. - Klass does not provide upstream paging for versions, codes, changes or correspondence maps. SDK paging bounds returned output but cannot reduce the complete provider payload received first.
- Klass has no official code-name search endpoint.
searchCodes()supports the provider's exact, wildcard and range code patterns only. - Klass correspondence may be one-to-one, many-to-one or one-to-many, supplies no weights in the discovered resources, and may be absent. Absence is not proof of equivalence or non-equivalence, and administrative mapping is not evidence of statistical comparability.
- The v0.8
geodata,environment,landandprofiles.natureAtLocationsurfaces are experimental. Upstream catalogue labels and ArcGIS/WFS schemas can change independently. - Geospatial support is vector-only and curated: no WMS image rendering, arbitrary URL proxy, reprojection engine, restricted Geovekst/AR5 data or unbounded full-country export.
- The v0.9
climatenamespace,weather.getAirQualityForecastandprofiles.conditionsAtLocationare experimental; see Weather, tide and climate limitations. - Protected endpoints, personal data, write operations and delegated authentication are not supported.
Weather, tide and climate limitations
- No warning list is an all-clear.
weather.getWarningsreturns what MetAlerts publishes for a query, and the conditions profile's NVE matches cover only warnings whose administrative areas name the municipality. Use yr.no, varsom.no and local authorities for safety decisions. - MET Norway's products have different coverage. The nowcast covers the Nordic radar area, which
excludes Svalbard; the ocean forecast covers Northern and Western European seas but not land; the
air-quality forecast covers Norwegian statistical areas only. Outside coverage the SDK returns
undefined, never invented values. - The air-quality forecast is MET Norway's version 0.1 product: model forecasts, not measurements, and its contract may change. Measured air quality from NILU is not included because its measurement endpoints now require authentication.
- Tide values away from a permanent gauge are modelled from a reference station, as
locationstates, and are always relative to the returnedreferenceLevel. Chart datum and NN2000 are not defined everywhere. - Frost's schemas follow its published Swagger models and have not yet been verified against the
live service; configure
MET_FROST_CLIENT_IDand run the live probes to do so. Frost does not distinguish "no data" from "no such station", so both resolve to an empty array, and the nearest station to a point can be far away or at a very different elevation: readdistanceKmandelevationMetres. - Frost's reference time, time offset and time resolution are passed through exactly as published.
For daily elements the observation time is not always
referenceTime; see Frost's documentation before computing one.
Geospatial limitations
- Geonorge describes datasets and services but does not guarantee that a distribution is open, reachable, current or suitable. The SDK classifies published links and retains publisher licence metadata; it never follows them. Verify the resource's access, licence and attribution before use.
- Naturbase coverage is four selected public vector datasets: current and proposed protected areas, modern selected NiN localities, and January 2023 intervention-free nature status. It is not a complete Naturbase layer catalogue or a current legal determination for a location.
- NIBIO coverage is generalized AR50. Small areas below 15 decares can be merged into surrounding classes; it is not parcel-precision AR5, soil/cultivation suitability or a forest inventory.
- Spatial results are bounded windows. Inspect each
pagination.truncated; an empty result means only that the selected upstr
