Layer400 developers · Cloud API v1
Build a reliable integration.
Keep requests within your approval, check freshness and completeness, and handle failures without retry storms.
Fetch complete windows carefully.
Live: page one refresh at a time
- Request your area without a cursor.
- While
next_cursoris non-null, repeat with the same area and that cursor. Each page is a separate metered request. - De-duplicate by
public_track_idwithin that refresh. Begin the next refresh without a cursor.
Pages are ordered by public reference, but positions can arrive or expire between requests. They are not an immutable snapshot and pagination cannot guarantee every moving track will be seen.
History: split if truncated
Inspect truncated even when the array is empty. It can be true because the underlying mixed-layer scan saturated before the requested layer was selected. Divide [start,end) into [start,mid) and [mid,end) and repeat. If one timestamp remains too dense, reduce the area. Avoid double-counting points at shared spatial boundaries.
History is ordered by observation time and public reference. It has no cursor and is not a packaged flight, a take-off/landing record or a guaranteed complete recording. Do not report a truncated result as complete.
Complete refresh and history examples.
The live example below collects pages within a fixed page budget. Each page consumes allowance. The history examples calculate a recent window and reject incomplete results.
Python · complete live ADS-B refresh
# Python 3; standard library only. Run on your server.
import json, os, urllib.parse, urllib.request
from datetime import datetime, timedelta, timezone
base = "https://layer400.com/business/api/v1"
def get(path, params):
request = urllib.request.Request(
base + path + "?" + urllib.parse.urlencode(params),
headers={"Authorization": "Bearer " + os.environ["LAYER400_API_KEY"],
"User-Agent": "YourOrganisation-YourIntegration/1.0"})
with urllib.request.urlopen(request, timeout=15) as response:
return json.load(response)
# One bounded live refresh. Each page consumes account allowance.
params = {"bbox": "-2.3,50.7,-2.0,50.9", "limit": 250}
tracks = {}
for page_number in range(20):
result = get("/live/adsb", params)
for row in result["data"]:
tracks[row["public_track_id"]] = row
cursor = result.get("next_cursor")
if cursor is None:
break
params["cursor"] = cursor
else:
raise RuntimeError("Page budget reached; refresh incomplete")
print(json.dumps(list(tracks.values()), indent=2))Python · recent ADS-B history
# 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 · recent ADS-B history
// 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));These are starter examples, not a background polling service. They make real metered requests with an approved key; no sandbox endpoint is offered. Set a descriptive User-Agent because an edge rejection may occur before the API receives a request.
Limits, polling and key lifecycle.
My Account shows your approved requests per minute, requests per day, history lookback, grant expiry and usage. There is no universal published polling allowance. All keys belonging to your account share the same quota.
Budget polling as areas × layers × pages × refreshes per minute. For example, two areas × two layers × one page × two refreshes uses eight requests per minute, before retries and history work. Choose an interval within your actual allowance; faster polling does not create new radio reports.
- Minute counters reset at the next minute; daily counters reset at midnight UTC.
- Successful authorisation consumes a request, including empty results, an out-of-lookback history rejection and later query failures. Invalid credentials or denied scopes do not consume the account quota.
X-RateLimit-RemainingandX-DailyLimit-Remainingshow remaining account allowance after authorisation. Concurrent requests can make these immediately stale.- At most five active keys per account. Keys expire no later than the grant and at most 365 days after creation.
- Rotate by creating a replacement, updating your integration and deleting/revoking the old key. Delete key permanently revokes its use; audit evidence is retained.
- Revoked or expired keys, inactive grants and disabled accounts are checked on requests. Updating an application does not expand or reinstate its grant.
Errors and troubleshooting.
| HTTP | API code | What to do |
|---|---|---|
| 400 | invalid_query | Check bounds, timestamps, cursor, duplicates and unknown parameters. |
| 401 | invalid_key | Check the Bearer header, key expiry and revocation in My Account. |
| 403 | scope_denied / history_window_denied | Check approved layer, active grant, grant expiry and permitted lookback. Repeating the same call will not approve access. |
| 404 | not_found | Check the exact resource path. |
| 405 | method_not_allowed | Data endpoints accept GET only. |
| 429 | quota_exceeded | Respect Retry-After; all keys share one account allowance. Edge limits may also apply. |
| 503 | busy / unavailable / query_unavailable / invalid_projection | Use bounded exponential backoff with jitter; reduce dense query windows and contact support if persistent. |
API-handled error shape
{
"error": {
"code": "scope_denied",
"message": "This account is not approved for this layer, or its grant is inactive."
},
"request_id": "11111111-2222-4333-8444-555555555555"
}Check HTTP status and content type before parsing an error: a proxy or edge security rejection may return HTML, without an API request ID. A 403 from the edge is different from JSON scope_denied.
On 429 or 503, honour Retry-After when provided and use exponential backoff with jitter and a maximum number of attempts. Some 503 and edge responses have no retry header. Avoid parallel retry storms. Send support the time, route, status and request ID—never your key.
Coverage, boundaries and compatibility.
The API returns available, privacy-projected received positions. Coverage depends on active receivers, supported broadcasts, terrain and reception conditions. It cannot detect a drone that is not broadcasting a supported signal. A permitted 30-day lookback is a query ceiling, not a promise of 30 days of complete data everywhere.
Version 1 does not expose raw ICAO or Remote ID identifiers, registration/callsign enrichment, owner identity, pilot/controller locations, private receiver coordinates, SAP/SWARM/ARC outputs, alert webhooks, radio control or telemetry uploads. Website features and subscription benefits do not imply an API endpoint.
Commercial use, redistribution, service levels and support arrangements must be agreed separately. This is not a certified collision-avoidance or emergency service. New JSON fields may be added to v1; breaking changes require a new version.
Documentation reviewed 2 October 2026. Coverage and access conditions.