GeoVeil API Documentation
GeoVeil exposes two API versions for access to the GNSS interference data of the ROMPOS system (86 active stations, 83 with spectrum analysis), operated by ANCPI through the National Centre for Cartography (CNC). V2 is the primary API — live data, spoofing analysis and ML prediction. V1 is the classic API with historical perspective and VADASE data.
Overview
The ROMPOS system has 86 active CORS stations, operated by ANCPI through
CNC.
GeoVeil connects to 83 stations with spectrum-analysis capability (NovAtel OEM7),
streaming ITDETECTSTATUSA and SPECTRUMANALYSIS in real time,
processed for interference, spoofing and meaconing detection.
74 stations hold a VADASE licence for positional displacement analysis.
- V2 live stream: ~340,000 events/day (data since 26 May 2026, growing); V1 classic archive: 549,182 records
- 7-indicator spoofing detection engine
- ML prediction: V1 — Classic (LightGBM, ~82 % accuracy, AUC 0.91) currently recommended; V2 — Live retraining on the new dataset
- Real-time updates via async TCP streaming
Authentication
The APIs are public and require no authentication for read operations. Administrative endpoints are not publicly documented and require authentication.
Base URLs
https://api.geoveil.ro/v2
https://api.geoveil.ro/v2/v1
Formats
All responses are application/json. Timestamps are ISO 8601 UTC.
Frequencies in MHz, bandwidths in MHz, power loss in dBm.
GET /i — Interference events (V2)
Returns interference events for a station within a date range.
| Parameter | Type | Description | |
|---|---|---|---|
| station_code | string | req | Station code (e.g. SULI) |
| start_date | date | req | Start date (YYYY-MM-DD) |
| end_date | date | req | End date (YYYY-MM-DD) |
[
{
"id": 12345,
"stationid": "SULI",
"datetime": "2026-05-26T14:23:00Z",
"rfpath": "L1",
"centfreq_mhz": 1575.42,
"bandwidth_mhz": 10.0,
"powerloss_dbm": -42.5,
"hestpowspecdens_dbmhz": -95.3
},
...
]
GET /lh — Last hour (V2)
Returns the raw events from the last hour. Without parameters it returns all stations (potentially large — 100k+ events). With station_code it filters per station; limit caps the row count (recommended when opening a station modal). For aggregate views, prefer /lh/summary.
| Parameter | Type | Description | |
|---|---|---|---|
| station_code | string | opt | Filter per station (e.g. SULI) |
| limit | int | opt | Maximum rows returned (ORDER BY datetime DESC). Recommended: 3000 when station_code is set. |
[
{
"stationid": "SULI",
"datetime": "2026-05-26T12:14:00Z",
"rfpath": "L1",
"centfreq_mhz": 1575.42,
"bandwidth_mhz": 8.0,
"powerloss_dbm": -38.2,
"hestpowspecdens_dbmhz": -92.1
},
...
]
GET /lh/summary — Last-hour summary (V2)
Returns one row per active station with pre-aggregated stats: event count, dominant band, minimum power loss, 6 ten-minute time buckets and the best spoofing analysis. Ideal for the Threats page — returns ~32 rows instead of 100k+ raw events.
None. Returns all stations active during the last hour.
[
{
"station_code": "SULI",
"stationid": 64,
"count": 4823,
"dom_rfpath": "L1",
"min_powerloss": -61.4,
"buckets": [820, 910, 750, 890, 780, 673],
"threat_type": "spoofing_candidate",
"confidence": 0.87,
"severity": "HIGH"
},
...
]
| Field | Description |
|---|---|
| station_code | Station code (e.g. SULI) |
| stationid | Internal numeric ID |
| count | Total events in the last hour |
| dom_rfpath | Dominant RF band (e.g. L1) |
| min_powerloss | Largest power loss (dBm, most negative) |
| buckets | Array of 6 values: event count per 10-minute window (chronological) |
| threat_type | Spoofing threat type (null if no analysis today) |
| confidence | Spoofing analysis confidence score (0–1) |
| severity | HIGH / MEDIUM / LOW / null |
GET /now — Right-now station snapshot (V2)
Returns the last-N-min interference snapshot for one station, with per-event PSD threat level + bandwidth shape, summary aggregates, sweep/chirp pattern detection over a wider context, and best-effort V1 enrichment of overlapping events.
| Parameter | Type | Description | |
|---|---|---|---|
| station_code | string | req | V2 station code (e.g. COST) |
| window | int | opt | Snapshot window in seconds, 1–900 (default 120) |
| context | int | opt | Wider lookback in seconds for sweep detection (window..3600, default 600) |
| include_v1 | 0/1 | opt | Enrich with V1 events that overlap the window (default 1) |
| Level | PSD |
|---|---|
| noise_floor | ≤ -130 |
| info | -130 .. -120 |
| noticeable | -120 .. -110 |
| significant | -110 .. -103 |
| high | -103 .. -98 |
| critical | > -98 |
narrowband ≤ 5 MHz · chirp 5..20 MHz · wideband > 20 MHz
{
"station": {"station_code": "COST", "station_name": "Constanta", "lat": 44.16, "lon": 28.66},
"window": {"from": "...", "to": "...", "seconds": 120, "context_seconds": 600},
"summary": {
"total_events": 47,
"max_threat_level": "high",
"by_threat_level": {"noise_floor": 5, "info": 10, "noticeable": 12, "significant": 14, "high": 6, "critical": 0},
"by_shape": {"narrowband": 41, "chirp": 5, "wideband": 1},
"by_band": {"L1": 8, "L2": 28, "L5": 11},
"peak_psd_dbmhz": -101.3,
"peak_powerloss_dbm": -42.5
},
"pattern": {
"sweep_detected": true,
"events_in_window": 7,
"distinct_minutes": 3,
"cf_span_mhz": 18.24,
"time_span_s": 120.0,
"drift_mhz_per_s": 0.152
},
"events": [ /* per-event with threat_level + shape */ ],
"v1_active": [ /* V1 events overlapping the window */ ],
"v1_included": true
}
GET /ce — Cross-station correlated events (V2)
Detects groups of events with the same frequency and bandwidth across multiple stations within the specified interval (cross-station correlation).
| Parameter | Type | Description | |
|---|---|---|---|
| start_date | date | req | Start date |
| end_date | date | req | End date |
| min_stations | int | opt | Minimum stations (default: 2) |
[
{
"centfreq_mhz": 1575.42,
"bandwidth_mhz": 10.0,
"station_count": 5,
"stations": ["SULI", "BBUC", "DEVA", "TIMI", "ARAD"],
"first_seen": "2026-05-26T12:00:00Z",
"last_seen": "2026-05-26T14:30:00Z"
},
...
]
GET /sa — Spoofing analysis (V2)
Returns the spoofing analysis for a station and interval. The engine scores 7 indicators: temporal, spatial, multi-band, spectral power, cross-station correlation, frequency pattern and temporal drift.
| Parameter | Type | Description | |
|---|---|---|---|
| station_code | string | req | Station code |
| start_date | date | req | Start date |
| end_date | date | req | End date |
| limit | int | opt | Maximum rows returned (default 500, max 5000) |
[
{
"stationid": "SULI",
"datetime": "2026-05-26T14:23:00Z",
"centfreq_mhz": 1575.42,
"threat_type": "spoofing_candidate",
"confidence": 0.87,
"severity": "HIGH",
"temporal_score": 0.9,
"spatial_score": 0.75,
"multiband_score": 0.8,
"psd_score": 0.85,
"correlation_score": 0.7,
"freq_pattern_score": 0.9,
"drift_score": 0.65,
"analysis_notes": "Strong temporal clustering with spatial spread..."
},
...
]
| Value | Description |
|---|---|
| spoofing_candidate | Signal with spoofing characteristics (7 indicators active) |
| matched_spectrum | Spectrum matching a known spoofing source |
| meaconing | Delayed re-broadcast (meaconing) |
| clean | No threat detected |
GET /analytics — Aggregate statistics (V2)
Aggregated statistics: top stations per constellation, frequency distribution, hour×day heatmap, BW/PL scatter, daily trend, weekday vs weekend.
| Parameter | Type | Description | |
|---|---|---|---|
| days | int | opt | Lookback days (default: 30) |
GET /trends — Daily counts + correlation matrix (V2)
Daily event counts + station×station co-occurrence matrix (stations active on the same day).
POST /api/predict — ML prediction (V2)
Generates a 24h interference forecast for a station and a specific hour. Returns probability, hourly forecast, per-RF-band predictions and model info.
| Field | Type | Description | |
|---|---|---|---|
| station_code | string | req | Station code (e.g. "SULI") |
| datetime | string | req | ISO 8601 (e.g. "2026-05-26T14:00") |
POST /api/predict
Content-Type: application/json
{
"station_code": "SULI",
"datetime": "2026-05-26T14:00"
}
{
"station_code": "SULI",
"datetime": "2026-05-26T14:00:00",
"probability": 73.0,
// 0–100 scale (not 0–1)
"expected_events": 45,
"forecast_24h": [
{ "hour": 14, "probability": 73.0, "expected_events": 45 },
{ "hour": 15, "probability": 68.0, "expected_events": 38 },
...
],
"band_predictions": [
{ "rfpath": "L1", "centfreq_mhz": 1575.42, "avg_bw": 8.2, "avg_pl": -42.1,
"avg_psd": -94.3, "occurrences": 2841, "pct": 58.9 },
{ "rfpath": "L2", "centfreq_mhz": 1227.60, "avg_bw": 6.1, "avg_pl": -51.0,
"avg_psd": -97.2, "occurrences": 1204, "pct": 24.9 },
...
],
"model_info": {
"model": "v1",
"classifier_accuracy": 0.823,
"classifier_auc": 0.909,
"classifier_f1": 0.828,
"training_samples": 549182,
"trained_at": "2026-05-26T19:24:00Z"
}
}
GET /api/predict/status
Current state of the LightGBM model: trained or training, accuracy, feature importance.
{
"trained": true,
"is_training": false,
"accuracy": {
"classifier_accuracy": 0.823,
"classifier_auc": 0.909,
"classifier_f1": 0.828,
"regressor_mae": 127,
"regressor_r2": 0.81
},
"training_samples": 549182,
"stations_with_baseline": 83,
"training_time": 480.3,
// seconds
"feature_importance": {
"hour": 0.312,
"station_avg_psd": 0.198,
"dow": 0.154,
"baseline_eph": 0.121,
"hour_sin": 0.089,
...
},
"trained_at": "2026-05-26T19:24:00Z"
}
GET /api/predict/compare
Side-by-side metrics for LightGBM and LSTM, with the winner per metric. If the LSTM is training, the is_training field is true and training_progress exposes the current step (e.g. "Epoch 32/60").
{
"lightgbm": {
"trained": true,
"is_training": false,
"accuracy": {
"classifier_accuracy": 0.823,
"classifier_auc": 0.909,
"classifier_f1": 0.828,
"regressor_mae": 127,
"regressor_r2": 0.81
}
},
"lstm": {
"trained": true,
"is_training": false,
"training_progress": null,
"accuracy": {
"classifier_accuracy": 0.919,
"classifier_auc": 0.969,
"classifier_f1": 0.811,
"regressor_mae": 18.69
}
},
"winner": {
"accuracy": "lightgbm",
"auc": "lightgbm",
"f1": "lightgbm",
"mae": "lstm"
}
}
POST /api/predict/retrain
Forces a retrain of the LightGBM model. The LSTM retraining is chained automatically after completion. Returns 409 if a training run is already in progress.
No body required.
{ "status": "retraining started" }
{ "error": "Training already in progress" }
GET /i — Events (V1 Classic)
Classic endpoint — returns events for a station with legacy field names. Used by the V1 polar chart.
| Parameter | Type | Description | |
|---|---|---|---|
| statie | string | req | Station name (e.g. Sulina) |
| start_date | date | req | Start date (YYYY-MM-DD) |
| end_date | date | req | End date (YYYY-MM-DD) |
[
{
"DataOra": "2026-04-05T14:23:00",
"DataOra_fin": "2026-04-05T14:45:00",
"BandaRF": "L1",
"Frecv_cent_MHz": 1575.42,
"LunBanda_MHz": 10.0,
"Pie_put_est_dBm": -42.5
},
...
]
GET /stats — Systematic statistics (V1)
Aggregated per-RF-band statistics: occurrences, CF min/max, BW max, PL max, time range.
| Parameter | Type | Description | |
|---|---|---|---|
| statie | string | req | Station name |
| start_date | date | req | Start date |
| end_date | date | req | End date |
GET /correlated — Correlated (V1)
Events with the same frequency detected simultaneously across multiple stations (cross-station correlation). Different implementation from V2 /ce (kept for historical reference).
| Parameter | Type | Description | |
|---|---|---|---|
| start_date | date | req | Start date |
| end_date | date | req | End date |
GET /vadase — VADASE data (V1)
VADASE (Variometric Approach for Displacement Analysis Stand-alone Engine) displacement data for detecting GNSS station positional changes. Available for 74 ROMPOS stations with active VADASE licences. Useful for correlating interferences with positional displacements.
| Parameter | Type | Description | |
|---|---|---|---|
| statie | string | req | Station name |
| start_date | date | req | Start date |
| end_date | date | req | End date |
[
{
"DataOra": "2026-04-05T14:00:00",
"East_mm": 2.3,
"North_mm": -1.1,
"Up_mm": 4.7
},
...
]
RF Bands
| Band | Frequency (MHz) | Constellation | Description |
|---|---|---|---|
| L1 | 1575.42 | GPS | Primary GPS band, the most vulnerable to interference |
| L2 | 1227.60 | GPS | Secondary GPS band, used for ionospheric corrections |
| L5 | 1176.45 | GPS / Galileo | Civil safety-of-life band |
| E1 | 1575.42 | Galileo | Primary Galileo band (co-located with GPS L1) |
| GLONASS L1 | 1598.0625–1605.375 | GLONASS | FDMA, channels 1–6 |
| GLONASS L2 | 1242.9375–1248.625 | GLONASS | Secondary FDMA |
Error codes
| HTTP code | Meaning |
|---|---|
| 200 | Success |
| 400 | Missing or invalid parameters |
| 401 | API key required (protected endpoint, e.g. /flush) |
| 404 | Station not found or no events |
| 409 | Conflict (e.g. model already training) |
| 429 | Too many requests — rate limit exceeded (see Rate limits) |
| 500 | Internal server error (check logs) |
| 503 | Service unavailable (model not trained) |
Rate limits
Public endpoints are rate-limited per IP to protect the infrastructure. Applied limits:
| Endpoint | Limit |
|---|---|
/i, /ce, /sa, /lh/summary | 60 / minute |
/lh, /now | 120 / minute |
/analytics, /trends | 10 / minute |
/api/predict, /api/predict/v1 | 20 / minute |
/flush admin | 10 / hour |
| other endpoints | 200 / minute (default) |
When the limit is hit, the API responds with HTTP 429 Too Many Requests and the body
{"error": "Rate limit exceeded", "retry_after": "..."} — respect the retry hint before retrying.
For intensive queries (e.g. all stations daily), leave at least 500 ms between requests
and prefer the aggregate endpoints (/analytics, /lh/summary) over individual per-station calls.
Examples
cURL — Prediction for Sulina
curl -X POST https://api.geoveil.ro/v2/api/predict \
-H "Content-Type: application/json" \
-d '{"station_code":"SULI","datetime":"2026-05-26T14:00"}'
Python — Last-hour events (specific station, limited)
import requests
resp = requests.get(
"https://api.geoveil.ro/v2/lh",
params={"station_code": "SULI", "limit": 3000}
)
events = resp.json()
for e in events:
print(f"{e['datetime']} {e['rfpath']} {e['centfreq_mhz']} MHz")
JavaScript — Last-hour summary (all stations)
const resp = await fetch("https://api.geoveil.ro/v2/lh/summary");
const stations = await resp.json();
const threats = stations.filter(s => s.threat_type === "spoofing_candidate");
console.log(`${threats.length} stations with spoofing candidates in the last hour`);
JavaScript — Cross-station correlated
const today = new Date().toISOString().split('T')[0];
const resp = await fetch(
`https://api.geoveil.ro/v2/ce?start_date=${today}&end_date=${today}&min_stations=3`
);
const correlated = await resp.json();
console.log(`${correlated.length} correlated event groups today`);
Python — V1 polar chart data
import requests
resp = requests.get(
"https://api.geoveil.ro/v2/v1/i",
params={"statie": "Sulina", "start_date": "2026-04-05", "end_date": "2026-04-05"}
)
events = resp.json()
for e in events:
print(f"{e['DataOra']} {e['BandaRF']} {e['Frecv_cent_MHz']} MHz")