Desenvolvedores
API do log
Leia e grave seu log do PingDX a partir de outro programa de log, de um script ou do seu site.
Nesta página
Introdução
A API do PingDX permite que um membro mantenha seu log do PingDX sincronizado com outro programa de log, um script ou o próprio site: baixar QSOs, adicionar novos, corrigi-los ou excluí-los. Ela só dá acesso ao log do membro que criou a chave.
URL base
https://pingdx.org/api/v1Requisições e respostas são em JSON (UTF-8). Envie Content-Type: application/json em toda requisição que tenha corpo. As datas são strings ISO 8601; o servidor sempre responde em UTC (2026-10-05T18:42:00.000Z).
Uma descrição legível por máquina (OpenAPI 3.1, pública, sem necessidade de chave) está disponível para geradores de código e ferramentas como o Postman: https://pingdx.org/api/v1/openapi.json
Obter uma chave
Cada membro cria suas próprias chaves em Perfil → Acesso à API. Dê a cada chave um nome (o programa ou site que a usa) e escolha seus direitos:
- Somente leitura (
READ): pode ler o log (requisiçõesGET) — suficiente para exibir seus QSOs em um site. - Leitura e escrita (
WRITE): também pode criar, atualizar e excluir QSOs — necessária para uma sincronização bidirecional com um programa de log. - A chave completa é exibida uma única vez, logo após a criação: copie-a imediatamente. O PingDX guarda apenas uma impressão digital dela e não consegue mostrá-la novamente.
- Você pode revogar uma chave a qualquer momento: ela deixa de funcionar imediatamente. No máximo 10 chaves ativas por membro.
Segurança
- Trate uma chave como uma senha. Nunca coloque uma chave WRITE no código público de um site (JavaScript enviado aos navegadores dos visitantes): qualquer pessoa poderia lê-la e modificar seu log. Chame a API a partir do seu servidor ou use uma chave READ.
- Uma chave abre apenas a API do log: nunca suas mensagens, seu perfil, sua senha ou o restante da sua conta.
- Use uma chave por programa, para poder revogar uma sem afetar as outras. As chaves começam com
pdx_…: fáceis de identificar se uma for publicada por engano — nesse caso, revogue-a.
Autenticação
Envie a chave em toda requisição, no header Authorization (Bearer) ou no header X-API-Key. Sua sessão de login do PingDX não é aceita por esta API, e cookies nunca são usados.
Authorization: Bearer pdx_…
# ou
X-API-Key: pdx_…Sem uma chave válida, a API responde 401; uma chave READ que tente gravar recebe 403:
401 API_KEYS.INVALIDChave ausente, desconhecida ou revogada (ou conta não mais ativa).403 API_KEYS.READ_ONLYA chave é somente leitura: crie uma chave WRITE para gravar.
Endpoints
Todos os caminhos são relativos à URL base. Cada QSO é identificado pelo seu clientId, um UUID escolhido pelo seu programa.
GET/me
Direitos necessários: chave READ ou WRITE
O membro dono da chave (indicativo, locator) e os direitos da chave. Útil para verificar uma chave.
Resposta
{
"callsign": "14KM001",
"locator": "JN03ql",
"scope": "WRITE"
}Erros
401 API_KEYS.INVALIDChave ausente, desconhecida ou revogada (ou conta não mais ativa).429 GENERIC.RATE_LIMITEDRequisições demais: aguarde um pouco e tente novamente.
GET/logs
Direitos necessários: chave READ ou WRITE
O log, da alteração mais antiga para a mais recente (ordenado pela última gravação no servidor), página por página.
Sem updatedSince, você recebe o log inteiro. Com ele, apenas os QSOs criados, modificados ou com confirmação cruzada após esse instante.
Parâmetros
updatedSincestring (ISO 8601)queryopcionalData e hora ISO 8601 com fuso horário (ex.:
2026-10-01T00:00:00Z). Passe oserverTimeda sua sincronização anterior.limitinteger 1–500 (200)queryopcionalQSOs por página, de 1 a 500. Padrão: 200.
cursorstringqueryopcionalO
nextCursorda página anterior, para obter a seguinte. Mantenha o mesmoupdatedSincedurante a paginação.
Resposta
items: os QSOs da página (veja o objeto QSO). nextCursor: passe-o como cursor para obter a próxima página; null na última página. serverTime: a hora do servidor no início da requisição — seu 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"
}Erros
400 VALIDATION.FAILEDRequisição inválida: um parâmetro incorreto, um clientId que não é um UUID, ou um batch sem um array entries válido (1 a 200). details descreve o problema.401 API_KEYS.INVALIDChave ausente, desconhecida ou revogada (ou conta não mais ativa).429 GENERIC.RATE_LIMITEDRequisições demais: aguarde um pouco e tente novamente.
GET/logs/ids
Direitos necessários: chave READ ou WRITE
Os ids de todos os QSOs do log, sem paginação. Compare-os com a sua cópia para encontrar os QSOs excluídos no PingDX (ou por outro programa).
Resposta
clientId pode ser null em alguns QSOs muito antigos, criados antes de a sincronização existir.
{
"items": [
{ "id": "cmgd4w1x70003s60e8k2v9qhz", "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f" },
{ "id": "cmgd51b2c0007s60efq3m1abc", "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b" }
]
}Erros
401 API_KEYS.INVALIDChave ausente, desconhecida ou revogada (ou conta não mais ativa).429 GENERIC.RATE_LIMITEDRequisições demais: aguarde um pouco e tente novamente.
POST/logs/batch
Direitos necessários: chave WRITE
Cria ou atualiza até 200 QSOs em uma única requisição. Um QSO cujo clientId é desconhecido é criado; um clientId existente atualiza esse QSO.
Cada QSO é validado individualmente: um QSO inválido é indicado no seu próprio resultado e nunca impede que os outros sejam salvos. A requisição só falha como um todo (400) quando entries está ausente, vazio ou tem mais de 200 itens.
Parâmetros
entriesQSO[] (1–200)corpoobrigatórioOs QSOs a criar ou atualizar (veja o objeto QSO).
Corpo da requisição
{
"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"
}
]
}Resposta
Um resultado por QSO. id é o id do servidor, ou null se o QSO foi rejeitado; error informa então o motivo. permanent: true significa que o próprio QSO é inválido: ele será rejeitado novamente até ser corrigido — não tente reenviá-lo como está. Uma rejeição sem permanent é temporária: tente novamente mais 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 }
]
}Erros
400 VALIDATION.FAILEDRequisição inválida: um parâmetro incorreto, um clientId que não é um UUID, ou um batch sem um array entries válido (1 a 200). details descreve o problema.401 API_KEYS.INVALIDChave ausente, desconhecida ou revogada (ou conta não mais ativa).403 API_KEYS.READ_ONLYA chave é somente leitura: crie uma chave WRITE para gravar.429 GENERIC.RATE_LIMITEDRequisições demais: aguarde um pouco e tente novamente.
DELETE/logs/{clientId}
Direitos necessários: chave WRITE
Exclui um QSO do seu log pelo seu clientId. Se ele tinha sido enviado ao cluster DX, o spot também é removido.
Parâmetros
clientIdstring (UUID)caminhoobrigatórioO
clientIddo QSO (um UUID).
Resposta
Sem corpo (204). Envie esta requisição sem corpo e sem Content-Type.
HTTP/1.1 204 No ContentErros
400 VALIDATION.FAILEDRequisição inválida: um parâmetro incorreto, um clientId que não é um UUID, ou um batch sem um array entries válido (1 a 200). details descreve o problema.401 API_KEYS.INVALIDChave ausente, desconhecida ou revogada (ou conta não mais ativa).403 API_KEYS.READ_ONLYA chave é somente leitura: crie uma chave WRITE para gravar.404 GENERIC.NOT_FOUNDNenhum QSO com este clientId no seu log.429 GENERIC.RATE_LIMITEDRequisições demais: aguarde um pouco e tente novamente.
GET/openapi.json
Direitos necessários: nenhum (público)
A descrição OpenAPI 3.1 desta API (JSON). Pública: não precisa de chave.
O objeto QSO
O mesmo objeto é enviado para POST /logs/batch e retornado por GET /logs. O campo band seleciona uma das duas variantes:
- 11 m (CB) —
"band": "ELEVEN_METERS":frequencyMhz(opcional);channel,ctcssToneedcsCodedevem estar ausentes ou sernull. - PMR446 —
"band": "PMR446":channel(1 a 16) é obrigatório,ctcssTone/dcsCodeopcionais;frequencyMhzdeve estar ausente ou sernull. - Uma atualização substitui o QSO: envie sempre o QSO completo. Um campo omitido é redefinido (vazio, ou
falseparaqslSent/qslReceived), excetooperatorCallsign,dxLat,dxLng,txPowerW,radioeantenna, que mantêm o valor armazenado quando omitidos. - O texto é aparado, os locators são normalizados (
jn18DU→JN18du) e campos desconhecidos são ignorados. Os campos somente leitura são definidos pelo servidor: não é preciso enviá-los.
clientIdobrigatóriostring (UUID)Seu id estável para este QSO: um UUID aleatório (ex.:
crypto.randomUUID()), gerado uma única vez e armazenado com o QSO no seu programa. É a chave de sincronização.bandobrigatório"ELEVEN_METERS" | "PMR446"A banda:
ELEVEN_METERS(11 m / CB) ouPMR446.qsoAtobrigatóriostring (ISO 8601)Data e hora do QSO, ISO 8601 com fuso horário (
Zou+02:00). Armazenado e retornado em UTC.frequencyMhzopcionalstring "27.555" · 26.000–28.000Somente 11 m: a frequência em MHz, como string com exatamente 3 casas decimais, entre 26.000 e 28.000.
channelobrigatório no PMR446integer 1–16Somente PMR446 (obrigatório nele): o canal, de 1 a 16.
ctcssToneopcionalstring "67.0" · ^\d{2,3}\.\d$Somente PMR446: tom CTCSS em Hz, com uma casa decimal (
67.0,103.5).dcsCodeopcionalstring "D023N" · ^D?[0-7]{3}[NI]?$Somente PMR446: código DCS, 3 dígitos octais com prefixo
Dopcional e polaridadeN/I(D023N).modeopcional"FM" | "AM" | "SSB" | "USB" | "LSB" | "CW" | "DIGITAL"O modo.
callsignWorkedopcionalstring ≤ 20O indicativo da estação contatada.
operatorCallsignopcionalstring ≤ 20Seu próprio indicativo neste QSO (útil se você usa vários). Mantido quando omitido em uma atualização.
dxLocatoropcionalstring "JN18" | "JN18du"Locator Maidenhead da estação contatada, com 4 ou 6 caracteres.
dxLatopcionalnumber −90…90Latitude exata da estação contatada, se conhecida. Os mapas a preferem ao locator.
dxLngopcionalnumber −180…180Longitude exata da estação contatada, se conhecida.
txPowerWopcionalinteger 0–100000Sua potência de transmissão em watts inteiros.
myLocatoropcionalstring "JN03" | "JN03ql"Seu locator neste QSO (ex.: em operação portátil), com 4 ou 6 caracteres.
splitKhzopcionalinteger −99999…99999Split em kHz (diferença entre as frequências de transmissão e de recepção).
pathopcional"SP" | "LP"Caminho de propagação: short path (
SP) ou long path (LP).qsoStatusopcional"HRD" | "WKD" | "CFM"Status do contato: ouvido (
HRD), contatado (WKD), confirmado (CFM).rstSentopcionalstring ≤ 10Relatório enviado (ex.:
59).rstReceivedopcionalstring ≤ 10Relatório recebido.
qslSentopcionalboolean (false)QSL enviado. Padrão:
false.qslReceivedopcionalboolean (false)QSL recebido. Padrão:
false.qslViaopcional"DIRECT" | "BUREAU" | "EQSL" | "ECARD"Como o QSL foi enviado:
DIRECT(correio),BUREAU,EQSL(eQSL.cc) ouECARD(outro cartão eletrônico).notesopcionalstring ≤ 2000Notas livres.
radioopcionalstring ≤ 80O rádio utilizado (texto livre).
antennaopcionalstring ≤ 80A antena utilizada (texto livre).
sendSpotopcionalbooleanSomente escrita, nunca retornado:
truetambém publica o QSO como spot no cluster DX do PingDX (uma vez por QSO). Apenas para QSOs ao vivo, nunca para importação de QSOs antigos.idsomente leiturastringO id do QSO no servidor.
crossConfirmedAtsomente leiturastring (ISO 8601) | nullDefinido pelo PingDX quando a outra estação registrou o mesmo QSO no PingDX (confirmação cruzada).
spottedAtsomente leiturastring (ISO 8601) | nullQuando o QSO foi publicado como spot no cluster, ou
null.createdAtsomente leiturastring (ISO 8601)Quando o QSO foi salvo pela primeira vez no PingDX.
syncedAtsomente leiturastring (ISO 8601)Última gravação do QSO no servidor (define a ordem de
GET /logs).
Guia de sincronização
Uma sincronização bidirecional robusta entre seu programa e o PingDX em seis etapas:
- 1
Primeiro download completo
Chame
GET /logs?limit=500semupdatedSincee siganextCursoraté que sejanull. Guarde oserverTimeda primeira página. - 2
Download incremental
Na vez seguinte, chame
GET /logs?updatedSince=…com oserverTimesalvo, percorra as páginas da mesma forma e então salve o novoserverTime(ainda o da primeira página) somente depois de processar todas as páginas. Receber um QSO duas vezes é inofensivo: aplique-o peloclientId. - 3
Envio dos seus QSOs
Atribua a cada QSO do seu programa um
clientIdUUID, uma vez por todas, e armazene-o. Envie os QSOs novos ou modificados comPOST /logs/batch, no máximo 200 por requisição. Reenviar o mesmo QSO é seguro: ele o atualiza em vez de criar uma duplicata. - 4
QSOs rejeitados
Leia cada resultado: se
idfornullcompermanent: true, mostre o QSO ao usuário para correção (errordiz o motivo) em vez de tentar de novo. Sempermanent, tente novamente mais tarde. - 5
Exclusões
Para excluir no PingDX:
DELETE /logs/{clientId}. Para encontrar QSOs excluídos no PingDX:GET /logs/idse remova da sua cópia todo QSO cujoclientIdnão esteja mais na lista. - 6
Conflitos
A última gravação vence, por
clientId: não há mesclagem campo a campo. O app PingDX recebe os QSOs gravados pela API na sua próxima sincronização (ao abrir, ou em alguns segundos quando está online).
Erros e limites
Os erros usam o código de status HTTP e um corpo JSON com um código estável (nunca um texto traduzido):
{ "error": { "code": "API_KEYS.READ_ONLY" } }400 VALIDATION.FAILEDRequisição inválida: um parâmetro incorreto, um clientId que não é um UUID, ou um batch sem um array entries válido (1 a 200). details descreve o problema.
401 API_KEYS.INVALIDChave ausente, desconhecida ou revogada (ou conta não mais ativa).
403 API_KEYS.READ_ONLYA chave é somente leitura: crie uma chave WRITE para gravar.
404 GENERIC.NOT_FOUNDNenhum QSO com este clientId no seu log.
429 GENERIC.RATE_LIMITEDRequisições demais: aguarde um pouco e tente novamente.
500 GENERIC.INTERNAL_ERRORErro do servidor: tente novamente mais tarde.
Motivos de rejeição por QSO
Em POST /logs/batch, um QSO rejeitado tem id: null e um código error:
CLIENT_ID_INVALIDclientId não é um UUID válido.DATETIME_INVALIDqsoAt não é uma data e hora ISO 8601 com fuso horário.LOCATOR_INVALIDdxLocator ou myLocator não é um locator Maidenhead de 4 ou 6 caracteres.FREQUENCY_FORMAT_INVALIDfrequencyMhz deve ser uma string com 3 casas decimais (27.555).FREQUENCY_OUT_OF_BAND_11MfrequencyMhz está fora de 26.000–28.000 MHz.CHANNEL_OUT_OF_RANGECanal PMR446 fora de 1–16.CTCSS_FORMAT_INVALIDctcssTone deve ter o formato 67.0.DCS_FORMAT_INVALIDdcsCode deve ter o formato D023N.CLIENT_ID_CONFLICTEste clientId já pertence ao QSO de outro membro: gere um novo UUID.
Outras violações do esquema (campo ausente, valor desconhecido em um enum, texto longo demais…) retornam a mensagem do validador em inglês, ex.: Invalid input: expected string, received undefined, também com permanent: true.
Limites
- 300 requisições por minuto por endereço IP; acima disso,
429(o headerRetry-Afterinforma quanto esperar). Agrupe seus QSOs em batches em vez de enviá-los um a um. - No máximo 200 QSOs por
POST /logs/batch. - 1 a 500 QSOs por página de
GET /logs(200 por padrão). - No máximo 10 chaves ativas por membro.
Chamada a partir de um navegador (CORS)
/api/v1 aceita requisições de qualquer origem (CORS), sem cookies nem credenciais. Uma página web pode, portanto, chamá-la diretamente — mas só com uma chave READ, já que o código de uma página pública é visível para todos.
Exemplos
Substitua pdx_… pela sua chave. Estes exemplos estão prontos para copiar; apenas os comentários estão em 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 no Node.js 18+ e no Deno. Mantenha a chave no lado do servidor (variável de 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"]}Uma dúvida ou um programa que você gostaria de conectar? Escreva para nós pela página de Contato.