# Quickstart The Skyfora API gives you Global Navigation Satellite System (GNSS) measurement data and reference station metadata. Use it in your own applications and workflows. This guide creates an API key, finds your stations, reads their latest values, and loads a time series into pandas. Responses are JSON. ## 1. Create an API key Sign in to the [Skyfora Portal](https://portal.skyfora.com) and create an API key under your organization settings. The key is scoped to your organization and grants access to the stations and networks you are entitled to. Include the key in every API request: ``` X-API-Key: your-api-key-here ``` ## 2. Find your stations Every measurement request needs station ids. Get them from [Stations](/api/stations): ```bash curl "https://api.skyfora.com/api/v1/stations?country_code=DE" \ -H "X-API-Key: your-api-key-here" ``` ```json { "stations": [ { "id": "4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "display_name": "BERG001", "resolution_seconds": 30, "country_code": "DE", "latitude": 50.94, "longitude": 6.96 } ], "pagination": { "page": 1, "page_size": 20, "total_items": 1, "total_pages": 1 } } ``` `id` is the value the measurement endpoints take. `resolution_seconds` is the station's observation interval. You can also filter by network, bounding box, or name. ## 3. Get the latest values [Nearest Time](/api/nearest-time) returns one observation per station. Without `time`, it is each station's latest observation: ```bash curl "https://api.skyfora.com/api/v1/measurements/nearest?station_ids=4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11&data_vars=ztd,pwv" \ -H "X-API-Key: your-api-key-here" ``` ```json { "time": "2026-03-20T12:00:10Z", "data": [ { "timestamp": "2026-03-20T11:59:30.000Z", "station_id": "4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "display_name": "BERG001", "ztd": 2353.5, "pwv": 12.3 } ], "count": 1, "missing_station_ids": [] } ``` Add `time` to get the observation nearest to another moment, for example `time=2026-03-01T06:00:00Z`. ## 4. Get a time series [Time Series](/api/time-series) returns every observation in a time range: ```bash curl -X POST https://api.skyfora.com/api/v1/measurements/query \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key-here" \ -d '{ "start_time": "2026-03-20T00:00:00Z", "end_time": "2026-03-21T00:00:00Z", "station_ids": ["4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11"], "data_vars": ["ztd", "ztd_sigma", "pwv"] }' ``` ```json { "data": [ { "timestamp": "2026-03-20T00:00:00.000Z", "station_id": "4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "display_name": "BERG001", "ztd": 2353.5, "ztd_sigma": 4.0, "pwv": 12.3 } ], "count": 1 } ``` For weeks or months of data, add `"interval_seconds": 900` to get one averaged row per station every 15 minutes. ## 5. Load into pandas ```python import pandas as pd import requests resp = requests.post( "https://api.skyfora.com/api/v1/measurements/query", headers={"X-API-Key": "your-api-key-here"}, json={ "start_time": "2026-03-01T00:00:00Z", "end_time": "2026-03-08T00:00:00Z", "station_ids": ["4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "7c2d9e40-1b5f-4c8a-8d3e-6f0a9b4c2d55"], "data_vars": ["ztd", "pwv"], "interval_seconds": 900, }, timeout=60, ) resp.raise_for_status() df = pd.DataFrame(resp.json()["data"]) df["timestamp"] = pd.to_datetime(df["timestamp"]) pwv = df.pivot(index="timestamp", columns="display_name", values="pwv") print(pwv.head()) ``` ## Good to know - **Quality filter.** Responses contain only observations that pass the Skyfora quality checks. See [Data checks and quality control](/api/time-series#data-checks-and-quality-control). - **Access.** You can read only stations in the networks your organization is entitled to. Other stations return `403 Forbidden`. - **Request size.** A time series request is limited in data points: the epochs in the range, summed over the stations. A request over your organization's limit returns `413 Content Too Large`. See [Data points](/api/time-series#data-points). - **Variables.** Units, value formats, and how PWV is derived are in [Variables](/api/reference/variables). - **Errors.** Every status code and what to do about it is in [Errors](/api/reference/errors). ## Insight dashboard The [Skyfora Insight dashboard](https://portal.skyfora.com) plots the same variables as the API, with the same default quality filter. Your organization needs access to the matching parameters. # Changelog ## 24 August 2026 - Station Naming and Metadata Stations now go by one name. Previously, the station responses carried both `mountpoint` and `display_name`. From this release, `display_name` is the station's name across the whole API, and the permanent identifier is the station `id` (UUID). What this means for your integration: - Read `display_name` instead of `mountpoint`. The `mountpoint` field is deprecated and will be removed after the notice period announced by email. - Provide `station_ids` to the measurements endpoints to identify the requested stations. Display names could change in the future without notice. # Stations & Networks Returns metadata for the stations and networks your organization is entitled to. Use it to find the station `id` values that [Time Series](/api/time-series) and [Nearest Time](/api/nearest-time) need. ## Request format ``` GET https://api.skyfora.com/api/v1/stations GET https://api.skyfora.com/api/v1/stations/{station_id} ``` | Header | Required | Value | |--------|----------|-------| | `X-API-Key` | Yes | Your organization API key | ```bash curl "https://api.skyfora.com/api/v1/stations?country_code=FI&page_size=50" \ -H "X-API-Key: your-api-key-here" ``` ```python import requests resp = requests.get( "https://api.skyfora.com/api/v1/stations", headers={"X-API-Key": "your-api-key-here"}, params={"country_code": "FI", "page_size": 50}, ) resp.raise_for_status() stations = resp.json()["stations"] ``` ## Station selection parameters All parameters are optional query parameters on `GET /stations`. | Parameter | Type | Description | |-----------|------|-------------| | `country_code` | string, repeatable | ISO 3166-1 alpha-2 code. Repeat to match any of several countries. Example: `country_code=FI&country_code=SE` | | `network_id` | string, repeatable | Network id. Repeat to match any of several networks. Get ids from `GET /networks`. Example: `network_id=...` | | `search` | string, max 128 | Case-insensitive match on `display_name` and `location_alias`. Example: `search=helsinki` | | `bbox` | 4 numbers | Bounding box in this order: `[latitude_min, longitude_min, latitude_max, longitude_max]`. Example: `bbox=[60.0,24.0,61.0,25.5]` | | `height_vertical_reference` | string | Vertical reference for `height`: `WGS84_ELLIPSOID` (default) or `EGM2008`. Example: `height_vertical_reference=EGM2008` | ## Sorting and pagination | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `sort_by` | string | `name` | One of `name`, `network`, `updated_at`, `data_last_updated`. | | `sort_order` | string | `asc` | `asc` or `desc`. | | `page` | integer | `1` | Page number, starts at 1. | | `page_size` | integer | `20` | Items per page, 1 to 200. | ## Response format ```json { "stations": [ { "id": "4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "display_name": "BTS001", "source_type": "cors", "historic_data_from": "2025-11-05T00:00:00Z", "resolution_seconds": 30, "location_alias": "Helsinki", "country_code": "FI", "latitude": 60.1756, "longitude": 24.9458, "height": 10.0, "height_vertical_reference": "WGS84_ELLIPSOID", "data_last_updated": "2026-03-09T12:00:00Z", "network_id": "0b6c...", "network_code": "...", "created_at": "2025-11-01T00:00:00Z", "updated_at": "2026-03-01T00:00:00Z" } ], "pagination": { "page": 1, "page_size": 20, "total_items": 1, "total_pages": 1 } } ``` `GET /stations/{station_id}` returns one station object with the same fields, and no `pagination`. | Field | Description | |-------|-------------| | `id` | Station id (UUID). Use it in measurement queries. | | `display_name` | Station name, unique across all stations. | | `mountpoint` | Present only when your organization owns the station's network. | | `source_type` | `stream_gnss` (Skyfora StreamGNSS receiver), `telco` (telecom tower receiver), or `cors` (continuously operating reference station). | | `historic_data_from` | Time of the earliest measurement. Can be `null`. | | `resolution_seconds` | Observation epoch interval in seconds. `0` means the native receiver rate. | | `location_alias`, `country_code` | Place name and country. | | `latitude`, `longitude` | Obfuscated position. `null` when your organization is not entitled to location. | | `height`, `height_vertical_reference` | Height in metres, and its vertical reference. | | `data_last_updated` | Time of the latest measurement. Can be `null`. See [Nearest Time](/api/nearest-time). | | `network_id`, `network_code` | The station's network. | ## Networks `GET /networks` returns the networks your organization is entitled to. Use the `id` values in the `network_id` filter. It accepts `page` and `page_size` (1 to 100). ```bash curl https://api.skyfora.com/api/v1/networks \ -H "X-API-Key: your-api-key-here" ``` | Field | Description | |-------|-------------| | `id` | Network id | | `code`, `name` | Network code and name | | `is_owner` | `true` when your organization owns the network | # Time Series Returns GNSS measurements for one or more stations over a time range. Use it to get Zenith Total Delay (ZTD), Precipitable Water Vapour (PWV), and per-satellite data for historical analysis. Get station ids from [Stations](/api/stations) first. ## Request format ```plaintext POST https://api.skyfora.com/api/v1/measurements/query ``` | Header | Required | Value | | -------------- | -------- | ------------------------- | | `X-API-Key` | Yes | Your organization API key | | `Content-Type` | Yes | `application/json` | **curl** ```bash curl -X POST https://api.skyfora.com/api/v1/measurements/query \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "start_time": "2026-03-20T00:00:00Z", "end_time": "2026-03-21T00:00:00Z", "station_ids": ["4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11"], "data_vars": ["ztd", "ztd_sigma"] }' ``` **Python** ```python import requests resp = requests.post( "https://api.skyfora.com/api/v1/measurements/query", headers={"X-API-Key": "your-api-key-here"}, json={ "start_time": "2026-03-20T00:00:00Z", "end_time": "2026-03-21T00:00:00Z", "station_ids": ["4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11"], "data_vars": ["ztd", "ztd_sigma"], }, ) resp.raise_for_status() rows = resp.json()["data"] ``` ## Required parameters | Parameter | Type | Description | Example | | ------------- | --------------- | --------------------------------------------------------------- | ---------------------- | | `start_time` | ISO 8601 string | Start of the time range, inclusive. | `2026-03-20T00:00:00Z` | | `end_time` | ISO 8601 string | End of the time range, exclusive. | `2026-03-21T00:00:00Z` | | `station_ids` | array of UUID | One or more station ids from `GET /stations`. | `["4f9b1c2e-..."]` | | `data_vars` | array of string | Variables to return. See [Variables](/api/reference/variables). | `["ztd", "pwv"]` | ## Optional parameters | Parameter | Type | Default | Description | | ------------------ | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `interval_seconds` | integer | none | Group observations into intervals of this many seconds, at least 30, aligned to UTC (for example `900` for 00:00, 00:15, 00:30). See [Intervals](#intervals). Omit to get every observation. | | `raw` | boolean | `false` | When `true`, the response also includes observations that the default quality filter excludes. Use for diagnostics. | ## Response format The response is JSON: one row per station and epoch, in time order. ```json { "data": [ { "timestamp": "2026-03-20T12:00:00.000Z", "station_id": "4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "display_name": "RAW_RAAS", "ztd": 2353.5, "ztd_sigma": 4.0 } ], "count": 1 } ``` | Field | Description | | --------------------- | ------------------------------------- | | `data[].timestamp` | Observation time (UTC) | | `data[].station_id` | Station id (UUID) | | `data[].display_name` | Station display name | | `data[].` | One field for each requested variable | | `count` | Number of rows in the response | Per-satellite variables come as comma-separated strings in the order of `prn`; see [Variables](/api/reference/variables) for each format. A row with satellite variables: ```json { "timestamp": "2026-03-20T12:00:00.000Z", "station_id": "4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "display_name": "RAW_RAAS", "prn": "E03,E05,G08", "el": "22.3,74.2,9.8", "az": "278.2,255.4,94.4" } ``` ## Intervals With `interval_seconds`, the response has one row per station and interval instead of one per observation. `timestamp` is the start of the interval. | Variable | Value in an interval | | ------------------------- | ------------------------------------------ | | `ztd`, `ztd_sigma`, `pwv` | Mean of the observations in the interval | | `snr`, `el`, `az`, `prn` | From the first observation in the interval | The quality filter applies before the mean. An interval with no observation for a station has no row for it. Intervals make long ranges smaller to download and easier to plot. Use `900` (15 minutes) or `3600` (1 hour) for weeks or months of data. ## Data checks and quality control By default, the response contains only observations that pass the Skyfora quality filter. Set `raw: true` to get all observations, including the excluded ones. Skyfora calculates each observation with precise point positioning (PPP): a station position and tropospheric delay solved from satellite signals and precise satellite orbit and clock corrections. An observation is returned only when that solution is stable and the station tracked enough good signals: | Check | What it detects | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Convergence | The solution has not settled yet. This happens after processing starts or restarts for a station, for example after the correction source changes. The ZTD uncertainty is high until it settles. | | Solution state | No full PPP solution for the epoch. | | Tracking quality | Too few satellites, weak signals, or many measurements rejected as outliers. Radio interference such as jamming, and obstructions around the antenna, cause these. | A gap in the response means the station sent no observations in that period, or none passed these checks. To tell the two apart, repeat the request with `raw: true`. The observation interval is different for each station. Read `resolution_seconds` from `GET /stations` to get the epoch interval of a station. ## Limits All observations in the time range come back in one response. There is no pagination. ### Data points The size of a request is counted in data points: the epochs in the range, summed over the stations. A station's epoch is its observation rate, `resolution_seconds` in [Stations](/api/stations), which differs between networks. When `interval_seconds` is longer than that, each interval counts once. For stations with a 30-second resolution: | Request | Data points | | --------------------------------------------- | ----------- | | 1 station, 1 day, every epoch | 2,880 | | 1 station, 7 days, `interval_seconds: 900` | 672 | | 10 stations, 3 days, `interval_seconds: 3600` | 720 | Each organization has a data-point limit per request. A request over the limit is rejected with `413 Content Too Large` before it runs, and the error message states the limit. Set `interval_seconds`, or split a larger request by time range or by groups of stations and combine the results. A request that runs past the processing deadline is rejected with `408 Request Timeout`. Too many requests at the same time are rejected with `429 Too Many Requests`; retry after the time in the `Retry-After` header. See [Errors](/api/reference/errors) for every status code. # Nearest Time Returns, for each station, the one observation closest to a time you choose. Leave out the time to get each station's latest observation. Use it for maps, dashboards, and "what was the value at 12:00?" lookups. For a range of observations, use [Time Series](/api/time-series). ## Request format ``` GET https://api.skyfora.com/api/v1/measurements/nearest ``` | Header | Required | Value | |--------|----------|-------| | `X-API-Key` | Yes | Your organization API key | ```bash curl "https://api.skyfora.com/api/v1/measurements/nearest?station_ids=4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11,7c2d9e40-1b5f-4c8a-8d3e-6f0a9b4c2d55&data_vars=ztd,pwv&time=2026-03-20T12:00:00Z&within_seconds=900" \ -H "X-API-Key: your-api-key-here" ``` ```python import requests resp = requests.get( "https://api.skyfora.com/api/v1/measurements/nearest", headers={"X-API-Key": "your-api-key-here"}, params={ "station_ids": "all", "data_vars": "ztd,pwv", "within_seconds": 900, }, ) resp.raise_for_status() latest = {row["station_id"]: row for row in resp.json()["data"]} ``` ## Required parameters | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `station_ids` | string | Comma-separated station ids from [Stations](/api/stations), or `all` for every station your organization is entitled to. | `4f9b1c2e-...,7c2d9e40-...` | | `data_vars` | string | Comma-separated variables. See [Variables](/api/reference/variables). | `ztd,pwv` | ## Optional parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `time` | ISO 8601 string | now | The time to look up. When omitted, the response has each station's latest observation. | | `within_seconds` | integer | `3600` | How far from `time` to look, 60 to 86400. With `time`, the window extends this far on both sides. Without it, only before now. | | `raw` | boolean | `false` | When `true`, observations that the default quality filter excludes can also be returned. | ## Response format One row per station that has an observation in the window, with the same fields as a [Time Series](/api/time-series) row. `timestamp` is the time of the observation found, not the requested time. ```json { "time": "2026-03-20T12:00:00Z", "data": [ { "timestamp": "2026-03-20T11:59:30.000Z", "station_id": "4f9b1c2e-6a70-4a3e-9a4f-2d8f5b7c0e11", "display_name": "RAW_RAAS", "ztd": 2353.5, "pwv": 12.3 } ], "count": 1, "missing_station_ids": ["7c2d9e40-1b5f-4c8a-8d3e-6f0a9b4c2d55"] } ``` | Field | Description | |-------|-------------| | `time` | The requested time, or the server time when `time` was omitted | | `data[]` | One row per station with an observation in the window | | `count` | Number of rows in `data` | | `missing_station_ids` | Requested stations with no observation in the window | ## Notes - When two observations are equally close to `time`, the earlier one is returned. - A station in `missing_station_ids` sent no observation, or none that passed the quality filter, in the window. Its `data_last_updated` in [Stations](/api/stations) shows when it last did. - The quality filter applies by default, so the observation returned can be further from `time` than the station's nearest raw observation. Set `raw=true` to include every observation. # Variables Values for the `data_vars` parameter of [Time Series](/api/time-series) and [Nearest Time](/api/nearest-time). | Variable | Description | Unit | JSON value | |----------|-------------|------|------------| | `ztd` | Zenith Total Delay | mm | Number | | `ztd_sigma` | ZTD uncertainty (standard deviation of the estimate) | mm | Number | | `pwv` | Precipitable Water Vapour | mm | Number | | `snr` | Carrier-to-noise density ratio (C/N0) per signal band | dB-Hz | Comma-separated string of 9 band slots: GPS L1, L2, L5; Galileo L1, L2, L5; BeiDou L1, L2, L5. A slot is empty when the band is not tracked | | `el` | Satellite elevation | degrees | Comma-separated string, one value per satellite in the order of `prn` | | `az` | Satellite azimuth | degrees | Comma-separated string, one value per satellite in the order of `prn` | | `prn` | Satellite pseudo-random noise (PRN) identifiers, e.g. `G08`, `E03` | - | Comma-separated string | The API adds `prn` automatically when you request `el`, `az`, or `snr`. ## Satellite identifiers (`prn`) Each GNSS satellite broadcasts its own pseudo-random noise (PRN) code, and the PRN number identifies the satellite within its constellation. `prn` lists the satellites the station tracked at that epoch, as a constellation letter followed by a two-digit number: | Letter | Constellation | Example | |--------|---------------|---------| | `G` | GPS | `G08` | | `E` | Galileo | `E03` | `el` and `az` list one value per satellite in the same order as `prn`, so the third elevation belongs to the third satellite. A station typically tracks 10 to 25 satellites per epoch, so the list length changes from epoch to epoch. ```json { "prn": "E03,E05,G08", "el": "22.3,74.2,9.8", "az": "278.2,255.4,94.4" } ``` Here `G08` is GPS satellite 8, at 9.8 degrees elevation and 94.4 degrees azimuth. `snr` is not per satellite: it has one value per signal band (see the table above). Its BeiDou slots are reserved and currently empty. ## How PWV is derived Skyfora calculates PWV from ZTD. The calculation also uses surface pressure, air temperature at 2 m, and relative humidity. These inputs come from the nearest suitable observation. When no observation is available, they come from a numerical weather prediction (NWP) model. Skyfora adjusts them for the height of the GNSS site. The response does not include these inputs. They are not always measured at the GNSS station. # Errors Errors come back as JSON. The shape depends on the endpoint. [Stations & Networks](/api/stations): ```json { "message": "human-readable message", "code": "...", "details": {} } ``` [Time Series](/api/time-series) and [Nearest Time](/api/nearest-time): ```json { "error": "human-readable message" } ``` | Status | Name | Meaning | What to do | |--------|------|---------|------------| | `400` | Bad Request | Validation error | Check the request against the parameter tables. | | `401` | Unauthorized | Missing or invalid API key | Send a valid key in `X-API-Key`. | | `403` | Forbidden | Not authorized for the requested stations | Request only stations in your entitled networks. | | `404` | Not Found | Station not found (`GET /stations/{station_id}`) | Check the station id. | | `408` | Request Timeout | Request ran past the processing deadline (Time Series) | Use a shorter time range or fewer stations. | | `413` | Content Too Large | Request over your data-point limit (Time Series) | Set `interval_seconds`, use a shorter time range or fewer stations. See [Data points](/api/time-series#data-points). | | `429` | Too Many Requests | Too many requests from your organization at the same time | Retry after the time in the `Retry-After` header. | | `500` | Internal Server Error | Unexpected error on the Skyfora side | Try again. If it continues, contact Skyfora support. | | `503` | Service Unavailable | Service temporarily unavailable | Try again later. |