Sviluppatori
API del log
Leggi e scrivi il tuo log PingDX da un altro programma di logging, da uno script o dal tuo sito web.
In questa pagina
Introduzione
L’API di PingDX permette a un membro di mantenere il proprio log PingDX sincronizzato con un altro programma di logging, uno script o il proprio sito web: scaricare i QSO, aggiungerne di nuovi, correggerli o eliminarli. Dà accesso solo al log del membro che ha creato la chiave.
URL di base
https://pingdx.org/api/v1Richieste e risposte sono in JSON (UTF-8). Invia Content-Type: application/json con ogni richiesta che ha un corpo. Le date sono stringhe ISO 8601; il server risponde sempre in UTC (2026-10-05T18:42:00.000Z).
Una descrizione leggibile da macchina (OpenAPI 3.1, pubblica, senza chiave) è disponibile per i generatori di codice e per strumenti come Postman: https://pingdx.org/api/v1/openapi.json
Ottenere una chiave
Ogni membro crea le proprie chiavi in Profilo → Accesso API. Dai a ogni chiave un nome (il programma o il sito che la usa) e scegli i suoi permessi:
- Sola lettura (
READ): può leggere il log (richiesteGET) — sufficiente per mostrare i tuoi QSO su un sito web. - Lettura e scrittura (
WRITE): può anche creare, aggiornare ed eliminare QSO — necessaria per una sincronizzazione bidirezionale con un programma di logging. - La chiave completa viene mostrata una sola volta, subito dopo la creazione: copiala subito. PingDX ne conserva solo un’impronta e non può mostrarla di nuovo.
- Puoi revocare una chiave in qualsiasi momento: smette di funzionare immediatamente. Al massimo 10 chiavi attive per membro.
Sicurezza
- Tratta una chiave come una password. Non inserire mai una chiave WRITE nel codice pubblico di un sito web (JavaScript inviato ai browser dei visitatori): chiunque potrebbe leggerla e modificare il tuo log. Chiama l’API dal tuo server, oppure usa una chiave READ.
- Una chiave apre solo l’API del log: mai i tuoi messaggi, il profilo, la password o il resto del tuo account.
- Usa una chiave per programma, così puoi revocarne una senza bloccare le altre. Le chiavi iniziano con
pdx_…: facili da riconoscere se una viene pubblicata per errore — in tal caso revocala.
Autenticazione
Invia la chiave con ogni richiesta, nell’header Authorization (Bearer) oppure nell’header X-API-Key. La tua sessione di accesso a PingDX non è accettata da questa API e i cookie non vengono mai usati.
Authorization: Bearer pdx_…
# oppure
X-API-Key: pdx_…Senza una chiave valida l’API risponde 401; una chiave READ che tenta di scrivere riceve 403:
401 API_KEYS.INVALIDChiave mancante, sconosciuta o revocata (o account non più attivo).403 API_KEYS.READ_ONLYLa chiave è di sola lettura: crea una chiave WRITE per scrivere.
Endpoint
Tutti i percorsi sono relativi all’URL di base. Ogni QSO è identificato dal proprio clientId, un UUID scelto dal tuo programma.
GET/me
Permessi richiesti: chiave READ o WRITE
Il membro a cui appartiene la chiave (indicativo, locator) e i permessi della chiave. Utile per verificare una chiave.
Risposta
{
"callsign": "14KM001",
"locator": "JN03ql",
"scope": "WRITE"
}Errori
401 API_KEYS.INVALIDChiave mancante, sconosciuta o revocata (o account non più attivo).429 GENERIC.RATE_LIMITEDTroppe richieste: attendi un po’ e riprova.
GET/logs
Permessi richiesti: chiave READ o WRITE
Il log, dalla modifica meno recente (ordinato per ultima scrittura lato server), pagina per pagina.
Senza updatedSince ottieni l’intero log. Con questo parametro, solo i QSO creati, modificati o confermati in modo incrociato dopo quell’istante.
Parametri
updatedSincestring (ISO 8601)queryfacoltativoData e ora ISO 8601 con fuso orario (es.
2026-10-01T00:00:00Z). Passa ilserverTimedella sincronizzazione precedente.limitinteger 1–500 (200)queryfacoltativoQSO per pagina, da 1 a 500. Predefinito: 200.
cursorstringqueryfacoltativoIl
nextCursordella pagina precedente, per ottenere la successiva. Mantieni lo stessoupdatedSincedurante la paginazione.
Risposta
items: i QSO della pagina (vedi l’oggetto QSO). nextCursor: passalo come cursor per ottenere la pagina successiva; null nell’ultima pagina. serverTime: l’ora del server all’inizio della richiesta — il tuo prossimo updatedSince.
{
"items": [
{
"id": "cmgd4w1x70003s60e8k2v9qhz",
"clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f",
"band": "ELEVEN_METERS",
"frequencyMhz": "27.555",
"channel": null,
"ctcssTone": null,
"dcsCode": null,
"mode": "USB",
"callsignWorked": "14KM123",
"operatorCallsign": "14KM001",
"dxLocator": "JN18du",
"dxLat": null,
"dxLng": null,
"txPowerW": 4,
"myLocator": "JN03ql",
"splitKhz": null,
"path": null,
"qsoStatus": null,
"rstSent": "59",
"rstReceived": "57",
"qslSent": false,
"qslReceived": false,
"qslVia": null,
"qsoAt": "2026-10-05T18:42:00.000Z",
"notes": null,
"radio": "President Lincoln II+",
"antenna": "Sirio 827",
"crossConfirmedAt": null,
"spottedAt": null,
"createdAt": "2026-10-05T18:43:10.512Z",
"syncedAt": "2026-10-05T18:43:10.512Z"
}
],
"nextCursor": null,
"serverTime": "2026-10-05T18:45:12.345Z"
}Errori
400 VALIDATION.FAILEDRichiesta non valida: un parametro errato, un clientId che non è un UUID, o un batch senza un array entries valido (da 1 a 200). details descrive il problema.401 API_KEYS.INVALIDChiave mancante, sconosciuta o revocata (o account non più attivo).429 GENERIC.RATE_LIMITEDTroppe richieste: attendi un po’ e riprova.
GET/logs/ids
Permessi richiesti: chiave READ o WRITE
Gli id di tutti i QSO del log, senza paginazione. Confrontali con la tua copia per trovare i QSO eliminati su PingDX (o da un altro programma).
Risposta
clientId può essere null per alcuni QSO molto vecchi creati prima dell’esistenza della sincronizzazione.
{
"items": [
{ "id": "cmgd4w1x70003s60e8k2v9qhz", "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f" },
{ "id": "cmgd51b2c0007s60efq3m1abc", "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b" }
]
}Errori
401 API_KEYS.INVALIDChiave mancante, sconosciuta o revocata (o account non più attivo).429 GENERIC.RATE_LIMITEDTroppe richieste: attendi un po’ e riprova.
POST/logs/batch
Permessi richiesti: chiave WRITE
Crea o aggiorna fino a 200 QSO in una sola richiesta. Un QSO il cui clientId è sconosciuto viene creato; un clientId esistente aggiorna quel QSO.
Ogni QSO viene validato separatamente: un QSO non valido è segnalato nel proprio risultato e non impedisce mai il salvataggio degli altri. La richiesta fallisce nel suo insieme (400) solo quando entries è mancante, vuoto o più lungo di 200.
Parametri
entriesQSO[] (1–200)corpoobbligatorioI QSO da creare o aggiornare (vedi l’oggetto QSO).
Corpo della richiesta
{
"entries": [
{
"clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f",
"band": "ELEVEN_METERS",
"frequencyMhz": "27.555",
"mode": "USB",
"callsignWorked": "14KM123",
"qsoAt": "2026-10-05T18:42:00Z",
"rstSent": "59",
"rstReceived": "57",
"dxLocator": "JN18DU"
},
{
"clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b",
"band": "PMR446",
"channel": 8,
"ctcssTone": "67.0",
"mode": "FM",
"callsignWorked": "PMR-ALPHA",
"qsoAt": "2026-10-05T19:05:00+02:00",
"dxLocator": "JN1"
}
]
}Risposta
Un risultato per ogni QSO. id è l’id del server, oppure null se il QSO è stato rifiutato; error ne indica allora il motivo. permanent: true significa che il QSO stesso non è valido: verrà rifiutato di nuovo finché non sarà corretto — non riprovare a inviarlo così com’è. Un rifiuto senza permanent è temporaneo: riprova più tardi.
{
"results": [
{ "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f", "id": "cmgd4w1x70003s60e8k2v9qhz", "spottedAt": null },
{ "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b", "id": null, "error": "LOCATOR_INVALID", "permanent": true }
]
}Errori
400 VALIDATION.FAILEDRichiesta non valida: un parametro errato, un clientId che non è un UUID, o un batch senza un array entries valido (da 1 a 200). details descrive il problema.401 API_KEYS.INVALIDChiave mancante, sconosciuta o revocata (o account non più attivo).403 API_KEYS.READ_ONLYLa chiave è di sola lettura: crea una chiave WRITE per scrivere.429 GENERIC.RATE_LIMITEDTroppe richieste: attendi un po’ e riprova.
DELETE/logs/{clientId}
Permessi richiesti: chiave WRITE
Elimina un QSO del tuo log tramite il suo clientId. Se era stato inviato al cluster DX, viene rimosso anche lo spot.
Parametri
clientIdstring (UUID)percorsoobbligatorioIl
clientIddel QSO (un UUID).
Risposta
Nessun corpo (204). Invia questa richiesta senza corpo e senza Content-Type.
HTTP/1.1 204 No ContentErrori
400 VALIDATION.FAILEDRichiesta non valida: un parametro errato, un clientId che non è un UUID, o un batch senza un array entries valido (da 1 a 200). details descrive il problema.401 API_KEYS.INVALIDChiave mancante, sconosciuta o revocata (o account non più attivo).403 API_KEYS.READ_ONLYLa chiave è di sola lettura: crea una chiave WRITE per scrivere.404 GENERIC.NOT_FOUNDNessun QSO con questo clientId nel tuo log.429 GENERIC.RATE_LIMITEDTroppe richieste: attendi un po’ e riprova.
GET/openapi.json
Permessi richiesti: nessuno (pubblico)
La descrizione OpenAPI 3.1 di questa API (JSON). Pubblica: nessuna chiave necessaria.
L’oggetto QSO
Lo stesso oggetto viene inviato a POST /logs/batch e restituito da GET /logs. Il campo band seleziona una delle due varianti:
- 11 m (CB) —
"band": "ELEVEN_METERS":frequencyMhz(facoltativo);channel,ctcssToneedcsCodedevono essere assenti onull. - PMR446 —
"band": "PMR446":channel(da 1 a 16) è obbligatorio,ctcssTone/dcsCodefacoltativi;frequencyMhzdeve essere assente onull. - Un aggiornamento sostituisce il QSO: invia sempre il QSO completo. Un campo omesso viene reimpostato (vuoto, oppure
falseperqslSent/qslReceived), tranneoperatorCallsign,dxLat,dxLng,txPowerW,radioeantenna, che mantengono il valore memorizzato se omessi. - Il testo viene ripulito dagli spazi, i locator vengono normalizzati (
jn18DU→JN18du) e i campi sconosciuti vengono ignorati. I campi di sola lettura sono impostati dal server: non serve inviarli.
clientIdobbligatoriostring (UUID)Il tuo id stabile per questo QSO: un UUID casuale (es.
crypto.randomUUID()), generato una sola volta e memorizzato con il QSO nel tuo programma. È la chiave di sincronizzazione.bandobbligatorio"ELEVEN_METERS" | "PMR446"La banda:
ELEVEN_METERS(11 m / CB) oppurePMR446.qsoAtobbligatoriostring (ISO 8601)Data e ora del QSO, ISO 8601 con fuso orario (
Zoppure+02:00). Memorizzato e restituito in UTC.frequencyMhzfacoltativostring "27.555" · 26.000–28.000Solo 11 m: la frequenza in MHz, come stringa con esattamente 3 decimali, tra 26.000 e 28.000.
channelobbligatorio su PMR446integer 1–16Solo PMR446 (qui obbligatorio): il canale, da 1 a 16.
ctcssTonefacoltativostring "67.0" · ^\d{2,3}\.\d$Solo PMR446: tono CTCSS in Hz, con un decimale (
67.0,103.5).dcsCodefacoltativostring "D023N" · ^D?[0-7]{3}[NI]?$Solo PMR446: codice DCS, 3 cifre ottali con prefisso
Dfacoltativo e polaritàN/I(D023N).modefacoltativo"FM" | "AM" | "SSB" | "USB" | "LSB" | "CW" | "DIGITAL"Il modo.
callsignWorkedfacoltativostring ≤ 20L’indicativo della stazione collegata.
operatorCallsignfacoltativostring ≤ 20Il tuo indicativo per questo QSO (utile se ne usi più di uno). Mantenuto se omesso in un aggiornamento.
dxLocatorfacoltativostring "JN18" | "JN18du"Locator Maidenhead della stazione collegata, di 4 o 6 caratteri.
dxLatfacoltativonumber −90…90Latitudine esatta della stazione collegata, se nota. Le mappe la preferiscono al locator.
dxLngfacoltativonumber −180…180Longitudine esatta della stazione collegata, se nota.
txPowerWfacoltativointeger 0–100000La tua potenza in trasmissione in watt interi.
myLocatorfacoltativostring "JN03" | "JN03ql"Il tuo locator per questo QSO (es. in portatile), di 4 o 6 caratteri.
splitKhzfacoltativointeger −99999…99999Split in kHz (differenza tra frequenza di trasmissione e di ricezione).
pathfacoltativo"SP" | "LP"Percorso di propagazione: short path (
SP) o long path (LP).qsoStatusfacoltativo"HRD" | "WKD" | "CFM"Stato del contatto: ascoltato (
HRD), collegato (WKD), confermato (CFM).rstSentfacoltativostring ≤ 10Rapporto inviato (es.
59).rstReceivedfacoltativostring ≤ 10Rapporto ricevuto.
qslSentfacoltativoboolean (false)QSL inviata. Predefinito:
false.qslReceivedfacoltativoboolean (false)QSL ricevuta. Predefinito:
false.qslViafacoltativo"DIRECT" | "BUREAU" | "EQSL" | "ECARD"Come ha viaggiato la QSL:
DIRECT(posta),BUREAU,EQSL(eQSL.cc) oppureECARD(altra cartolina elettronica).notesfacoltativostring ≤ 2000Note libere.
radiofacoltativostring ≤ 80La radio utilizzata (testo libero).
antennafacoltativostring ≤ 80L’antenna utilizzata (testo libero).
sendSpotfacoltativobooleanSolo scrittura, mai restituito:
truepubblica anche il QSO come spot sul cluster DX di PingDX (una sola volta per QSO). Solo per QSO in diretta, mai per l’importazione di quelli vecchi.idsola letturastringL’id del QSO sul server.
crossConfirmedAtsola letturastring (ISO 8601) | nullImpostato da PingDX quando l’altra stazione ha registrato lo stesso QSO su PingDX (conferma incrociata).
spottedAtsola letturastring (ISO 8601) | nullQuando il QSO è stato pubblicato come spot sul cluster, oppure
null.createdAtsola letturastring (ISO 8601)Quando il QSO è stato salvato per la prima volta su PingDX.
syncedAtsola letturastring (ISO 8601)Ultima scrittura lato server del QSO (determina l’ordine di
GET /logs).
Guida alla sincronizzazione
Una sincronizzazione bidirezionale robusta tra il tuo programma e PingDX in sei passaggi:
- 1
Primo download completo
Chiama
GET /logs?limit=500senzaupdatedSince, poi seguinextCursorfinché non ènull. Conserva ilserverTimedella prima pagina. - 2
Download incrementale
La volta successiva, chiama
GET /logs?updatedSince=…con ilserverTimesalvato, segui le pagine allo stesso modo, poi salva il nuovoserverTime(sempre quello della prima pagina) solo dopo aver elaborato tutte le pagine. Ricevere due volte lo stesso QSO è innocuo: applicalo in base alclientId. - 3
Invio dei tuoi QSO
Assegna una volta per tutte a ogni QSO del tuo programma un
clientIdUUID e memorizzalo. Invia i QSO nuovi o modificati conPOST /logs/batch, al massimo 200 per richiesta. Inviare di nuovo lo stesso QSO è sicuro: lo aggiorna invece di creare un duplicato. - 4
QSO rifiutati
Leggi ogni risultato: se
idènullconpermanent: true, mostra il QSO all’utente per la correzione (errorne spiega il motivo) invece di riprovare. Senzapermanent, riprova più tardi. - 5
Eliminazioni
Per eliminare su PingDX:
DELETE /logs/{clientId}. Per trovare i QSO eliminati su PingDX:GET /logs/ids, poi rimuovi dalla tua copia ogni QSO il cuiclientIdnon è più elencato. - 6
Conflitti
Vince l’ultima scrittura, per
clientId: non c’è unione campo per campo. L’app PingDX recepisce i QSO scritti tramite l’API alla sua sincronizzazione successiva (all’apertura, o entro pochi secondi se è online).
Errori e limiti
Gli errori usano il codice di stato HTTP e un corpo JSON con un codice stabile (mai un testo tradotto):
{ "error": { "code": "API_KEYS.READ_ONLY" } }400 VALIDATION.FAILEDRichiesta non valida: un parametro errato, un clientId che non è un UUID, o un batch senza un array entries valido (da 1 a 200). details descrive il problema.
401 API_KEYS.INVALIDChiave mancante, sconosciuta o revocata (o account non più attivo).
403 API_KEYS.READ_ONLYLa chiave è di sola lettura: crea una chiave WRITE per scrivere.
404 GENERIC.NOT_FOUNDNessun QSO con questo clientId nel tuo log.
429 GENERIC.RATE_LIMITEDTroppe richieste: attendi un po’ e riprova.
500 GENERIC.INTERNAL_ERRORErrore del server: riprova più tardi.
Motivi di rifiuto per singolo QSO
In POST /logs/batch, un QSO rifiutato ha id: null e un codice error:
CLIENT_ID_INVALIDclientId non è un UUID valido.DATETIME_INVALIDqsoAt non è una data e ora ISO 8601 con fuso orario.LOCATOR_INVALIDdxLocator o myLocator non è un locator Maidenhead di 4 o 6 caratteri.FREQUENCY_FORMAT_INVALIDfrequencyMhz deve essere una stringa con 3 decimali (27.555).FREQUENCY_OUT_OF_BAND_11MfrequencyMhz è al di fuori di 26.000–28.000 MHz.CHANNEL_OUT_OF_RANGECanale PMR446 al di fuori di 1–16.CTCSS_FORMAT_INVALIDctcssTone deve avere la forma 67.0.DCS_FORMAT_INVALIDdcsCode deve avere la forma D023N.CLIENT_ID_CONFLICTQuesto clientId appartiene già al QSO di un altro membro: genera un nuovo UUID.
Altre violazioni dello schema (campo mancante, valore sconosciuto in un enum, testo troppo lungo…) restituiscono il messaggio del validatore in inglese, es. Invalid input: expected string, received undefined, sempre con permanent: true.
Limiti
- 300 richieste al minuto per indirizzo IP, oltre le quali
429(l’headerRetry-Afterindica quanto attendere). Raggruppa i QSO in batch invece di inviarli uno a uno. - Al massimo 200 QSO per
POST /logs/batch. - Da 1 a 500 QSO per pagina di
GET /logs(200 per impostazione predefinita). - Al massimo 10 chiavi attive per membro.
Chiamata da un browser (CORS)
/api/v1 accetta richieste da qualsiasi origine (CORS), senza cookie né credenziali. Una pagina web può quindi chiamarla direttamente — ma solo con una chiave READ, poiché il codice di una pagina pubblica è visibile a tutti.
Esempi
Sostituisci pdx_… con la tua chiave. Questi esempi sono pronti da copiare; solo i commenti sono in inglese.
curl
# Who am I? (checks the key)
curl -H "Authorization: Bearer pdx_…" https://pingdx.org/api/v1/me
# Everything changed since my last sync
curl -H "Authorization: Bearer pdx_…" \
"https://pingdx.org/api/v1/logs?updatedSince=2026-10-01T00:00:00Z&limit=500"
# Create or update a QSO (WRITE key)
curl -X POST -H "Authorization: Bearer pdx_…" \
-H "Content-Type: application/json" \
https://pingdx.org/api/v1/logs/batch \
-d '{"entries":[{"clientId":"0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f",
"band":"ELEVEN_METERS","frequencyMhz":"27.555","mode":"USB",
"callsignWorked":"14KM123","qsoAt":"2026-10-05T18:42:00Z",
"rstSent":"59","rstReceived":"57"}]}'
# Delete it (no body, no Content-Type)
curl -X DELETE -H "Authorization: Bearer pdx_…" \
https://pingdx.org/api/v1/logs/0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6fJavaScript (fetch)
Funziona con Node.js 18+ e Deno. Tieni la chiave lato server (variabile d’ambiente).
const BASE = "https://pingdx.org/api/v1";
const KEY = process.env.PINGDX_API_KEY; // pdx_… — keep it server-side
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 204) return null;
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error?.code}`);
return data;
}
// Full download, page by page
async function pullAll(updatedSince) {
const qsos = [];
let cursor = null;
let since = null;
do {
const q = new URLSearchParams({ limit: "500" });
if (updatedSince) q.set("updatedSince", updatedSince);
if (cursor) q.set("cursor", cursor);
const page = await call("GET", `/logs?${q}`);
since ??= page.serverTime; // keep the FIRST page's serverTime
qsos.push(...page.items);
cursor = page.nextCursor;
} while (cursor);
return { qsos, nextUpdatedSince: since };
}
// Push local QSOs (create or update by clientId)
const { results } = await call("POST", "/logs/batch", {
entries: [{
clientId: crypto.randomUUID(), // store it with your QSO, reuse it forever
band: "PMR446", channel: 8, ctcssTone: "67.0", mode: "FM",
callsignWorked: "PMR-ALPHA", qsoAt: new Date().toISOString(),
}],
});
for (const r of results) if (r.id === null) console.warn(r.clientId, r.error, r.permanent);Python (requests)
import os, uuid, requests
BASE = "https://pingdx.org/api/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['PINGDX_API_KEY']}" # pdx_…
def pull(updated_since=None):
"""Every QSO changed since updated_since (all of them if None)."""
qsos, cursor, next_since = [], None, None
while True:
params = {"limit": 500}
if updated_since:
params["updatedSince"] = updated_since
if cursor:
params["cursor"] = cursor
r = S.get(f"{BASE}/logs", params=params, timeout=30)
r.raise_for_status()
page = r.json()
next_since = next_since or page["serverTime"] # first page's serverTime
qsos += page["items"]
cursor = page["nextCursor"]
if not cursor:
return qsos, next_since
def push(entries):
"""Create or update up to 200 QSOs; returns the rejected ones."""
r = S.post(f"{BASE}/logs/batch", json={"entries": entries}, timeout=30)
r.raise_for_status()
return [x for x in r.json()["results"] if x["id"] is None]
rejected = push([{
"clientId": str(uuid.uuid4()), # keep it with your QSO
"band": "ELEVEN_METERS", "frequencyMhz": "27.555", "mode": "USB",
"callsignWorked": "14KM123", "qsoAt": "2026-10-05T18:42:00Z",
}])
# Deleted on PingDX? Compare clientIds with your copy
server_ids = {x["clientId"] for x in S.get(f"{BASE}/logs/ids", timeout=30).json()["items"]}Una domanda o un programma che vorresti collegare? Scrivici dalla pagina Contatti.