Desarrolladores
API del log
Lee y escribe tu log de PingDX desde otro programa de registro, un script o tu sitio web.
En esta página
Introducción
La API de PingDX permite a un miembro mantener su log de PingDX sincronizado con otro programa de registro, un script o su propio sitio web: descargar QSO, añadir nuevos, corregirlos o eliminarlos. Solo da acceso al log del miembro que creó la clave.
URL base
https://pingdx.org/api/v1Las solicitudes y respuestas son JSON (UTF-8). Envía Content-Type: application/json con cada solicitud que tenga cuerpo. Las fechas son cadenas ISO 8601; el servidor siempre responde en UTC (2026-10-05T18:42:00.000Z).
Hay disponible una descripción legible por máquinas (OpenAPI 3.1, pública, sin clave) para generadores de código y herramientas como Postman: https://pingdx.org/api/v1/openapi.json
Obtener una clave
Cada miembro crea sus propias claves en Perfil → Acceso API. Dale a cada clave un nombre (el programa o sitio que la usa) y elige sus permisos:
- Solo lectura (
READ): puede leer el log (solicitudesGET) — suficiente para mostrar tus QSO en un sitio web. - Lectura y escritura (
WRITE): también puede crear, actualizar y eliminar QSO — necesaria para una sincronización bidireccional con un programa de registro. - La clave completa se muestra una sola vez, justo después de crearla: cópiala de inmediato. PingDX solo conserva una huella de ella y no puede volver a mostrarla.
- Puedes revocar una clave en cualquier momento: deja de funcionar al instante. Máximo 10 claves activas por miembro.
Seguridad
- Trata una clave como una contraseña. Nunca pongas una clave WRITE en el código público de un sitio web (JavaScript enviado a los navegadores de los visitantes): cualquiera podría leerla y modificar tu log. Llama a la API desde tu servidor o usa una clave READ.
- Una clave solo abre la API del log: nunca tus mensajes, tu perfil, tu contraseña ni el resto de tu cuenta.
- Usa una clave por programa, así puedes revocar una sin romper las demás. Las claves empiezan por
pdx_…: es fácil detectarlas si una se publica por error — revócala entonces.
Autenticación
Envía la clave en cada solicitud, en la cabecera Authorization (Bearer) o en la cabecera X-API-Key. Tu sesión de PingDX no es aceptada por esta API y nunca se usan cookies.
Authorization: Bearer pdx_…
# o
X-API-Key: pdx_…Sin una clave válida la API responde 401; una clave READ que intenta escribir recibe 403:
401 API_KEYS.INVALIDClave ausente, desconocida o revocada (o cuenta que ya no está activa).403 API_KEYS.READ_ONLYLa clave es de solo lectura: crea una clave WRITE para escribir.
Endpoints
Todas las rutas son relativas a la URL base. Cada QSO se identifica por su clientId, un UUID elegido por tu programa.
GET/me
Permisos necesarios: clave READ o WRITE
El miembro al que pertenece la clave (indicativo, locator) y los permisos de la clave. Útil para comprobar una clave.
Respuesta
{
"callsign": "14KM001",
"locator": "JN03ql",
"scope": "WRITE"
}Errores
401 API_KEYS.INVALIDClave ausente, desconocida o revocada (o cuenta que ya no está activa).429 GENERIC.RATE_LIMITEDDemasiadas solicitudes: espera un poco y vuelve a intentarlo.
GET/logs
Permisos necesarios: clave READ o WRITE
El log, primero el cambio más antiguo (ordenado por la última escritura en el servidor), página a página.
Sin updatedSince obtienes todo el log. Con él, solo los QSO creados, modificados o con confirmación cruzada después de ese instante.
Parámetros
updatedSincestring (ISO 8601)queryopcionalFecha y hora ISO 8601 con zona horaria (p. ej.
2026-10-01T00:00:00Z). Pasa elserverTimede tu sincronización anterior.limitinteger 1–500 (200)queryopcionalQSO por página, de 1 a 500. Por defecto: 200.
cursorstringqueryopcionalEl
nextCursorde la página anterior, para obtener la siguiente. Mantén el mismoupdatedSincemientras paginas.
Respuesta
items: los QSO de la página (ver el objeto QSO). nextCursor: pásalo como cursor para obtener la página siguiente; null en la última página. serverTime: la hora del servidor al inicio de la solicitud — tu próximo 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"
}Errores
400 VALIDATION.FAILEDSolicitud no válida: un parámetro incorrecto, un clientId que no es un UUID, o un lote sin un array entries válido (1 a 200). details describe el problema.401 API_KEYS.INVALIDClave ausente, desconocida o revocada (o cuenta que ya no está activa).429 GENERIC.RATE_LIMITEDDemasiadas solicitudes: espera un poco y vuelve a intentarlo.
GET/logs/ids
Permisos necesarios: clave READ o WRITE
Los ids de todos los QSO del log, sin paginación. Compáralos con tu copia para encontrar los QSO eliminados en PingDX (o desde otro programa).
Respuesta
clientId puede ser null en algunos QSO muy antiguos creados antes de que existiera la sincronización.
{
"items": [
{ "id": "cmgd4w1x70003s60e8k2v9qhz", "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f" },
{ "id": "cmgd51b2c0007s60efq3m1abc", "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b" }
]
}Errores
401 API_KEYS.INVALIDClave ausente, desconocida o revocada (o cuenta que ya no está activa).429 GENERIC.RATE_LIMITEDDemasiadas solicitudes: espera un poco y vuelve a intentarlo.
POST/logs/batch
Permisos necesarios: clave WRITE
Crea o actualiza hasta 200 QSO en una solicitud. Un QSO cuyo clientId es desconocido se crea; un clientId existente actualiza ese QSO.
Cada QSO se valida por separado: un QSO no válido se indica en su resultado y nunca impide guardar los demás. La solicitud solo falla en su conjunto (400) cuando entries falta, está vacío o tiene más de 200.
Parámetros
entriesQSO[] (1–200)cuerpoobligatorioLos QSO a crear o actualizar (ver el objeto QSO).
Cuerpo de la solicitud
{
"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"
}
]
}Respuesta
Un resultado por QSO. id es el id del servidor, o null si el QSO fue rechazado; error da entonces el motivo. permanent: true significa que el propio QSO no es válido: será rechazado de nuevo hasta que se corrija — no lo reintentes tal cual. Un rechazo sin permanent es temporal: inténtalo de nuevo más tarde.
{
"results": [
{ "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f", "id": "cmgd4w1x70003s60e8k2v9qhz", "spottedAt": null },
{ "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b", "id": null, "error": "LOCATOR_INVALID", "permanent": true }
]
}Errores
400 VALIDATION.FAILEDSolicitud no válida: un parámetro incorrecto, un clientId que no es un UUID, o un lote sin un array entries válido (1 a 200). details describe el problema.401 API_KEYS.INVALIDClave ausente, desconocida o revocada (o cuenta que ya no está activa).403 API_KEYS.READ_ONLYLa clave es de solo lectura: crea una clave WRITE para escribir.429 GENERIC.RATE_LIMITEDDemasiadas solicitudes: espera un poco y vuelve a intentarlo.
DELETE/logs/{clientId}
Permisos necesarios: clave WRITE
Elimina un QSO de tu log por su clientId. Si se había enviado al cluster DX, el spot también se elimina.
Parámetros
clientIdstring (UUID)rutaobligatorioEl
clientIddel QSO (un UUID).
Respuesta
Sin cuerpo (204). Envía esta solicitud sin cuerpo y sin Content-Type.
HTTP/1.1 204 No ContentErrores
400 VALIDATION.FAILEDSolicitud no válida: un parámetro incorrecto, un clientId que no es un UUID, o un lote sin un array entries válido (1 a 200). details describe el problema.401 API_KEYS.INVALIDClave ausente, desconocida o revocada (o cuenta que ya no está activa).403 API_KEYS.READ_ONLYLa clave es de solo lectura: crea una clave WRITE para escribir.404 GENERIC.NOT_FOUNDNo hay ningún QSO con este clientId en tu log.429 GENERIC.RATE_LIMITEDDemasiadas solicitudes: espera un poco y vuelve a intentarlo.
GET/openapi.json
Permisos necesarios: ninguno (público)
La descripción OpenAPI 3.1 de esta API (JSON). Pública: no se necesita clave.
El objeto QSO
El mismo objeto se envía a POST /logs/batch y lo devuelve GET /logs. El campo band selecciona una de dos variantes:
- 11 m (CB) —
"band": "ELEVEN_METERS":frequencyMhz(opcional);channel,ctcssToneydcsCodedeben estar ausentes o sernull. - PMR446 —
"band": "PMR446":channel(1 a 16) es obligatorio,ctcssTone/dcsCodeopcionales;frequencyMhzdebe estar ausente o sernull. - Una actualización reemplaza el QSO: envía siempre el QSO completo. Un campo omitido se restablece (vacío, o
falseparaqslSent/qslReceived), salvooperatorCallsign,dxLat,dxLng,txPowerW,radioyantenna, que conservan su valor almacenado cuando se omiten. - Se recortan los espacios del texto, los locators se normalizan (
jn18DU→JN18du) y los campos desconocidos se ignoran. Los campos de solo lectura los establece el servidor: no hace falta enviarlos.
clientIdobligatoriostring (UUID)Tu id estable para este QSO: un UUID aleatorio (p. ej.
crypto.randomUUID()), generado una sola vez y guardado con el QSO en tu programa. Es la clave de sincronización.bandobligatorio"ELEVEN_METERS" | "PMR446"La banda:
ELEVEN_METERS(11 m / CB) oPMR446.qsoAtobligatoriostring (ISO 8601)Fecha y hora del QSO, ISO 8601 con zona horaria (
Zo+02:00). Se almacena y devuelve en UTC.frequencyMhzopcionalstring "27.555" · 26.000–28.000Solo 11 m: la frecuencia en MHz, como cadena con exactamente 3 decimales, entre 26.000 y 28.000.
channelobligatorio en PMR446integer 1–16Solo PMR446 (obligatorio allí): el canal, de 1 a 16.
ctcssToneopcionalstring "67.0" · ^\d{2,3}\.\d$Solo PMR446: tono CTCSS en Hz, un decimal (
67.0,103.5).dcsCodeopcionalstring "D023N" · ^D?[0-7]{3}[NI]?$Solo PMR446: código DCS, 3 dígitos octales con prefijo
Dopcional y polaridadN/I(D023N).modeopcional"FM" | "AM" | "SSB" | "USB" | "LSB" | "CW" | "DIGITAL"El modo.
callsignWorkedopcionalstring ≤ 20El indicativo de la estación contactada.
operatorCallsignopcionalstring ≤ 20Tu propio indicativo para este QSO (útil si usas varios). Se conserva si se omite en una actualización.
dxLocatoropcionalstring "JN18" | "JN18du"Locator Maidenhead de la estación contactada, 4 o 6 caracteres.
dxLatopcionalnumber −90…90Latitud exacta de la estación contactada, si se conoce. Los mapas la prefieren al locator.
dxLngopcionalnumber −180…180Longitud exacta de la estación contactada, si se conoce.
txPowerWopcionalinteger 0–100000Tu potencia de transmisión en vatios enteros.
myLocatoropcionalstring "JN03" | "JN03ql"Tu locator para este QSO (p. ej. en portable), 4 o 6 caracteres.
splitKhzopcionalinteger −99999…99999Split en kHz (diferencia entre las frecuencias de transmisión y recepción).
pathopcional"SP" | "LP"Camino de propagación: camino corto (
SP) o camino largo (LP).qsoStatusopcional"HRD" | "WKD" | "CFM"Estado del contacto: escuchado (
HRD), trabajado (WKD), confirmado (CFM).rstSentopcionalstring ≤ 10Reporte enviado (p. ej.
59).rstReceivedopcionalstring ≤ 10Reporte recibido.
qslSentopcionalboolean (false)Tarjeta QSL enviada. Por defecto:
false.qslReceivedopcionalboolean (false)Tarjeta QSL recibida. Por defecto:
false.qslViaopcional"DIRECT" | "BUREAU" | "EQSL" | "ECARD"Cómo viajó la QSL:
DIRECT(correo),BUREAU,EQSL(eQSL.cc) oECARD(otra tarjeta electrónica).notesopcionalstring ≤ 2000Notas libres.
radioopcionalstring ≤ 80El equipo de radio usado (texto libre).
antennaopcionalstring ≤ 80La antena usada (texto libre).
sendSpotopcionalbooleanSolo escritura, nunca se devuelve:
truepublica además el QSO como spot en el cluster DX de PingDX (una vez por QSO). Solo para QSO en directo, nunca para importaciones de QSO antiguos.idsolo lecturastringEl id del QSO en el servidor.
crossConfirmedAtsolo lecturastring (ISO 8601) | nullLo establece PingDX cuando la otra estación registró el mismo QSO en PingDX (confirmación cruzada).
spottedAtsolo lecturastring (ISO 8601) | nullCuándo se publicó el QSO como spot en el cluster, o
null.createdAtsolo lecturastring (ISO 8601)Cuándo se guardó el QSO por primera vez en PingDX.
syncedAtsolo lecturastring (ISO 8601)Última escritura del QSO en el servidor (ordena
GET /logs).
Guía de sincronización
Una sincronización bidireccional robusta entre tu programa y PingDX en seis pasos:
- 1
Primera descarga completa
Llama a
GET /logs?limit=500sinupdatedSincey siguenextCursorhasta que seanull. Guarda elserverTimede la primera página. - 2
Descarga incremental
La próxima vez, llama a
GET /logs?updatedSince=…con elserverTimeguardado, sigue las páginas igual y guarda el nuevoserverTime(siempre el de la primera página) solo cuando todas las páginas hayan sido procesadas. Recibir un QSO dos veces es inofensivo: aplícalo porclientId. - 3
Enviar tus QSO
Asigna a cada QSO de tu programa un UUID como
clientIduna vez por todas y guárdalo. Envía los QSO nuevos o modificados conPOST /logs/batch, como máximo 200 por solicitud. Volver a enviar el mismo QSO es seguro: lo actualiza en lugar de crear un duplicado. - 4
QSO rechazados
Lee cada resultado: si
idesnullconpermanent: true, muestra el QSO al usuario para que lo corrija (errorindica el motivo) en lugar de reintentarlo. Sinpermanent, reintenta más tarde. - 5
Eliminaciones
Para eliminar en PingDX:
DELETE /logs/{clientId}. Para encontrar los QSO eliminados en PingDX:GET /logs/idsy luego elimina de tu copia cada QSO cuyoclientIdya no aparezca en la lista. - 6
Conflictos
Gana la última escritura, por
clientId: no hay fusión campo a campo. La app de PingDX recoge los QSO escritos mediante la API en su próxima sincronización (al abrirse, o en pocos segundos si está en línea).
Errores y límites
Los errores usan el código de estado HTTP y un cuerpo JSON con un código estable (nunca un texto traducido):
{ "error": { "code": "API_KEYS.READ_ONLY" } }400 VALIDATION.FAILEDSolicitud no válida: un parámetro incorrecto, un clientId que no es un UUID, o un lote sin un array entries válido (1 a 200). details describe el problema.
401 API_KEYS.INVALIDClave ausente, desconocida o revocada (o cuenta que ya no está activa).
403 API_KEYS.READ_ONLYLa clave es de solo lectura: crea una clave WRITE para escribir.
404 GENERIC.NOT_FOUNDNo hay ningún QSO con este clientId en tu log.
429 GENERIC.RATE_LIMITEDDemasiadas solicitudes: espera un poco y vuelve a intentarlo.
500 GENERIC.INTERNAL_ERRORError del servidor: inténtalo de nuevo más tarde.
Motivos de rechazo por QSO
En POST /logs/batch, un QSO rechazado tiene id: null y un código error:
CLIENT_ID_INVALIDclientId no es un UUID válido.DATETIME_INVALIDqsoAt no es una fecha y hora ISO 8601 con zona horaria.LOCATOR_INVALIDdxLocator o myLocator no es un locator Maidenhead de 4 o 6 caracteres.FREQUENCY_FORMAT_INVALIDfrequencyMhz debe ser una cadena con 3 decimales (27.555).FREQUENCY_OUT_OF_BAND_11MfrequencyMhz está fuera de 26.000–28.000 MHz.CHANNEL_OUT_OF_RANGECanal PMR446 fuera de 1–16.CTCSS_FORMAT_INVALIDctcssTone debe tener el formato 67.0.DCS_FORMAT_INVALIDdcsCode debe tener el formato D023N.CLIENT_ID_CONFLICTEste clientId ya pertenece al QSO de otro miembro: genera un nuevo UUID.
Otras violaciones del esquema (campo ausente, valor desconocido en un enum, texto demasiado largo…) devuelven el mensaje del validador en inglés, p. ej. Invalid input: expected string, received undefined, también con permanent: true.
Límites
- 300 solicitudes por minuto por dirección IP; por encima,
429(la cabeceraRetry-Afterindica cuánto esperar). Agrupa tus QSO en lotes en lugar de enviarlos uno a uno. - 200 QSO como máximo por
POST /logs/batch. - 1 a 500 QSO por página de
GET /logs(200 por defecto). - 10 claves activas como máximo por miembro.
Llamada desde un navegador (CORS)
/api/v1 acepta solicitudes de cualquier origen (CORS), sin cookies ni credenciales. Una página web puede por tanto llamarla directamente — pero solo con una clave READ, ya que el código de una página pública es visible para todos.
Ejemplos
Sustituye pdx_… por tu clave. Estos ejemplos están listos para copiar; solo los comentarios están en inglés.
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)
Funciona en Node.js 18+ y Deno. Mantén la clave en el servidor (variable de entorno).
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 pregunta o un programa que quieras conectar? Escríbenos desde la página de Contacto.