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

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

  1. Request your area without a cursor.
  2. While next_cursor is non-null, repeat with the same area and that cursor. Each page is a separate metered request.
  3. De-duplicate by public_track_id within 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 live pagination
# 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 history request
# 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 history request
// 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-Remaining and X-DailyLimit-Remaining show 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.

HTTPAPI codeWhat to do
400invalid_queryCheck bounds, timestamps, cursor, duplicates and unknown parameters.
401invalid_keyCheck the Bearer header, key expiry and revocation in My Account.
403scope_denied / history_window_deniedCheck approved layer, active grant, grant expiry and permitted lookback. Repeating the same call will not approve access.
404not_foundCheck the exact resource path.
405method_not_allowedData endpoints accept GET only.
429quota_exceededRespect Retry-After; all keys share one account allowance. Edge limits may also apply.
503busy / unavailable / query_unavailable / invalid_projectionUse bounded exponential backoff with jitter; reduce dense query windows and contact support if persistent.

API-handled error shape

Example
{
  "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.