Layer400 developers · Cloud API v1
Endpoint reference.
Four read-only GET resources. Each needs its own explicitly approved account permission.
Base URL: https://layer400.com/business/api/v1. Every call uses the Bearer key in its Authorization header. New here? Start with your first request.
At a glance
Base URL: https://layer400.com/business/api/v1. All four data endpoints use GET and Authorization: Bearer <your-key>. The permission applies to the account, not to a separate key for each data layer.
| Method + path | Required scope | Returns |
|---|---|---|
GET /live/rid | rid.live | Current received drone Remote ID positions |
GET /live/adsb | adsb.live | Current received aircraft ADS-B positions |
GET /history/rid | rid.history | Retained drone position observations |
GET /history/adsb | adsb.history | Retained aircraft position observations |
Live approval does not include history, and history approval does not include live. Requests, approval and key management happen in the website’s account and admin interfaces.
Current received drone Remote ID positions
/live/ridRequired permission: rid.live.
Required: bbox. Maximum 30° per side. Optional limit (1–1000, default 250) and next-page cursor.
These examples fetch one page. Follow next_cursor when present; see the integration guide for a complete refresh.
Request examples for rid.live
curl
# Store the key in LAYER400_API_KEY using your secret manager first.
curl --fail-with-body --get 'https://layer400.com/business/api/v1/live/rid' \
--header "Authorization: Bearer $LAYER400_API_KEY" \
--user-agent 'YourOrganisation-YourIntegration/1.0' \
--data-urlencode 'bbox=-2.3,50.7,-2.0,50.9' \
--data-urlencode 'limit=250'Python
# Python 3, standard library. Run on your server.
import json, os, urllib.parse, urllib.request
key = os.environ["LAYER400_API_KEY"]
params = {"bbox": "-2.3,50.7,-2.0,50.9", "limit": 250}
url = "https://layer400.com/business/api/v1/live/rid?" + urllib.parse.urlencode(params)
request = urllib.request.Request(url, headers={
"Authorization": "Bearer " + key,
"User-Agent": "YourOrganisation-YourIntegration/1.0",
})
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if result["next_cursor"] is not None:
print("More live pages exist; repeat with the returned cursor")
print(json.dumps(result, indent=2))Node.js
// Node.js 20+. Save as request.mjs; run: node request.mjs
const key = process.env.LAYER400_API_KEY;
if (!key) throw new Error('Set LAYER400_API_KEY on your server');
const url = new URL('https://layer400.com/business/api/v1/live/rid');
url.searchParams.set('bbox', '-2.3,50.7,-2.0,50.9');
url.searchParams.set('limit', '250');
const response = await fetch(url, {
headers: { Authorization: 'Bearer ' + key,
'User-Agent': 'YourOrganisation-YourIntegration/1.0' },
signal: AbortSignal.timeout(15000),
});
if (!response.ok) {
throw new Error('HTTP ' + response.status + '; request ' +
(response.headers.get('x-request-id') || 'unavailable') +
'; retry-after ' + (response.headers.get('retry-after') || 'not supplied'));
}
const result = await response.json();
if (result.next_cursor !== null) console.log('More pages exist:', result.next_cursor);
console.log(JSON.stringify(result, null, 2));Example live response · fictional
{
"version": "1",
"request_id": "11111111-2222-4333-8444-555555555555",
"generated_at": "2026-10-02T12:00:00Z",
"layer": "rid",
"mode": "live",
"data": [
{
"public_track_id": "L4-D-0123456789ABCDEF",
"observed_at": "2026-10-02T11:59:58Z",
"longitude": -2.15,
"latitude": 50.8,
"altitude_m": 120,
"height_agl_m": 35,
"speed_mps": 6.2,
"heading_deg": 90,
"confidence": "single",
"confidence_score": null,
"receiver_count": 1,
"track_kind": "drone",
"simulated": true,
"test_broadcast": true,
"emergency_state": "unknown",
"stale_after": "2026-10-02T12:00:28Z"
}
],
"next_cursor": null,
"truncated": false
}Current received aircraft ADS-B positions
/live/adsbRequired permission: adsb.live.
Required: bbox. Maximum 30° per side. Optional limit (1–1000, default 250) and next-page cursor.
These examples fetch one page. Follow next_cursor when present; see the integration guide for a complete refresh.
Request examples for adsb.live
curl
# Store the key in LAYER400_API_KEY using your secret manager first.
curl --fail-with-body --get 'https://layer400.com/business/api/v1/live/adsb' \
--header "Authorization: Bearer $LAYER400_API_KEY" \
--user-agent 'YourOrganisation-YourIntegration/1.0' \
--data-urlencode 'bbox=-2.3,50.7,-2.0,50.9' \
--data-urlencode 'limit=250'Python
# Python 3, standard library. Run on your server.
import json, os, urllib.parse, urllib.request
key = os.environ["LAYER400_API_KEY"]
params = {"bbox": "-2.3,50.7,-2.0,50.9", "limit": 250}
url = "https://layer400.com/business/api/v1/live/adsb?" + urllib.parse.urlencode(params)
request = urllib.request.Request(url, headers={
"Authorization": "Bearer " + key,
"User-Agent": "YourOrganisation-YourIntegration/1.0",
})
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if result["next_cursor"] is not None:
print("More live pages exist; repeat with the returned cursor")
print(json.dumps(result, indent=2))Node.js
// Node.js 20+. Save as request.mjs; run: node request.mjs
const key = process.env.LAYER400_API_KEY;
if (!key) throw new Error('Set LAYER400_API_KEY on your server');
const url = new URL('https://layer400.com/business/api/v1/live/adsb');
url.searchParams.set('bbox', '-2.3,50.7,-2.0,50.9');
url.searchParams.set('limit', '250');
const response = await fetch(url, {
headers: { Authorization: 'Bearer ' + key,
'User-Agent': 'YourOrganisation-YourIntegration/1.0' },
signal: AbortSignal.timeout(15000),
});
if (!response.ok) {
throw new Error('HTTP ' + response.status + '; request ' +
(response.headers.get('x-request-id') || 'unavailable') +
'; retry-after ' + (response.headers.get('retry-after') || 'not supplied'));
}
const result = await response.json();
if (result.next_cursor !== null) console.log('More pages exist:', result.next_cursor);
console.log(JSON.stringify(result, null, 2));Example live response · fictional
{
"version": "1",
"request_id": "11111111-2222-4333-8444-555555555555",
"generated_at": "2026-10-02T12:00:00Z",
"layer": "adsb",
"mode": "live",
"data": [
{
"public_track_id": "L4-A-0123456789ABCDEF",
"observed_at": "2026-10-02T11:59:58Z",
"longitude": -2.15,
"latitude": 50.8,
"altitude_m": 2400,
"height_agl_m": null,
"speed_mps": 105,
"heading_deg": 90,
"confidence": "single",
"confidence_score": null,
"receiver_count": 1,
"track_kind": "adsb_aircraft",
"simulated": true,
"test_broadcast": false,
"emergency_state": "unknown",
"stale_after": "2026-10-02T12:00:28Z"
}
],
"next_cursor": null,
"truncated": false
}Retained drone position observations
/history/ridRequired permission: rid.history.
Required: bbox, start and end. Maximum 5° per side and one hour per window, within your approved lookback (at most 30 days). No cursor; split truncated windows.
Both examples below calculate a recent ten-minute window and check truncation. No dates need uncommenting or manual replacement.
Request examples for rid.history
Python
# Python 3, standard library. Run on your server.
import json, os, urllib.parse, urllib.request
from datetime import datetime, timedelta, timezone
key = os.environ["LAYER400_API_KEY"]
params = {"bbox": "-2.3,50.7,-2.0,50.9", "limit": 250}
end = datetime.now(timezone.utc) - timedelta(minutes=1)
start = end - timedelta(minutes=10)
params.update(start=start.isoformat(), end=end.isoformat())
url = "https://layer400.com/business/api/v1/history/rid?" + urllib.parse.urlencode(params)
request = urllib.request.Request(url, headers={
"Authorization": "Bearer " + key,
"User-Agent": "YourOrganisation-YourIntegration/1.0",
})
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if result["truncated"]:
raise RuntimeError("Incomplete history: split the time/area window and query again")
print(json.dumps(result, indent=2))Node.js
// Node.js 20+. Save as request.mjs; run: node request.mjs
const key = process.env.LAYER400_API_KEY;
if (!key) throw new Error('Set LAYER400_API_KEY on your server');
const url = new URL('https://layer400.com/business/api/v1/history/rid');
url.searchParams.set('bbox', '-2.3,50.7,-2.0,50.9');
url.searchParams.set('limit', '250');
const end = new Date(Date.now() - 60_000);
const start = new Date(end.getTime() - 10 * 60_000);
url.searchParams.set('start', start.toISOString());
url.searchParams.set('end', end.toISOString());
const response = await fetch(url, {
headers: { Authorization: 'Bearer ' + key,
'User-Agent': 'YourOrganisation-YourIntegration/1.0' },
signal: AbortSignal.timeout(15000),
});
if (!response.ok) {
throw new Error('HTTP ' + response.status + '; request ' +
(response.headers.get('x-request-id') || 'unavailable') +
'; retry-after ' + (response.headers.get('retry-after') || 'not supplied'));
}
const result = await response.json();
if (result.truncated) throw new Error('Incomplete history: split the time/area window');
console.log(JSON.stringify(result, null, 2));Example history response · fictional
{
"version": "1",
"request_id": "11111111-2222-4333-8444-555555555555",
"generated_at": "2026-10-02T12:00:00Z",
"layer": "rid",
"mode": "history",
"data": [
{
"public_track_id": "L4-D-0123456789ABCDEF",
"observed_at": "2026-10-02T11:59:58Z",
"longitude": -2.15,
"latitude": 50.8,
"altitude_m": 120,
"height_agl_m": 35,
"speed_mps": 6.2,
"heading_deg": 90,
"confidence": "single",
"confidence_score": null,
"receiver_count": 1,
"track_kind": "drone",
"simulated": true,
"test_broadcast": true,
"emergency_state": "unknown"
}
],
"next_cursor": null,
"truncated": false,
"continuation": "Split an overfull time window into smaller half-open [start,end) windows; use smaller bounds if needed."
}Retained aircraft position observations
/history/adsbRequired permission: adsb.history.
Required: bbox, start and end. Maximum 5° per side and one hour per window, within your approved lookback (at most 30 days). No cursor; split truncated windows.
Both examples below calculate a recent ten-minute window and check truncation. No dates need uncommenting or manual replacement.
Request examples for adsb.history
Python
# Python 3, standard library. Run on your server.
import json, os, urllib.parse, urllib.request
from datetime import datetime, timedelta, timezone
key = os.environ["LAYER400_API_KEY"]
params = {"bbox": "-2.3,50.7,-2.0,50.9", "limit": 250}
end = datetime.now(timezone.utc) - timedelta(minutes=1)
start = end - timedelta(minutes=10)
params.update(start=start.isoformat(), end=end.isoformat())
url = "https://layer400.com/business/api/v1/history/adsb?" + urllib.parse.urlencode(params)
request = urllib.request.Request(url, headers={
"Authorization": "Bearer " + key,
"User-Agent": "YourOrganisation-YourIntegration/1.0",
})
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if result["truncated"]:
raise RuntimeError("Incomplete history: split the time/area window and query again")
print(json.dumps(result, indent=2))Node.js
// Node.js 20+. Save as request.mjs; run: node request.mjs
const key = process.env.LAYER400_API_KEY;
if (!key) throw new Error('Set LAYER400_API_KEY on your server');
const url = new URL('https://layer400.com/business/api/v1/history/adsb');
url.searchParams.set('bbox', '-2.3,50.7,-2.0,50.9');
url.searchParams.set('limit', '250');
const end = new Date(Date.now() - 60_000);
const start = new Date(end.getTime() - 10 * 60_000);
url.searchParams.set('start', start.toISOString());
url.searchParams.set('end', end.toISOString());
const response = await fetch(url, {
headers: { Authorization: 'Bearer ' + key,
'User-Agent': 'YourOrganisation-YourIntegration/1.0' },
signal: AbortSignal.timeout(15000),
});
if (!response.ok) {
throw new Error('HTTP ' + response.status + '; request ' +
(response.headers.get('x-request-id') || 'unavailable') +
'; retry-after ' + (response.headers.get('retry-after') || 'not supplied'));
}
const result = await response.json();
if (result.truncated) throw new Error('Incomplete history: split the time/area window');
console.log(JSON.stringify(result, null, 2));Example history response · fictional
{
"version": "1",
"request_id": "11111111-2222-4333-8444-555555555555",
"generated_at": "2026-10-02T12:00:00Z",
"layer": "adsb",
"mode": "history",
"data": [
{
"public_track_id": "L4-A-0123456789ABCDEF",
"observed_at": "2026-10-02T11:59:58Z",
"longitude": -2.15,
"latitude": 50.8,
"altitude_m": 2400,
"height_agl_m": null,
"speed_mps": 105,
"heading_deg": 90,
"confidence": "single",
"confidence_score": null,
"receiver_count": 1,
"track_kind": "adsb_aircraft",
"simulated": true,
"test_broadcast": false,
"emergency_state": "unknown"
}
],
"next_cursor": null,
"truncated": false,
"continuation": "Split an overfull time window into smaller half-open [start,end) windows; use smaller bounds if needed."
}Query parameters.
| Parameter | Rules |
|---|---|
bbox | Required for every request: west,south,east,north, WGS84 decimal degrees. Longitude −180…180; latitude −90…90. East must exceed west and north must exceed south. Live: maximum 30° per side. History: maximum 5° per side. Split areas crossing ±180° into two requests. |
limit | Optional integer, 1–1000; default 250. A limit counts records, not guaranteed unique aircraft across history. |
cursor | Live only. Pass the exact non-null next_cursor from the previous response. Omit it to start a new refresh. |
start / end | Required for history; not for live. RFC3339 timestamps with timezone, preferably UTC Z. End must be after start and not in the future. Maximum one hour per query, within the last 30 days and your shorter approved lookback. Use URL encoding, particularly for a + timezone offset. |
History includes start and excludes end: [start, end). Unknown or repeated parameters are rejected. Keys in query strings are not supported. Keep the encoded query under 2048 characters.
Documentation reviewed 2 October 2026. Coverage and access conditions.