Développeurs
API du journal
Lisez et écrivez votre journal PingDX depuis un autre logiciel de log, un script ou votre site web.
Sur cette page
Introduction
L'API PingDX permet à un membre de garder son journal PingDX synchronisé avec un autre logiciel de log, un script ou son propre site : télécharger les QSO, en ajouter, les corriger ou les supprimer. Elle donne accès uniquement au journal du membre qui a créé la clé.
URL de base
https://pingdx.org/api/v1Requêtes et réponses sont en JSON (UTF-8). Envoyez Content-Type: application/json avec toute requête qui a un corps. Les dates sont des chaînes ISO 8601 ; le serveur répond toujours en UTC (2026-10-05T18:42:00.000Z).
Une description lisible par les machines (OpenAPI 3.1, publique, sans clé) est disponible pour les générateurs de code et les outils comme Postman : https://pingdx.org/api/v1/openapi.json
Obtenir une clé
Chaque membre crée ses propres clés dans Profil → Accès API. Donnez un nom à chaque clé (le logiciel ou le site qui l'utilise) et choisissez ses droits :
- Lecture seule (
READ) : peut lire le journal (requêtesGET) — suffisant pour afficher vos QSO sur un site. - Lecture et écriture (
WRITE) : peut aussi créer, modifier et supprimer des QSO — nécessaire pour une synchronisation dans les deux sens avec un logiciel de log. - La clé complète n'est affichée qu'une seule fois, juste après sa création : copiez-la tout de suite. PingDX n'en garde qu'une empreinte et ne peut plus la réafficher.
- Vous pouvez révoquer une clé à tout moment : elle cesse aussitôt de fonctionner. Au plus 10 clés actives par membre.
Sécurité
- Traitez une clé comme un mot de passe. Ne mettez jamais une clé WRITE dans le code public d'un site (le JavaScript envoyé aux navigateurs des visiteurs) : n'importe qui pourrait la lire et modifier votre journal. Appelez l'API depuis votre serveur, ou utilisez une clé READ.
- Une clé n'ouvre que l'API du journal : jamais vos messages, votre profil, votre mot de passe ni le reste de votre compte.
- Utilisez une clé par logiciel, pour pouvoir en révoquer une sans casser les autres. Les clés commencent par
pdx_…: faciles à repérer si l'une est publiée par erreur — révoquez-la alors.
Authentification
Envoyez la clé avec chaque requête, dans l'en-tête Authorization (Bearer) ou dans l'en-tête X-API-Key. Votre session de connexion PingDX n'est pas acceptée par cette API, et les cookies ne sont jamais utilisés.
Authorization: Bearer pdx_…
# ou
X-API-Key: pdx_…Sans clé valide, l'API répond 401 ; une clé READ qui tente d'écrire reçoit 403 :
401 API_KEYS.INVALIDClé absente, inconnue ou révoquée (ou compte qui n'est plus actif).403 API_KEYS.READ_ONLYLa clé est en lecture seule : créez une clé WRITE pour écrire.
Points d'accès
Tous les chemins sont relatifs à l'URL de base. Chaque QSO est identifié par son clientId, un UUID choisi par votre logiciel.
GET/me
Droits nécessaires : clé READ ou WRITE
Le membre à qui appartient la clé (indicatif, locator) et les droits de la clé. Pratique pour vérifier une clé.
Réponse
{
"callsign": "14KM001",
"locator": "JN03ql",
"scope": "WRITE"
}Erreurs
401 API_KEYS.INVALIDClé absente, inconnue ou révoquée (ou compte qui n'est plus actif).429 GENERIC.RATE_LIMITEDTrop de requêtes : patientez un peu puis réessayez.
GET/logs
Droits nécessaires : clé READ ou WRITE
Le journal, du plus ancien changement au plus récent (trié par dernière écriture côté serveur), page par page.
Sans updatedSince, vous obtenez tout le journal. Avec, seulement les QSO créés, modifiés ou confirmés par croisement après cet instant.
Paramètres
updatedSincestring (ISO 8601)queryfacultatifDate-heure ISO 8601 avec fuseau (ex.
2026-10-01T00:00:00Z). Passez leserverTimede votre synchronisation précédente.limitinteger 1–500 (200)queryfacultatifNombre de QSO par page, de 1 à 500. Par défaut : 200.
cursorstringqueryfacultatifLe
nextCursorde la page précédente, pour obtenir la suivante. Gardez le mêmeupdatedSincependant la pagination.
Réponse
items : les QSO de la page (voir l'objet QSO). nextCursor : à passer en cursor pour la page suivante ; null sur la dernière page. serverTime : l'heure du serveur au début de la requête — votre prochain 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"
}Erreurs
400 VALIDATION.FAILEDRequête invalide : mauvais paramètre, clientId qui n'est pas un UUID, ou lot sans tableau entries valide (1 à 200). details décrit le problème.401 API_KEYS.INVALIDClé absente, inconnue ou révoquée (ou compte qui n'est plus actif).429 GENERIC.RATE_LIMITEDTrop de requêtes : patientez un peu puis réessayez.
GET/logs/ids
Droits nécessaires : clé READ ou WRITE
Les identifiants de tous les QSO du journal, sans pagination. Comparez-les avec votre copie pour trouver les QSO supprimés sur PingDX (ou depuis un autre logiciel).
Réponse
clientId peut valoir null pour quelques très anciens QSO, créés avant l'existence de la synchronisation.
{
"items": [
{ "id": "cmgd4w1x70003s60e8k2v9qhz", "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f" },
{ "id": "cmgd51b2c0007s60efq3m1abc", "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b" }
]
}Erreurs
401 API_KEYS.INVALIDClé absente, inconnue ou révoquée (ou compte qui n'est plus actif).429 GENERIC.RATE_LIMITEDTrop de requêtes : patientez un peu puis réessayez.
POST/logs/batch
Droits nécessaires : clé WRITE
Crée ou met à jour jusqu'à 200 QSO en une requête. Un QSO dont le clientId est inconnu est créé ; un clientId existant met à jour ce QSO.
Chaque QSO est validé séparément : un QSO invalide est signalé dans son résultat et n'empêche jamais les autres d'être enregistrés. La requête n'échoue en bloc (400) que si entries est absent, vide ou dépasse 200.
Paramètres
entriesQSO[] (1–200)corpsobligatoireLes QSO à créer ou mettre à jour (voir l'objet QSO).
Corps de la requête
{
"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"
}
]
}Réponse
Un résultat par QSO. id est l'identifiant serveur, ou null si le QSO a été refusé ; error en donne alors la raison. permanent: true signifie que le QSO lui-même est invalide : il sera refusé à nouveau tant qu'il n'est pas corrigé — ne le renvoyez pas tel quel. Un refus sans permanent est temporaire : réessayez plus tard.
{
"results": [
{ "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f", "id": "cmgd4w1x70003s60e8k2v9qhz", "spottedAt": null },
{ "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b", "id": null, "error": "LOCATOR_INVALID", "permanent": true }
]
}Erreurs
400 VALIDATION.FAILEDRequête invalide : mauvais paramètre, clientId qui n'est pas un UUID, ou lot sans tableau entries valide (1 à 200). details décrit le problème.401 API_KEYS.INVALIDClé absente, inconnue ou révoquée (ou compte qui n'est plus actif).403 API_KEYS.READ_ONLYLa clé est en lecture seule : créez une clé WRITE pour écrire.429 GENERIC.RATE_LIMITEDTrop de requêtes : patientez un peu puis réessayez.
DELETE/logs/{clientId}
Droits nécessaires : clé WRITE
Supprime un QSO de votre journal par son clientId. S'il avait été envoyé au cluster DX, le spot est retiré aussi.
Paramètres
clientIdstring (UUID)cheminobligatoireLe
clientIddu QSO (un UUID).
Réponse
Pas de corps (204). Envoyez cette requête sans corps et sans Content-Type.
HTTP/1.1 204 No ContentErreurs
400 VALIDATION.FAILEDRequête invalide : mauvais paramètre, clientId qui n'est pas un UUID, ou lot sans tableau entries valide (1 à 200). details décrit le problème.401 API_KEYS.INVALIDClé absente, inconnue ou révoquée (ou compte qui n'est plus actif).403 API_KEYS.READ_ONLYLa clé est en lecture seule : créez une clé WRITE pour écrire.404 GENERIC.NOT_FOUNDAucun QSO avec ce clientId dans votre journal.429 GENERIC.RATE_LIMITEDTrop de requêtes : patientez un peu puis réessayez.
GET/openapi.json
Droits nécessaires : aucun (public)
La description OpenAPI 3.1 de cette API (JSON). Publique : aucune clé nécessaire.
L'objet QSO
Le même objet est envoyé à POST /logs/batch et renvoyé par GET /logs. Le champ band choisit l'une des deux variantes :
- 11 m (CB) —
"band": "ELEVEN_METERS":frequencyMhz(facultatif) ;channel,ctcssToneetdcsCodedoivent être absents ounull. - PMR446 —
"band": "PMR446":channel(1 à 16) est obligatoire,ctcssTone/dcsCodefacultatifs ;frequencyMhzdoit être absent ounull. - Une mise à jour remplace le QSO : envoyez toujours le QSO complet. Un champ omis est remis à zéro (vide, ou
falsepourqslSent/qslReceived), saufoperatorCallsign,dxLat,dxLng,txPowerW,radioetantenna, qui gardent leur valeur enregistrée s'ils sont omis. - Les textes sont nettoyés des espaces en trop, les locators normalisés (
jn18DU→JN18du) et les champs inconnus ignorés. Les champs en lecture seule sont fixés par le serveur : inutile de les envoyer.
clientIdobligatoirestring (UUID)Votre identifiant stable pour ce QSO : un UUID aléatoire (ex.
crypto.randomUUID()), généré une fois et conservé avec le QSO dans votre logiciel. C'est la clé de synchronisation.bandobligatoire"ELEVEN_METERS" | "PMR446"La bande :
ELEVEN_METERS(11 m / CB) ouPMR446.qsoAtobligatoirestring (ISO 8601)Date et heure du QSO, ISO 8601 avec fuseau (
Zou+02:00). Enregistrée et renvoyée en UTC.frequencyMhzfacultatifstring "27.555" · 26.000–28.00011 m uniquement : la fréquence en MHz, sous forme de chaîne avec exactement 3 décimales, entre 26.000 et 28.000.
channelobligatoire en PMR446integer 1–16PMR446 uniquement (obligatoire) : le canal, de 1 à 16.
ctcssTonefacultatifstring "67.0" · ^\d{2,3}\.\d$PMR446 uniquement : tonalité CTCSS en Hz, une décimale (
67.0,103.5).dcsCodefacultatifstring "D023N" · ^D?[0-7]{3}[NI]?$PMR446 uniquement : code DCS, 3 chiffres octaux avec préfixe
Det polaritéN/Ifacultatifs (D023N).modefacultatif"FM" | "AM" | "SSB" | "USB" | "LSB" | "CW" | "DIGITAL"Le mode.
callsignWorkedfacultatifstring ≤ 20L'indicatif de la station contactée.
operatorCallsignfacultatifstring ≤ 20Votre propre indicatif pour ce QSO (utile si vous en avez plusieurs). Conservé s'il est omis lors d'une mise à jour.
dxLocatorfacultatifstring "JN18" | "JN18du"Locator Maidenhead de la station contactée, 4 ou 6 caractères.
dxLatfacultatifnumber −90…90Latitude exacte de la station contactée, si connue. Les cartes la préfèrent au locator.
dxLngfacultatifnumber −180…180Longitude exacte de la station contactée, si connue.
txPowerWfacultatifinteger 0–100000Votre puissance d'émission en watts entiers.
myLocatorfacultatifstring "JN03" | "JN03ql"Votre locator pour ce QSO (ex. en portable), 4 ou 6 caractères.
splitKhzfacultatifinteger −99999…99999Split en kHz (écart entre fréquences d'émission et de réception).
pathfacultatif"SP" | "LP"Trajet de propagation : court (
SP) ou long (LP).qsoStatusfacultatif"HRD" | "WKD" | "CFM"Statut du contact : entendu (
HRD), contacté (WKD), confirmé (CFM).rstSentfacultatifstring ≤ 10Report envoyé (ex.
59).rstReceivedfacultatifstring ≤ 10Report reçu.
qslSentfacultatifboolean (false)Carte QSL envoyée. Par défaut :
false.qslReceivedfacultatifboolean (false)Carte QSL reçue. Par défaut :
false.qslViafacultatif"DIRECT" | "BUREAU" | "EQSL" | "ECARD"Comment la QSL a voyagé :
DIRECT(courrier),BUREAU,EQSL(eQSL.cc) ouECARD(autre carte électronique).notesfacultatifstring ≤ 2000Notes libres.
radiofacultatifstring ≤ 80Le poste utilisé (texte libre).
antennafacultatifstring ≤ 80L'antenne utilisée (texte libre).
sendSpotfacultatifbooleanEn écriture seulement, jamais renvoyé :
truepublie aussi le QSO en spot sur le cluster DX de PingDX (une fois par QSO). Uniquement pour un QSO en direct, jamais pour l'import d'anciens QSO.idlecture seulestringL'identifiant serveur du QSO.
crossConfirmedAtlecture seulestring (ISO 8601) | nullFixé par PingDX quand l'autre station a noté le même QSO sur PingDX (confirmation croisée).
spottedAtlecture seulestring (ISO 8601) | nullQuand le QSO a été publié en spot sur le cluster, ou
null.createdAtlecture seulestring (ISO 8601)Quand le QSO a été enregistré pour la première fois sur PingDX.
syncedAtlecture seulestring (ISO 8601)Dernière écriture du QSO côté serveur (sert au tri de
GET /logs).
Guide de synchronisation
Une synchronisation fiable dans les deux sens entre votre logiciel et PingDX, en six étapes :
- 1
Premier téléchargement complet
Appelez
GET /logs?limit=500sansupdatedSince, puis suiveznextCursorjusqu'à ce qu'il vaillenull. Gardez leserverTimede la première page. - 2
Téléchargement incrémental
La fois suivante, appelez
GET /logs?updatedSince=…avec leserverTimegardé, suivez les pages de la même façon, puis enregistrez le nouveauserverTime(toujours celui de la première page) seulement une fois toutes les pages traitées. Recevoir deux fois un QSO est sans danger : appliquez-le parclientId. - 3
Envoyer vos QSO
Donnez une fois pour toutes à chaque QSO de votre logiciel un
clientIdUUID, et conservez-le. Envoyez les QSO nouveaux ou modifiés avecPOST /logs/batch, au plus 200 par requête. Renvoyer le même QSO est sans risque : il est mis à jour au lieu d'être dupliqué. - 4
QSO refusés
Lisez chaque résultat : si
idvautnullavecpermanent: true, montrez le QSO à l'utilisateur pour qu'il le corrige (errordit pourquoi) au lieu de réessayer. Sanspermanent, réessayez plus tard. - 5
Suppressions
Pour supprimer sur PingDX :
DELETE /logs/{clientId}. Pour trouver les QSO supprimés sur PingDX :GET /logs/ids, puis retirez de votre copie chaque QSO dont leclientIdn'y figure plus. - 6
Conflits
La dernière écriture gagne, par
clientId: pas de fusion champ par champ. L'application PingDX récupère les QSO écrits via l'API à sa prochaine synchronisation (à l'ouverture, ou en quelques secondes quand elle est en ligne).
Erreurs et limites
Les erreurs utilisent le code de statut HTTP et un corps JSON avec un code stable (jamais un texte traduit) :
{ "error": { "code": "API_KEYS.READ_ONLY" } }400 VALIDATION.FAILEDRequête invalide : mauvais paramètre, clientId qui n'est pas un UUID, ou lot sans tableau entries valide (1 à 200). details décrit le problème.
401 API_KEYS.INVALIDClé absente, inconnue ou révoquée (ou compte qui n'est plus actif).
403 API_KEYS.READ_ONLYLa clé est en lecture seule : créez une clé WRITE pour écrire.
404 GENERIC.NOT_FOUNDAucun QSO avec ce clientId dans votre journal.
429 GENERIC.RATE_LIMITEDTrop de requêtes : patientez un peu puis réessayez.
500 GENERIC.INTERNAL_ERRORErreur du serveur : réessayez plus tard.
Raisons de refus d'un QSO
Dans POST /logs/batch, un QSO refusé a id: null et un code error :
CLIENT_ID_INVALIDclientId n'est pas un UUID valide.DATETIME_INVALIDqsoAt n'est pas une date-heure ISO 8601 avec fuseau.LOCATOR_INVALIDdxLocator ou myLocator n'est pas un locator Maidenhead de 4 ou 6 caractères.FREQUENCY_FORMAT_INVALIDfrequencyMhz doit être une chaîne à 3 décimales (27.555).FREQUENCY_OUT_OF_BAND_11MfrequencyMhz est hors de 26.000–28.000 MHz.CHANNEL_OUT_OF_RANGECanal PMR446 hors de 1–16.CTCSS_FORMAT_INVALIDctcssTone doit ressembler à 67.0.DCS_FORMAT_INVALIDdcsCode doit ressembler à D023N.CLIENT_ID_CONFLICTCe clientId appartient déjà au QSO d'un autre membre : générez un nouvel UUID.
Les autres violations du schéma (champ manquant, valeur inconnue dans une liste, texte trop long…) renvoient le message du validateur en anglais, par ex. Invalid input: expected string, received undefined, avec aussi permanent: true.
Limites
- 300 requêtes par minute par adresse IP, au-delà
429(l'en-têteRetry-Afterindique combien de temps attendre). Regroupez vos QSO en lots plutôt que de les envoyer un par un. - 200 QSO au plus par
POST /logs/batch. - 1 à 500 QSO par page de
GET /logs(200 par défaut). - 10 clés actives au plus par membre.
Appeler depuis un navigateur (CORS)
/api/v1 accepte les requêtes de n'importe quelle origine (CORS), sans cookies ni identifiants. Une page web peut donc l'appeler directement — mais uniquement avec une clé READ, puisque le code d'une page publique est visible de tous.
Exemples
Remplacez pdx_… par votre clé. Ces exemples sont prêts à copier ; seuls les commentaires sont en anglais.
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)
Fonctionne avec Node.js 18+ et Deno. Gardez la clé côté serveur (variable d'environnement).
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"]}Une question, ou un logiciel que vous aimeriez connecter ? Écrivez-nous depuis la page Contact.