Skip to content
LAYER400DRONE INTELLIGENCE NETWORK
Menu
Documentation menu
On this pageYour integration

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 + pathRequired scopeReturns
GET /live/ridrid.liveCurrent received drone Remote ID positions
GET /live/adsbadsb.liveCurrent received aircraft ADS-B positions
GET /history/ridrid.historyRetained drone position observations
GET /history/adsbadsb.historyRetained 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

GET/live/rid

Required 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

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
# 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
// 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
rid.live JSON response
{
  "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
}

Field reference · Account limits

Current received aircraft ADS-B positions

GET/live/adsb

Required 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

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
# 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
// 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
adsb.live JSON response
{
  "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
}

Field reference · Account limits

Retained drone position observations

GET/history/rid

Required 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
# 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
// 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
rid.history JSON response
{
  "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."
}

Field reference · History completeness

Retained aircraft position observations

GET/history/adsb

Required 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
# 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
// 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
adsb.history JSON response
{
  "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."
}

Field reference · History completeness

Query parameters.

ParameterRules
bboxRequired 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.
limitOptional integer, 1–1000; default 250. A limit counts records, not guaranteed unique aircraft across history.
cursorLive only. Pass the exact non-null next_cursor from the previous response. Omit it to start a new refresh.
start / endRequired 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.