stratolink
In this section
All docs

API reference

Read fleet records, telemetry, and available forecasts as JSON. Ingestion is a separate authenticated service.

Read telemetry

GET /api/telemetry returns an array of public records. Set resource to devices or telemetry, and provide an explicit comma-separated select list. A successful request with no matches returns [].

List devices:

/api/telemetry?resource=devices&select=device_id,launcher_name,status,launched_at&order=device_id.asc

Read one balloon’s latest 100 packets:

/api/telemetry?resource=telemetry&select=id,device_id,time,lat,lon,altitude_m,pressure,battery_voltage&device_id=eq.stratolink-3&order=time.desc,id.desc&limit=100

Use the same origin as the site. You do not need a database key to call these public read routes. They accept GET only, return JSON, and use Cache-Control: no-store.

ParameterAccepted values
resourcedevices or telemetry
selectExplicit fields from the chosen resource. Wildcards and nested selections are not supported.
device_ideq.my-balloon or in.(balloon-a,balloon-b)
timegte.ISO_TIMESTAMP and/or lte.ISO_TIMESTAMP for telemetry. Repeat the parameter to set both bounds.
lat / lonnot.is.null for telemetry
orderdevice_id.asc for devices; time.asc,id.asc or descending order for telemetry.
limitNon-negative integer, capped at 1,000. Default 1,000.
offsetNon-negative row offset.

Other query parameters return 400. There is no count envelope or cursor token; paginate using a stable order and offset. Fields supported by one resource are not automatically valid on the other.

Time ranges and pagination

This browser example reads a bounded UTC interval. Replace the ID and times with the flight you need. The upper bound limits the query to a fixed window while pages load.

const params = new URLSearchParams({
  resource: 'telemetry',
  select: 'id,device_id,time,lat,lon,pressure',
  device_id: 'eq.stratolink-3',
  order: 'time.asc,id.asc',
  limit: '1000'
});
params.append('time', 'gte.2026-05-17T00:00:00Z');
params.append('time', 'lte.2026-05-19T00:00:00Z');

const rows = [];
for (let offset = 0; ; offset += 1000) {
  params.set('offset', String(offset));
  const response = await fetch('/api/telemetry?' + params);
  if (!response.ok) throw new Error('Telemetry: ' + response.status);
  const page = await response.json();
  if (!Array.isArray(page)) throw new Error('Expected an array');
  rows.push(...page);
  if (page.length < 1000) break;
}

Offset pagination is not a database snapshot. Backfilled or deleted rows can move between pages; deduplicate by id if your analysis needs that protection. For a quick map query, add lat=not.is.null and lon=not.is.null. Omitting those filters preserves packets with useful sensors but no position.

Device fields

FieldMeaning
device_idStable dashboard device identifier. It is not necessarily the DevEUI.
launcher_nameDisplay name associated with the launch.
statusStored status, such as flying, landed, or missing. The dashboard’s recent-reception active filter is a separate calculation.
launched_atRecorded launch timestamp, when available.
planned_launch_dateOptional approximate launch date, as YYYY-MM-DD. This is separate from the actual launch timestamp.
launch_lat / launch_lonPublic launch coordinates in degrees, or null. Privacy rules can coarsen them.
location_approximateAdded by the public response when coordinates are treated approximately.

The read route does not expose account credentials, a registration write operation, or proof of device ownership.

Telemetry fields

FieldsDecoded units and meaning
id, device_idRecord ID and dashboard device identifier.
timeTTN reception timestamp. Not an independent timestamp for each sensor.
lat, lonGPS coordinates in degrees, nullable.
altitude_mReceiver height in meters. Distinct from the pressure altitude computed by the dashboard.
gps_speed, gps_headingGround speed in m/s and heading in degrees clockwise from north.
gps_satellitesSatellites used in the fix.
temperature, pressurePayload temperature in °C and pressure in hPa.
battery_voltage, solar_voltageStorage VSTOR and solar-input voltage in volts.
mems_accel_x, mems_accel_y, mems_accel_zBoard-axis acceleration in m/s², including gravity.
velocity_x, velocity_yDerived horizontal eastward and northward velocity in m/s, when available.
uv_index, ambient_luxInteger UV-index estimate and ambient illumination in lux.
acoustic_eventShort-window sound-threshold flag. Not a sound classification.
rssi, snrGateway received signal strength in dBm and signal-to-noise ratio in dB.
lora_sf, lora_bw, frequency_hzSpreading factor, bandwidth in Hz, and uplink frequency in Hz from network metadata.
gatewaysArray of receiver records, or null. Public fields: gateway_id, lat, lon, alt, rssi, snr; some include location_approximate.
firmware_version, uptime_s, tx_countOptional compatibility fields for firmware version, uptime in seconds, and transmit count. Do not assume every packet format supplies them.
hdop, power_mode, sleep_msOptional dilution-of-precision, power-mode, and sleep-duration fields. Sleep is in milliseconds.
telemetry_versionBinary format: 1 for 35-byte packets; 2 or 3 for 40-byte packets.
power_tierLoad-shedding tier: 0 (full) through 4 (critical).
reset_causeReset code: 0 unknown, 1 watchdog, 2 software, 3 low-power or option reset, 4 cold power-on, 5 warm brownout, 6 reset pin.
boot_countOne-byte boot counter. A change can identify a restart; it is not elapsed time.
gps_fix_age_minMinutes since a fresh accepted GPS fix. Null when none is available this boot.
server_proof_count_mod8Authenticated server/session confirmations modulo 8, supplied by v3 packets.
server_qualified_miss_streakConsecutive qualified confirmation misses, encoded from 0 to 3.
server_recovery_parityCompleted same-region session recoveries modulo 2.
command_ack_seqLast durably applied fPort-10 command sequence, or null.
relay_enabledWhether balloon-to-balloon forwarding is enabled.
relay_fwd_delta, ctt_tags_deltaPacked activity counters: relay forwards from 0 to 7, wildlife tags from 0 to 15. They are not lifetime totals.
location_approximateTrue when coordinates were deliberately coarsened. May be absent on an ordinary telemetry record.

Request these fields by name in select, except location_approximate, which the response adds when applicable. Preserve null; replacing missing GPS, a saturated sensor, or an unavailable channel with zero changes the meaning of the data.

The public API applies location privacy before returning records. Approximate positions can also suppress heading and derived velocity. Receiver coordinates and identifiers receive the same privacy treatment where applicable. The API does not apply every outlier filter used by the rendered flight track.

For acquisition and encoding, see Packets and data flow. For interpreting the charts, see the dashboard guide.

Binary packets and auxiliary channels

Primary telemetry is fPort 1: 35 bytes historically, or 40 bytes for development v2/v3. Multibyte integers are big-endian. The decoder download handles the version markers and unavailable-value sentinels.

fPort 11 wildlife records and fPort 12 balloon-to-balloon records are stored separately. Those auxiliary tables are not part of this public read API. Diagnostic fields are nullable on packet versions that do not supply them. Decoding a B2B record is not the same as authenticating it with the fleet key.

Forecasts

GET /api/forecast?device=stratolink-3 returns the available forecast for one device. It does not return a new measured location. Add &view=path to return only generated_at and nominal_path when drawing a fleet map.

Add &view=history to return only generated_at and the reconstructed track for a balloon that is no longer flying. Successful responses may be served from a shared cache for up to five minutes (Cache-Control: public, s-maxage=300, stale-while-revalidate=3600) and carry an ETag derived from generated_at; a request with a matching If-None-Match header receives 304 Not Modified. Pending, unknown-device and error responses are not cached.

FieldShape and meaning
generated_atForecast generation timestamp.
forecast_origin.time_utcTime of the modeled starting point.
forecast_horizon_hTime span in hours after the origin.
nominal_pathArray of [longitude, latitude] coordinate pairs.
ensembleArray of paths representing model runs.
ellipsesTime slices containing e50.polygon and e90.polygon coordinate arrays. These are model uncertainty regions.
predicted_hindcast.pathModeled path from an earlier fix toward the forecast origin.
observed.reconstructed_pathReconstructed path through earlier GPS gaps. Despite the field name, it includes modeled geometry.
observed.reconstructed_trackTimed reconstruction points with lon, lat, and time_utc.
stale_gps.coverage_limitedWhen true, the model ran out of weather coverage before reaching the intended origin.
divergenceOptional lonlat, spread_km, and threshold_km describing where paths diverged.

Coordinate order is longitude first. Longitudes can extend past ±180° to keep paths continuous across the date line. Empty arrays and absent optional fields are valid. Location privacy also applies to forecast geometry.

Check the HTTP status before interpreting JSON. A 202 response means no stored forecast is available; it is not a complete forecast object. A 404 means the device is unknown or unconnected. A 503 means the forecast service could not complete the read. An unavailable forecast leaves the recorded telemetry intact.

Errors

StatusMeaning
400Unsupported telemetry fields or filters, or an invalid forecast device ID.
404The forecast device is unknown or has no connected radio identity.
405The public endpoint is read-only. Use GET.
502The telemetry data could not be read.
503The stored forecast could not be read.
202No stored forecast is available. The response contains status: "pending" and the device ID.

Error responses contain a message string. The forecast endpoint reads the stored result; it does not start a new calculation. A 202 response is a pending result, not an error or a complete forecast. Do not treat an error object as an empty telemetry array.

TTN webhooks

Complete Connect TTN in the dashboard first. It provides a webhook URL and a separate Authorization header for the verified radio identity.

  1. In the same TTN application and cluster, open Integrations → Webhooks and add a custom webhook.
  2. Choose JSON. Paste the complete Webhook URL from the dashboard into Base URL.
  3. Enable Uplink message and leave its path empty. The copied base URL already ends in /api/ttn-webhook. Leave the other event types disabled.
  4. Leave Filter event data empty so the complete device identity, raw bytes, and reception metadata are forwarded.
  5. Add a header named Authorization. Paste the dashboard’s complete value, including Bearer , into its value field.
  6. Save and send a real payload uplink. Check successful delivery in TTN and Last received under Your balloons.

Copy the webhook credential before closing the result. Stratolink stores its hash, so the credential cannot be shown again. If it is lost or exposed, select Reconnect, verify the device again, then replace the header in TTN. Replacing it immediately invalidates the old credential.

The webhook credential is separate from the TTN API key used for verification, the radio’s AppKey, and your GitHub session. Never substitute one for another.

Packet identity and retries

The receiver matches the authenticated connection, application ID, TTN device ID, and DevEUI to the registered balloon. Regional identities are linked explicitly. A suffix such as -eu is not proof of ownership. TTN webhooks belong to applications, so they can also deliver packets from other devices. Those events are acknowledged without being assigned to your balloon.

Keep the complete TTN payload. Primary telemetry uses fPort 1, wildlife records use fPort 11, and balloon-to-balloon records use fPort 12. Repeat deliveries are acknowledged without inserting the same record again. A received packet can be valid without GPS coordinates.

Response codes
CodeMeaning
200Accepted packet or acknowledged duplicate.
202Authenticated event for another device in the same application. No record stored.
400Invalid identity, event type, port, or payload.
401Missing, revoked, or mismatched connection credentials.
405Use POST for webhook delivery.
413Request exceeds the 256 KiB body limit.
503Ingestion is temporarily unavailable. TTN can retry the same delivery.
The Things Stack custom webhook form with format, base URL and additional headers.
The official Console example. Use the URL and Authorization header from your dashboard, and leave Filter event data empty.

Run your own backend

The website and its Node API handlers live in web. Supabase stores accounts, device identities, and telemetry. The forecast worker writes to private Vercel Blob storage.

cd web
cp .env.example .env.local
ONNXRUNTIME_NODE_INSTALL=skip npm ci
npm run dev

Use Node 22. Set your Supabase URL, public authentication key, server-only database key, Mapbox token, and site origin in .env.local. Configure GitHub sign-in with a redirect back to your site’s /dashboard. Keep server credentials out of public environment variables.

Apply the database schema and migrations for the same source revision. The historical schema.sql alone is insufficient. Follow the migration instructions in the repository before connecting devices.

For Vercel, use web as the project root and dist as the build output. The included configuration serves the static pages and Node APIs. Configure the private forecast store and scheduled worker if you need projected paths.

Run npm run verify before deploying. npm run test:backend also checks the database contracts in a disposable local PostgreSQL container; it requires Docker.

References