Quick Start
Get up and running in 3 steps:
1. Get an API key
Sign up at portal.napspan.com to get a free API key (14-day trial, 1,000 requests/day).
2. Make your first request
curl -H "X-API-Key: YOUR_API_KEY" \ "https://api.napspan.com/api/v1/events?jurisdiction=DE&limit=5"
3. Parse the response
{
"data": [
{
"id": "de-evt-a100-001",
"type": "construction",
"sub_type": "roadwork",
"severity": "minor",
"title": "Roadworks on A100 Berlin",
"description": "Lane closure between Spandauer Damm and Kaiserdamm...",
"latitude": 52.5167,
"longitude": 13.2833,
"jurisdiction": "DE",
"start_time": "2026-03-23T07:00:00Z"
}
],
"total": 90,
"limit": 5,
"has_more": true
}OpenAPI Spec
The full machine-readable API spec — covering only the data endpoints (events, features, map, analytics, truck corridor, fuel prices, metadata). Use it to generate clients, import into Postman or Insomnia, or browse the interactive reference.
Authentication
All API requests require an API key. Pass it via the X-API-Key header (recommended) or the api_key query parameter.
curl -H "X-API-Key: sk_live_abc123" https://api.napspan.com/api/v1/events
curl "https://api.napspan.com/api/v1/events?api_key=sk_live_abc123"
Errors & Rate Limits
The API returns standard HTTP status codes. Rate limit headers are included in every response.
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request (missing/invalid parameters) |
| 401 | Missing or invalid API key |
| 403 | Plan limit exceeded (jurisdiction, feature gate, expired trial) |
| 429 | Rate limit exceeded — retry after Retry-After seconds |
| 500 | Server error — please retry or contact support |
Events
Traffic events include incidents, construction, closures, weather alerts, restrictions, and special events across all active jurisdictions.
GET /api/v1/events
| Parameter | Type | Description |
|---|---|---|
| ids | string | Comma-separated event IDs for batch lookup. When present, all other filters are ignored and pagination is disabled — the response always returns the full set in one page, regardless of status. Bounded by the plan's max_batch_size. |
| jurisdiction | string | Filter by ISO country/region code (e.g., DE, FR, AT) required on Free. Querying a primary code auto-expands to all members of its group when one exists. |
| type | string | Filter by event type: incident, construction, closure, weather, special_event, advisory, restriction |
| severity | string | Filter: minor, moderate, major, critical |
| lat, lng, radius_km | number | Radius search (e.g., lat=52.52&lng=13.40&radius_km=50) |
| bbox | string | Bounding box: minLng,minLat,maxLng,maxLat |
| limit | int | Results per page (default 100, max per plan) |
| offset | int | Pagination offset |
curl -H "X-API-Key: $KEY" \ "https://api.napspan.com/api/v1/events?ids=de-evt-a9-001,fr-evt-a1-002,at-evt-a4-015"
# Events near Berlin, DE curl -H "X-API-Key: $KEY" \ "https://api.napspan.com/api/v1/events?jurisdiction=DE&lat=52.52&lng=13.40&radius_km=100"
import requests resp = requests.get("https://api.napspan.com/api/v1/events", params={ "jurisdiction": "DE", "lat": 52.52, "lng": 13.40, "radius_km": 100 }, headers={"X-API-Key": KEY}) events = resp.json()["data"]
const res = await fetch( `https://api.napspan.com/api/v1/events?jurisdiction=DE&lat=52.52&lng=13.40&radius_km=100`, { headers: { "X-API-Key": KEY } } ); const { data, total } = await res.json();
req, _ := http.NewRequest("GET", "https://api.napspan.com/api/v1/events?jurisdiction=DE&lat=52.52&lng=13.40&radius_km=100", nil) req.Header.Set("X-API-Key", key) resp, _ := http.DefaultClient.Do(req)
{
"data": [
{
"id": "de-evt-a100-001",
"source_id": "EVT-2026-04521",
"source": "de",
"jurisdiction": "DE",
"type": "incident",
"severity": "major",
"status": "active",
"title": "Multi-vehicle collision on A100 westbound near Spandauer Damm",
"description": "Two right lanes blocked. Emergency services on scene.",
"affected_roads": ["A100"],
"direction": "westbound",
"lanes_affected": "2 of 4 lanes blocked",
"start_time": "2026-03-29T14:30:00Z",
"end_time": null,
"estimated_end_time": "2026-03-29T17:00:00Z",
"latitude": 52.5167,
"longitude": 13.2833,
"road_class": "motorway",
"last_updated": "2026-03-29T14:45:00Z",
"created_at": "2026-03-29T14:30:00Z"
}
],
"total": 42,
"limit": 100,
"offset": 0,
"has_more": false
}GET /api/v1/events/{id}
Single event by ID. Returns the full event with all metadata fields populated.
{
"id": "de-evt-a100-001",
"source_id": "EVT-2026-04521",
"source": "de",
"jurisdiction": "DE",
"type": "incident",
"severity": "major",
"status": "active",
"title": "Multi-vehicle collision on A100 westbound near Spandauer Damm",
"description": "Two right lanes blocked. Emergency services on scene.",
"location": { "type": "Point", "coordinates": [13.2833, 52.5167] },
"affected_roads": ["A100"],
"direction": "westbound",
"lanes_affected": "2 of 4 lanes blocked",
"start_time": "2026-03-29T14:30:00Z",
"end_time": null,
"estimated_start_time": null,
"estimated_end_time": "2026-03-29T17:00:00Z",
"source_created_at": "2026-03-29T14:28:55Z",
"source_updated_at": "2026-03-29T14:45:00Z",
"latitude": 52.5167,
"longitude": 13.2833,
"road_class": "motorway",
"metadata": { "vehicles_involved": 3 },
"last_updated": "2026-03-29T14:45:00Z",
"created_at": "2026-03-29T14:30:00Z",
"archived_at": null,
"archive_reason": null
}archive_reason is null while the event is active. When an event is archived it carries one of two values:
observed— the upstream feed responded cleanly and the event was no longer in the response, so we retired it. This is the normal case.stale_sweep— the per-poll archival path could not retire the event (upstream responses were empty or errored) and a safety-net sweep eventually forced it to archived after roughlypoll_interval × sweep_factor(default factor 15) with no fresh observation. Treatarchived_aton these rows as a "presumed gone by" timestamp rather than a precise end. A persistentstale_sweepratio on a given source is a signal the upstream feed is unreliable about emitting clearance records.
GET /api/v1/events/geojson Starter+
Same parameters as /events, returns a GeoJSON FeatureCollection. Use it directly with Leaflet, Mapbox, or any GeoJSON-aware tool. Content-Type: application/geo+json.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [13.2833, 52.5167] },
"properties": {
"id": "de-evt-a100-001",
"source": "de",
"jurisdiction": "DE",
"type": "incident",
"severity": "major",
"status": "active",
"title": "Multi-vehicle collision on A100 westbound near Spandauer Damm",
"affected_roads": ["A100"],
"direction": "westbound",
"start_time": "2026-03-29T14:30:00Z"
}
}
]
}Features
Features are non-event data: cameras, weather stations, rest areas, signs, truck parking, EV charging, and more. All types share the same endpoint — specify a single type, or use group to fan out across all types in a category (e.g. group=trucking returns nine truck-related types in one call).
GET /api/v1/features
| Parameter | Type | Description |
|---|---|---|
| type | string | Feature type (see Feature Types). Either type or group is required. |
| group | string | Feature group id (see Feature Groups). Convenience expansion to all types in the group. |
| jurisdiction | string | Filter by jurisdiction code. Querying a primary code auto-expands to all members of its group when one exists. |
| active | bool | true to only return active features |
| lat, lng, radius_km | number | Radius search |
| bbox | string | Bounding box: minLng,minLat,maxLng,maxLat |
| limit, offset | int | Pagination |
Every row carries a has_details boolean. When true, the source publishes richer data than the list row carries — call GET /features/{id}/details (or batch via POST /features/details/batch below) to pull it. When false, the list row already has everything the source exposes for that (source, feature_type) pair.
curl -H "X-API-Key: $KEY" \ "https://api.napspan.com/api/v1/features?type=cameras&jurisdiction=DE&lat=48.14&lng=11.58&radius_km=50"
curl -H "X-API-Key: $KEY" \ "https://api.napspan.com/api/v1/features?group=trucking&jurisdiction=DE"
The shape of properties varies by feature_type. The samples below show the typical fields for the most common types — pick a tab to see each shape.
{
"data": [
{
"id": "de-cam-a8-muc-01",
"source": "de",
"jurisdiction": "DE",
"feature_type": "cameras",
"name": "A8 Munich Ost",
"latitude": 48.1391,
"longitude": 11.6754,
"road_name": "A8",
"direction": "eastbound",
"is_active": true,
"properties": {
"url": "https://verkehr.bayern.de/cameras/a8_muc_ost.jpg",
"video_url": null
},
"last_updated": "2026-03-29T14:50:00Z",
"has_details": true
}
],
"total": 8120, "limit": 100, "offset": 0, "has_more": true
}{
"data": [
{
"id": "at-wx-a4-vie",
"jurisdiction": "AT",
"feature_type": "weather_stations",
"name": "A4 Vienna Weather Station",
"latitude": 48.2082, "longitude": 16.3738,
"road_name": "A4",
"is_active": true,
"properties": {
"temperature": 12.4,
"wind_speed": 18.2,
"wind_direction": "NW",
"precipitation": "light_rain",
"road_surface": "wet",
"humidity": 78,
"visibility": 12.0
},
"last_updated": "2026-03-29T14:45:00Z",
"has_details": false
}
],
"total": 2150, "limit": 100, "offset": 0, "has_more": true
}{
"data": [
{
"id": "fr-dms-a4-paris-512",
"jurisdiction": "FR",
"feature_type": "signs",
"name": "A4 EB DMS at PR 12",
"latitude": 48.8566, "longitude": 2.4500,
"road_name": "A4",
"direction": "eastbound",
"is_active": true,
"properties": {
"message": "ACCIDENT 5 KM / 2 VOIES FERMÉES / RALENTISSEMENTS",
"posted_at": "2026-03-29T14:35:00Z",
"sign_type": "DMS"
},
"last_updated": "2026-03-29T14:50:00Z",
"has_details": true
}
],
"total": 3120, "limit": 100, "offset": 0, "has_more": true
}{
"data": [
{
"id": "nl-ev-amsterdam-a2-12",
"source": "ndw",
"jurisdiction": "NL",
"feature_type": "ev_charging",
"name": "Fastned Amsterdam Zuid",
"latitude": 52.3399, "longitude": 4.8732,
"is_active": true,
"properties": {
"network": "Fastned",
"connector_types": ["CCS2", "CHAdeMO", "Type2"],
"max_kw": 300,
"port_count": 8,
"access": "public",
"pricing": "€0.69/kWh"
},
"last_updated": "2026-03-15T00:00:00Z",
"has_details": true
}
],
"total": 42100, "limit": 100, "offset": 0, "has_more": true
}{
"data": [
{
"id": "de-bridge-a8-iller",
"source": "bast",
"feature_type": "bridge_clearances",
"name": "A8 Iller Viaduct",
"latitude": 48.21, "longitude": 10.05,
"is_active": true,
"properties": {
"min_vertical_clearance_m": 4.50,
"posting_status": "open",
"weight_limit_tons": null,
"year_built": 1972,
"structure_kind": "prestressed concrete",
"bridge_number": "BAST-7811-A8"
},
"last_updated": "2026-01-15T00:00:00Z",
"has_details": false
}
],
"total": 39800, "limit": 100, "offset": 0, "has_more": true
}{
"data": [
{
"id": "de-tp-a7-soltau",
"jurisdiction": "DE",
"feature_type": "truck_parking",
"name": "A7 Truck Parking Soltau-Ost",
"latitude": 52.99, "longitude": 9.83,
"is_active": true,
"properties": {
"capacity": 62,
"available": 12,
"availability_pct": 19,
"amenities": ["restrooms", "vending", "lighting"],
"observed_at": "2026-03-29T14:30:00Z"
},
"last_updated": "2026-03-29T14:50:00Z",
"has_details": false
}
],
"total": 580, "limit": 100, "offset": 0, "has_more": true
}GET /api/v1/features/types
Returns every active feature_type with total and active counts. Cached 2 minutes. Useful for building a UI picker that only shows types you actually have data for.
[
{ "type": "cameras", "count": 8120, "active_count": 7842 },
{ "type": "weather_stations", "count": 2150, "active_count": 2098 },
{ "type": "signs", "count": 3120, "active_count": 2987 },
{ "type": "ev_charging", "count": 42100, "active_count": 42100 },
{ "type": "bridge_clearances", "count": 39800, "active_count": 39800 }
]GET /api/v1/features/groups
Returns the 16-group feature taxonomy with member feature types. Use the id field as the group parameter on /features. Cached for 1 hour. See Feature Groups below for the full table.
[
{
"id": "imagery",
"name": "Imagery",
"description": "Live traffic camera feeds",
"sort_order": 1,
"feature_types": ["cameras"]
},
{
"id": "trucking",
"name": "Trucking & Commercial Vehicles",
"description": "Truck restrictions, routes, parking, inspections",
"sort_order": 8,
"feature_types": [
"truck_restrictions", "weight_restrictions", "bridge_clearances",
"truck_routes", "freight_corridors", "truck_parking",
"truck_rest_areas", "weigh_stations", "inspection_stations"
]
}
]GET /api/v1/features/{id}/details
Lazy-loaded detail for a single feature. Some sources expose only compact fields in the list endpoint; this returns the full properties object plus a detail_available flag and a cache hint (hit / miss / none / poll / error). Use the has_details field on the list response to decide whether the call is worth making.
{
"data": {
"id": "de-cam-a8-muc-01",
"feature_type": "cameras",
"name": "A8 Munich Ost",
"latitude": 48.1391, "longitude": 11.6754,
"properties": {
"url": "https://verkehr.bayern.de/cameras/a8_muc_ost.jpg",
"video_url": "https://verkehr.bayern.de/cameras/a8_muc_ost.m3u8",
"views": [
{ "name": "Eastbound", "url": "https://verkehr.bayern.de/cameras/a8_muc_ost_eb.jpg" },
{ "name": "Westbound", "url": "https://verkehr.bayern.de/cameras/a8_muc_ost_wb.jpg" }
]
}
},
"detail_available": true,
"cache": "miss"
}POST /api/v1/features/details/batch
Resolve detail for many features in one round-trip. Per-id caching is identical to the single-id endpoint (Redis-backed, keyed on the id) — a batch that overlaps with previous calls costs nothing for the overlap. Partial success is preserved: a single failed upstream fetch sets cache: "error" on that entry without failing the whole request; missing ids land in the response with not_found: true. The batch size is bounded by the plan's max_batch_size.
curl -X POST -H "X-API-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{"ids": ["de-cam-a8-muc-01", "de-cam-a9-nbg-04", "missing-id"]}' \ "https://api.napspan.com/api/v1/features/details/batch"
{
"count": 3,
"data": [
{
"id": "de-cam-a8-muc-01",
"data": { "id": "de-cam-a8-muc-01", "name": "A8 Munich Ost", "properties": { "url": "…", "views": ["…"] } },
"detail_available": true,
"cache": "hit"
},
{
"id": "de-cam-a9-nbg-04",
"data": { "id": "de-cam-a9-nbg-04", "name": "A9 Nürnberg Nord" },
"detail_available": true,
"cache": "miss"
},
{
"id": "missing-id",
"detail_available": false,
"cache": "",
"not_found": true
}
]
}Truck Corridor Pro+
Find all truck restrictions along a route. Draws a buffered corridor between two points and returns every bridge clearance, weight restriction, truck route, and restriction that intersects it. Uses PostGIS spatial queries for accurate great-circle buffering.
GET /api/v1/truck/corridor
| Parameter | Type | Description |
|---|---|---|
| from_lat required | number | Origin latitude |
| from_lng required | number | Origin longitude |
| to_lat required | number | Destination latitude |
| to_lng required | number | Destination longitude |
| buffer_km | number | Buffer distance around corridor (default 5, max 50 km) |
| height | number | Vehicle height in meters — filters clearances below this |
| weight | number | Vehicle weight in metric tons — filters weight limits below this |
| jurisdiction | string | Restrict to one jurisdiction |
| limit | int | Max results (default 500, max 2000) |
bridge_clearances, bridges, weight_restrictions, truck_restrictions, truck_routes, freight_corridors, truck_parkingcurl -H "X-API-Key: $KEY" \ "https://api.napspan.com/api/v1/truck/corridor?\ from_lat=48.14&from_lng=11.58&\ to_lat=47.81&to_lng=13.05&\ buffer_km=10"
{
"corridor": {
"from": [48.14, 11.58],
"to": [47.81, 13.05],
"buffer_km": 10,
"distance_km": 142.5
},
"data": [
{
"id": "de-bridge-a8-iller",
"feature_type": "bridge_clearances",
"name": "A8 Iller Viaduct",
"latitude": 48.21,
"longitude": 10.05,
"properties": {
"posting_status": "open",
"year_built": 1972,
"_distance_km": "2.31"
}
},
{
"id": "at-trk-a1-salzburg",
"feature_type": "truck_restrictions",
"name": "Salzburg West",
"properties": {
"restriction": "Section ban for trucks > 7.5 t on weekends",
"_distance_km": "0.45"
}
}
],
"total": 98,
"limit": 500
}GET /api/v1/truck/corridor/geojson Pro+ GeoJSON
Same query, returns GeoJSON FeatureCollection. Use this to render corridor results directly on a Leaflet or Mapbox map.
// Render truck corridor on Leaflet map const url = `https://api.napspan.com/api/v1/truck/corridor/geojson` + `?from_lat=48.14&from_lng=11.58&to_lat=47.81&to_lng=13.05&buffer_km=10`; fetch(url, { headers: { "X-API-Key": KEY } }) .then(r => r.json()) .then(geojson => { L.geoJSON(geojson, { pointToLayer: (f, ll) => L.circleMarker(ll, { radius: 6 }), onEachFeature: (f, layer) => { layer.bindPopup(`<b>${f.properties.name}</b><br>${f.properties.feature_type}`); } }).addTo(map); });
import requests resp = requests.get("https://api.napspan.com/api/v1/truck/corridor", params={ "from_lat": 48.14, "from_lng": 11.58, "to_lat": 47.81, "to_lng": 13.05, "buffer_km": 10 }, headers={"X-API-Key": KEY}) for feat in resp.json()["data"]: if feat["feature_type"] == "bridge_clearances": print(f"Bridge: {feat['name']} ({feat['properties'].get('posting_status', 'unknown')})")
{
"type": "FeatureCollection",
"corridor": {
"from": [48.14, 11.58],
"to": [47.81, 13.05],
"buffer_km": 10,
"distance_km": 142.5
},
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [10.05, 48.21] },
"properties": {
"id": "de-bridge-a8-iller",
"feature_type": "bridge_clearances",
"name": "A8 Iller Viaduct",
"min_vertical_clearance_m": 4.50,
"posting_status": "open",
"_distance_km": "2.31"
}
}
]
}Analytics Pro+
Historical analytics and trends across events, features, and source health. All endpoints require Pro or Enterprise plan.
GET /api/v1/analytics/history/{id}
Lifecycle timeline for a single event — created, severity changes, lane changes, end-time changes, archived.
[
{ "id": 1, "event_id": "de-evt-a100-001",
"change_type": "created",
"old_value": null, "new_value": "active",
"changed_at": "2026-03-29T14:30:00Z" },
{ "id": 2, "event_id": "de-evt-a100-001",
"change_type": "severity_change",
"old_value": "moderate", "new_value": "major",
"changed_at": "2026-03-29T14:35:00Z" }
]GET /api/v1/analytics/changes
Recent change feed across all events. Paginated. Useful for syncing a downstream copy of event state without polling each event individually.
{
"data": [
{ "id": 45, "event_id": "de-evt-a100-001",
"change_type": "severity_change",
"old_value": "moderate", "new_value": "major",
"jurisdiction": "DE",
"event_title": "Multi-vehicle collision on A100 westbound",
"changed_at": "2026-03-29T14:35:00Z" }
],
"total": 128, "limit": 100, "offset": 0, "has_more": true
}GET /api/v1/analytics/clearance
P50 / P95 / average clearance times by jurisdiction and event type. Computed from start_time → archived_at.
[
{ "jurisdiction": "DE", "event_type": "incident",
"total_events": 320,
"p50_minutes": 42.5, "p95_minutes": 175.0, "avg_minutes": 59.4 },
{ "jurisdiction": "AT", "event_type": "construction",
"total_events": 150,
"p50_minutes": 4320.0, "p95_minutes": 21600.0, "avg_minutes": 7200.0 }
]GET /api/v1/analytics/corridors
Per-road event frequency and average clearance time. Best signal for "which corridor is unreliable today."
[
{ "road": "A100", "jurisdiction": "DE",
"total_events": 245,
"avg_clearance_minutes": 52.3,
"incident_count": 180, "construction_count": 65 }
]GET /api/v1/analytics/trends
Daily event totals with type breakdown.
[
{ "period": "2026-03-29", "total": 142,
"by_type": { "incident": 58, "construction": 62, "closure": 12, "weather": 10 } },
{ "period": "2026-03-28", "total": 128,
"by_type": { "incident": 45, "construction": 60, "closure": 15, "weather": 8 } }
]GET /api/v1/analytics/hotspots
Geographic clusters of events with the dominant type and road per cluster.
[
{ "cluster_id": 1,
"latitude": 52.5167, "longitude": 13.2833,
"event_count": 28, "radius_km": 2.5,
"top_type": "incident", "top_road": "A100" }
]GET /api/v1/analytics/weather
Historical weather station readings — the data behind the weather-incident correlation features.
[
{ "station_id": "at-wx-a4-vie",
"name": "A4 Vienna Weather Station",
"latitude": 48.2082, "longitude": 16.3738,
"temperature": 12.4, "wind_speed": 18.2, "wind_direction": "NW",
"precipitation": "light_rain", "road_surface": "wet",
"humidity": 78, "visibility": 12.0,
"recorded_at": "2026-03-29T14:45:00Z" }
]Other analytics endpoints
Same shape as the examples above — see the interactive reference for full schemas.
| Endpoint | Description |
|---|---|
| /analytics/feature-history | Feature lifecycle changes (created, deactivated, reactivated). |
| /analytics/event-weather/{id} | Weather snapshot recorded at event creation time (PostGIS nearest-station lookup). |
| /analytics/weather-correlation | Aggregated event-vs-weather correlation rollup. |
Metadata & Status
Lightweight catalogue and status endpoints. /health and /status are public (no API key); the rest are gated by API key but not by plan tier.
GET /api/v1/jurisdictions
Active jurisdictions with code, name, country, and scope. By default only real geographic jurisdictions are returned (scope=state). Pass ?scope=federal for cross-cutting national sources, ?scope=regional for sub-state feeds, or ?scope=all to return everything.
When group_id is non-null the jurisdiction belongs to a group (see /api/v1/jurisdictions/groups below). Querying ?jurisdiction=<primary> on /events or /features auto-expands to all members; query a non-primary code or a comma-list to opt out. The EU deployment currently exposes no groups — all rows return group_id: null — but the field is documented so clients can adopt it once sibling feeds appear.
[
{ "code": "DE", "name": "Germany", "country": "DE", "scope": "state", "group_id": null, "is_active": true },
{ "code": "AT", "name": "Austria", "country": "AT", "scope": "state", "group_id": null, "is_active": true }
]GET /api/v1/jurisdictions/groups
Canonical bundles for jurisdictions whose feed is split across multiple codes. The primary_jurisdiction_code is the one to use on /events and /features — the query is transparently expanded to every members entry. The EU deployment currently returns an empty array; the endpoint is shipped so clients can rely on it once Mobilithek or other national sources need bundling.
[]
GET /api/v1/stats/summary
Rich rollup stats — used by the marketing pages for live counters. Heavier query than /stats; cached 60s.
{
"jurisdictions": { "total": 28, "active": 26, "by_country": { "DE": 1, "AT": 1, "FR": 1 } },
"events": {
"active": 3120,
"by_type": { "incident": 820, "construction": 1450, "closure": 280 },
"by_severity": { "minor": 1080, "moderate": 1450, "major": 510, "critical": 80 }
},
"features": { "total": 96450, "by_type": { "cameras": 8120, "ev_charging": 42100 } },
"last_sync": "2026-03-29T14:55:00Z",
"uptime": "72h15m"
}GET /api/v1/fuel-prices
European fuel prices (where licensed feeds are available). Use latest=true for the most recent observation per jurisdiction + fuel_type.
{
"data": [
{ "jurisdiction": "DE", "country": "DE", "fuel_type": "diesel",
"price": 1.69, "currency": "EUR", "unit": "litre",
"observed_at": "2026-05-05", "source": "BAFA" },
{ "jurisdiction": "FR", "country": "FR", "fuel_type": "diesel",
"price": 1.74, "currency": "EUR", "unit": "litre",
"observed_at": "2026-05-05", "source": "DGEC" }
],
"total": 128, "limit": 100, "offset": 0, "has_more": true
}GET /api/v1/status
Public status snapshot — safe for an unauthenticated status page or external uptime monitor. Aggregate counts plus per-jurisdiction breakdown with 7-day uptime. 60s cache.
{
"overall": "operational",
"generated_at": "2026-05-08T14:00:00Z",
"worker": { "last_seen": "2026-05-08T13:59:50Z", "staleness_seconds": 10, "healthy": true },
"total_resources": 186, "healthy": 175, "warning": 8, "critical": 3,
"jurisdictions": [
{ "code": "DE", "name": "Germany", "country": "DE",
"status": "operational", "total_resources": 12, "open_circuits": 0,
"uptime_7day_percent": 99.8 }
]
}Webhooks Pro+
Subscribe to real-time event changes. When a traffic event is created, changes severity or status, or is archived, the API sends an HMAC-SHA256 signed POST to your endpoint. Pro and Enterprise customers configure subscriptions in their portal — portal.napspan.com/webhooks. The auto-generated secret is shown once on creation; copy it then.
Each webhook is scoped by jurisdiction (mandatory, multi-select — pick one or more), and optionally by event type, sub-type and severity (also multi-select). Filters AND-combine across dimensions; within a dimension, the webhook fires when the event matches any selected value.
The optional sub_type filter narrows within a type — e.g. type: ["incident"] + sub_type: ["accident"] delivers only crashes, not breakdowns or debris. It is additive: omit it (or send an empty array) and you receive every sub-type, exactly as before. sub_type is also a field on every event and a ?sub_type= query param on /events. See the full vocabulary in the API reference.
Event types
| Event | Fires when |
|---|---|
event.created | A new traffic event arrives from a national mobility data source. |
event.severity_change | An existing event's severity changes (e.g. moderate → major). |
event.status_change | Status flips (e.g. active → archived). |
event.archived | The event has ended. |
test | Sent only by the portal "Test" button — fires immediately, never recorded to delivery history. |
Immediate payload (default)
One POST per change. Use this when you need the lowest possible latency and your receiver can handle bursts.
{
"event": "event.severity_change",
"timestamp": "2026-03-29T14:35:00Z",
"data": {
"event_id": "de-evt-a100-001",
"jurisdiction": "DE",
"type": "incident",
"sub_type": "accident",
"severity": "major",
"road_name": "A100",
"changes": {
"old_severity": "moderate",
"new_severity": "major"
}
}
}Batched payload
Set batch_window_seconds (1–300) when creating the webhook to collect changes for that window and deliver them as a single POST. Reduces HTTP volume on chatty receivers (Slack, Teams, Lambda) at the cost of up to window_seconds of added latency.
{
"batch": {
"window_seconds": 60,
"started_at": "2026-03-29T14:30:00Z",
"ended_at": "2026-03-29T14:31:00Z",
"event_count": 2
},
"events": [
{
"event": "event.created",
"timestamp": "2026-03-29T14:30:15Z",
"data": { "event_id": "de-evt-a8-002", "jurisdiction": "DE", "type": "construction", "sub_type": "roadwork", "severity": "minor", "road_name": "A8" }
},
{
"event": "event.severity_change",
"timestamp": "2026-03-29T14:30:42Z",
"data": { "event_id": "de-evt-a100-001", "jurisdiction": "DE", "type": "incident", "sub_type": "accident", "severity": "major", "road_name": "A100", "changes": { "old_severity": "moderate", "new_severity": "major" } }
}
]
}Receivers branch on the presence of the batch key:
if ("batch" in body) { // batched: iterate body.events for (const ev of body.events) handle(ev); } else { // immediate: handle the body itself handle(body); }
A single batch is capped at 100 events; longer queues are split across multiple POSTs in the same window.
Verifying the signature
Every delivery includes an X-Webhook-Signature header containing the lowercase-hex HMAC-SHA256 of the request body, keyed with your webhook secret. Reject requests where the signature doesn't match.
import crypto from "crypto"; app.post("/napspan-webhook", (req, res) => { const sig = req.headers["x-webhook-signature"]; const expected = crypto .createHmac("sha256", process.env.WEBHOOK_SECRET) .update(req.rawBody) // raw bytes, NOT JSON.stringify(req.body) .digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).end(); } // process req.body… res.status(200).end(); });
Reliability
Failed deliveries are retried up to 3 times with exponential backoff (10s, 30s, 90s). After 10 consecutive failures the subscription is auto-disabled — re-enable it from the portal once your endpoint is healthy. Use the Test button on each row in the portal to fire a dummy test event right now and confirm reachability before depending on a webhook in production.
Truck Routing Pro+
A truck-aware route from origin to destination — computed by our routing engine (including ADR tunnel-category restrictions for hazardous loads), then enriched with every nearby hazard our database knows about: live RTTI/SRTI incidents, planned construction, bridge clearances filtered for the requested truck's height, truck and weight restrictions, weather along the corridor, and active alerts — as each NAP publishes the underlying data.
POST /api/v1/routing/route
| Field | Type | Description |
|---|---|---|
| origin required | object | { "lat": ..., "lng": ... } |
| destination required | object | { "lat": ..., "lng": ... } |
| truck required | object | Vehicle block — profile (tractor, straight_truck, van), weight_t, height_m, optional length_m, width_m, axles, hazmat |
| truck.profile_id | string | Reference a saved truck profile from your fleet library (managed in the portal under My Trucks) instead of re-sending the full block. Inline truck.* fields sent alongside it are deep-merged on top (inline wins per field). The resolved block is echoed as resolved_truck. See Reusable truck profiles. |
| alternatives | int | Number of alternate routes (0–3, default 0) |
| avoid | array | Optional: tolls, ferries, tunnels |
| include | array | Optional detail channels. Add tolls to have the routing engine price the route's toll systems for your truck — the cost returns as summary.toll_costs[] (one entry per currency) and sections[].tolls[] (per system). polyline and summary are always included. Toll pricing adds upstream cost, so it is off unless requested. |
| currency | string | ISO 4217 currency for toll fares — only honored with include: ["tolls"]. Omit to get each toll system's native currency. |
| enrichment | object | Tunes the post-routing warnings filter — none of these fields are sent to the routing engine. Subkeys: exclude_types, min_severity, buffer_m (10–1000, default 100), clearance_pad_m (0–1, default 0.15), skip_temporal_filter, max_distance_m. |
| cargo | object | Cargo metadata stored on the saved route. Subkey: hazmat_class (ADR class, e.g. "1.3D"). |
| truck.permit_number | string | Oversize/overweight permit ID. Echoed in the response. |
| customer_route_id | string | Opaque correlation key (≤128 chars). Echoed verbatim. |
| tags | array | Fleet/lane analytics labels (max 16 entries, each ≤64 chars). |
curl -X POST -H "X-API-Key: $KEY" \ -H "Content-Type: application/json" \ https://api.napspan.com/api/v1/routing/route \ -d '{ "origin": { "lat": 52.5200, "lng": 13.4050 }, "destination": { "lat": 48.1351, "lng": 11.5820 }, "truck": { "profile": "tractor", "weight_t": 20, "height_m": 4.0 }, "include": ["tolls"], "currency": "EUR" }'
{
"route_id": "rt_01HXYZ4K8Q2V7R9N3M5P6T8W0E",
"expires_at": "2026-05-18T14:32:00Z",
"routes": [
{
"summary": {
"distance_km": 585.4,
"duration_min": 352,
"toll_costs": [ { "currency": "EUR", "value": 38.70 } ]
},
"geometry": {
"type": "LineString",
"coordinates": [
[13.4050, 52.5200],
[12.3700, 51.3400],
[11.5820, 48.1351]
]
},
"warnings": [
{
"type": "bridge_clearance",
"severity": "critical",
"feature_id": "DE-bridge-A9-K1247",
"name": "A9 overpass near Leipzig",
"latitude": 51.3400,
"longitude": 12.3700,
"clearance_m": 4.0,
"truck_height_m": 4.2,
"message": "Posted vertical clearance is below the requested truck height."
}
]
}
]
}Warning types
Each entry in warnings[] carries a type, a severity (info / warning / critical), the distance along the route, a projected arrival time, and a type-specific properties block. Use the enrichment object to filter by severity or drop types you don't care about. Availability of each type depends on what the NAP publishes for that corridor.
| Type | Typical severity | Fires when |
|---|---|---|
| bridge_clearance | critical | Posted clearance < truck height + clearance_pad_m, or load rating < truck weight |
| truck_restriction | critical | A posted height / width / length / weight limit on the route is below the truck's value (flagged per dimension) |
| weight_restriction | critical | A posted weight limit on the route is below the truck weight |
| traffic_event | critical–info | A live RTTI/SRTI incident, closure or construction event intersects the corridor within the travel window |
| future_construction | warning | Planned construction overlaps the projected travel time at that point on the route |
| special_event | info | A planned closure or special event touches the route |
| alert | info | A general jurisdiction alert applies along the corridor |
| weather | info | Weather conditions / alerts reported by stations near the route |
ADR tunnel-category restrictions are applied by the router itself (via the truck's tunnel category and hazardous-goods class), so prohibited tunnels are avoided in the route geometry rather than reported as a warning. Opt into the separate features[] channel with enrichment.include_features to also get nearby amenities along the route — truck_parking, truck_rest_areas, truck-friendly rest_areas, service_plazas, and ev_charging. The number of feature types you can request at once is plan-gated (Free: none; the cap rises with tier; Enterprise: unlimited) — your cap is published as routing_max_feature_types on GET /api/v1/customer/plans. Exceeding it returns 403 feature_types_limit.
Hours-of-Service planning
Add a truck.hos block — the driver's current Hours-of-Service clock at departure — and the response gains an hos[] channel: the points where the driver must take a break or stop driving under the chosen regime, each with the truck parking and rest areas still reachable before that limit. It covers the EU (Regulation 561/2006), the US (FMCSA), and Canada (south of 60°), and it is free on every plan. Omit ruleset to default to your deployment's region (eu here).
Each *_remaining_s field is the seconds left against that limit — omit one to assume a fresh driver. A reusable truck profile should store only ruleset; send the per-trip counters inline.
"truck": { "profile": "tractor", "weight_t": 20, "height_m": 4.0, "hos": { "ruleset": "eu", "drive_remaining_s": 32400, "since_break_s": 9000 } }
"hos": [ { "reason": "break_required", "distance_along_route_m": 112000, "projected_time": "2026-05-18T13:30:00Z", "legal_deadline": "2026-05-18T13:30:00Z", "feasible": true, "suggested_stops": [ { "type": "truck_parking", "name": "Autohof Montabaur A3", "distance_along_route_m": 106000 } ] } ]
reason is one of break_required, drive_limit, duty_window, or cycle_limit. suggested_stops are ordered closest-to-the-deadline first, so the driver uses the most legal driving time before stopping; feasible: false is a real signal that no legal parking is reachable before that limit. An unknown ruleset returns 400 invalid_hos_ruleset.
Saved routes
Every call is auto-saved for 30 minutes under its route_id. Refetch via GET /api/v1/routing/route/saved/{id} at any point inside that window to get re-evaluated warnings against the current live event/feature state — refetches do not consume routing quota. To keep a route beyond the 30-minute TTL, call POST /api/v1/routing/route/saved/{id}/persist; persisted routes count against your plan's saved-route slot limit.
Reusable truck profiles
If you route the same handful of vehicles over and over, save each truck once instead of re-sending its full block on every call. Create and manage named profiles in your portal under My Trucks, then reference one by its ID with truck.profile_id:
curl -X POST -H "X-API-Key: $KEY" \ -H "Content-Type: application/json" \ https://api.napspan.com/api/v1/routing/route \ -d '{ "origin": { "lat": 48.2082, "lng": 16.3738 }, "destination": { "lat": 48.3069, "lng": 14.2858 }, "truck": { "profile_id": "9f1c2e7a-6b3d-4a21-9f0e-2c5d8b7a1e34", "weight_t": 38 } }'
The server loads the stored profile and deep-merges any inline truck.* fields you send alongside it — inline values win field by field, so you can keep a base profile and override, say, weight_t for one loaded run. The fully-resolved truck block is echoed back as resolved_truck so you can confirm exactly what the routing engine saw:
{
"route_id": "rt_01HXYZ4K8Q2V7R9N3M5P6T8W0E",
"resolved_truck": {
"profile_id": "9f1c2e7a-6b3d-4a21-9f0e-2c5d8b7a1e34",
"profile": "tractor",
"weight_t": 38,
"height_m": 4.0,
"axles": 5
},
"routes": [ /* … */ ]
}An unknown ID returns 400 profile_not_found. A saved profile cannot itself contain a profile_id.
Advanced avoidance & route detail
Beyond avoid, the request accepts finer route controls: avoid_areas (block bbox / circle / polygon no-go zones), avoid_zones (environmental, congestion_pricing), avoid_truck_roads, avoid_countries and the stricter exclude_countries (ISO-3 codes), and details[] for per-span road attributes (speed limits, functional class, lanes, …). Each route also carries the routing engine's own notices[] — flags such as violatedVehicleRestriction when no fully-compliant path exists. See the reference for the complete field list.
Full request/response schema (every truck profile field, every warning type, error envelopes): see the interactive reference at /reference.html.
Feature Groups
57 feature types organized into 16 groups. Pass the id as the group parameter on /features to fan out across all members in a single call.
| Group ID | Name | Members |
|---|---|---|
| imagery | Imagery | cameras |
| weather | Weather & Environment | weather_stations, regional_weather, weather_forecasts, weather_alerts, wind_warnings |
| road_conditions | Road & Surface Conditions | road_conditions, ice_roads, snow_plans |
| traffic_performance | Traffic Performance | traffic_segments, speed_data, travel_times, express_lanes, hov_lanes |
| planned_events | Planned Events & Closures | workzones, future_construction, future_roadwork, special_events, seasonal_loads |
| alerts_advisories | Alerts & Advisories | alerts, emergency_alerts, advisories, general_info |
| wildfires | Wildfires | wildfires, wildfire_incidents, wildfire_perimeters |
| trucking | Trucking & Commercial Vehicles | truck_restrictions, weight_restrictions, bridge_clearances, truck_routes, freight_corridors, truck_parking, truck_rest_areas, weigh_stations, inspection_stations |
| traveler_services | Traveler Services & POIs | rest_areas, service_centres, info_centres, visitor_locations, parks, communities, carpool_lots, airports |
| fuel_charging | Fuel & Charging | ev_charging, alt_fuel_stations |
| borders | Borders & Crossings | border_crossings, ports_of_entry |
| ferries | Ferries | ferries, coastal_ferries |
| transit | Public Transit | transit_hubs, transit_stops |
| tolls | Tolls | tolls |
| static_infrastructure | Static Infrastructure & Enforcement | bridges, signs, roundabouts, speed_cameras |
| operations | Operations | service_vehicles |
Feature Types
Pass any of these as the type parameter to /features.
Event Types
| Type | Severity Range | Description |
|---|---|---|
| incident | moderate–critical | Crashes, disabled vehicles, hazards |
| construction | minor–moderate | Road work, maintenance, paving |
| closure | major–critical | Full road closures |
| weather | moderate–critical | Weather-related road impacts |
| special_event | minor–moderate | Planned events (parades, races) |
| advisory | minor–moderate | Travel advisories, warnings |
| restriction | minor–moderate | Weight, height, speed restrictions |
Plans & Limits
| Limit | Free (14-day trial) | Starter (€29/mo) | Pro (€99/mo) | Enterprise |
|---|---|---|---|---|
| RPM | 60 | 300 | 1,000 | 5,000 |
| Daily requests | 1,000 | 50,000 | 500,000 | Unlimited |
| API keys | 1 | 3 | 10 | 50 |
| Jurisdictions/req | 2 | 10 | Unlimited | Unlimited |
| Results/page | 100 | 500 | 1,000 | Unlimited |
| GeoJSON export | No | Yes | Yes | Yes |
| Analytics | No | No | Yes | Yes |
| Truck corridor | No | No | Yes | Yes |
| Data delay | 15 min | Real-time | Real-time | Real-time |