Разработчикам
API журнала
Читайте и записывайте свой журнал PingDX из другой программы ведения журнала, скрипта или вашего сайта.
На этой странице
Введение
API PingDX позволяет участнику синхронизировать свой журнал PingDX с другой программой ведения журнала, скриптом или собственным сайтом: загружать QSO, добавлять новые, исправлять или удалять их. API даёт доступ только к журналу участника, создавшего ключ.
Базовый URL
https://pingdx.org/api/v1Запросы и ответы — в формате JSON (UTF-8). Передавайте Content-Type: application/json с каждым запросом, имеющим тело. Даты — строки ISO 8601; сервер всегда отвечает в UTC (2026-10-05T18:42:00.000Z).
Машиночитаемое описание (OpenAPI 3.1, общедоступное, ключ не нужен) предназначено для генераторов кода и инструментов вроде Postman: https://pingdx.org/api/v1/openapi.json
Получение ключа
Каждый участник создаёт свои ключи в разделе Профиль → Доступ к API. Дайте ключу имя (программа или сайт, которые его используют) и выберите права:
- Только чтение (
READ): позволяет читать журнал (запросыGET) — достаточно, чтобы показывать ваши QSO на сайте. - Чтение и запись (
WRITE): позволяет также создавать, изменять и удалять QSO — нужно для двусторонней синхронизации с программой ведения журнала. - Полный ключ показывается только один раз, сразу после создания: скопируйте его немедленно. PingDX хранит лишь его отпечаток и не сможет показать ключ повторно.
- Ключ можно отозвать в любой момент: он сразу перестаёт работать. Не более 10 активных ключей на участника.
Безопасность
- Относитесь к ключу как к паролю. Никогда не помещайте ключ WRITE в публичный код сайта (JavaScript, отправляемый в браузеры посетителей): любой сможет его прочитать и изменить ваш журнал. Вызывайте API со своего сервера или используйте ключ READ.
- Ключ открывает только API журнала: никогда не сообщения, профиль, пароль или остальную часть вашей учётной записи.
- Используйте по одному ключу на программу, чтобы можно было отозвать один, не нарушив работу остальных. Ключи начинаются с
pdx_…: это легко заметить, если ключ случайно опубликован — тогда отзовите его.
Аутентификация
Передавайте ключ с каждым запросом в заголовке Authorization (Bearer) или в заголовке X-API-Key. Сессия входа в PingDX этим API не принимается, cookie никогда не используются.
Authorization: Bearer pdx_…
# или
X-API-Key: pdx_…Без действительного ключа API отвечает 401; ключ READ, пытающийся выполнить запись, получает 403:
401 API_KEYS.INVALIDКлюч отсутствует, неизвестен или отозван (или учётная запись больше не активна).403 API_KEYS.READ_ONLYКлюч только для чтения: для записи создайте ключ WRITE.
Эндпоинты
Все пути указаны относительно базового URL. Каждое QSO идентифицируется своим clientId — UUID, выбираемым вашей программой.
GET/me
Необходимые права: ключ READ или WRITE
Участник, которому принадлежит ключ (позывной, локатор), и права ключа. Удобно для проверки ключа.
Ответ
{
"callsign": "14KM001",
"locator": "JN03ql",
"scope": "WRITE"
}Ошибки
401 API_KEYS.INVALIDКлюч отсутствует, неизвестен или отозван (или учётная запись больше не активна).429 GENERIC.RATE_LIMITEDСлишком много запросов: немного подождите и повторите.
GET/logs
Необходимые права: ключ READ или WRITE
Журнал, сначала самые старые изменения (порядок по последней записи на сервере), постранично.
Без updatedSince вы получаете весь журнал. С ним — только QSO, созданные, изменённые или перекрёстно подтверждённые после этого момента.
Параметры
updatedSincestring (ISO 8601)queryнеобязательноДата и время ISO 8601 с часовым поясом (например,
2026-10-01T00:00:00Z). ПередайтеserverTimeпредыдущей синхронизации.limitinteger 1–500 (200)queryнеобязательноQSO на страницу, от 1 до 500. По умолчанию: 200.
cursorstringqueryнеобязательноnextCursorпредыдущей страницы, чтобы получить следующую. При постраничной загрузке сохраняйте тот жеupdatedSince.
Ответ
items: QSO страницы (см. объект QSO). nextCursor: передайте его как cursor, чтобы получить следующую страницу; null на последней странице. serverTime: время сервера в начале запроса — ваш следующий 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"
}Ошибки
400 VALIDATION.FAILEDНеверный запрос: неправильный параметр, clientId не в формате UUID или пакет без допустимого массива entries (от 1 до 200). details описывает проблему.401 API_KEYS.INVALIDКлюч отсутствует, неизвестен или отозван (или учётная запись больше не активна).429 GENERIC.RATE_LIMITEDСлишком много запросов: немного подождите и повторите.
GET/logs/ids
Необходимые права: ключ READ или WRITE
Идентификаторы всех QSO журнала, без постраничной разбивки. Сравните их со своей копией, чтобы найти QSO, удалённые на PingDX (или из другой программы).
Ответ
clientId может быть null у нескольких очень старых QSO, созданных до появления синхронизации.
{
"items": [
{ "id": "cmgd4w1x70003s60e8k2v9qhz", "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f" },
{ "id": "cmgd51b2c0007s60efq3m1abc", "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b" }
]
}Ошибки
401 API_KEYS.INVALIDКлюч отсутствует, неизвестен или отозван (или учётная запись больше не активна).429 GENERIC.RATE_LIMITEDСлишком много запросов: немного подождите и повторите.
POST/logs/batch
Необходимые права: ключ WRITE
Создаёт или обновляет до 200 QSO одним запросом. QSO с неизвестным clientId создаётся; существующий clientId обновляет это QSO.
Каждое QSO проверяется отдельно: неверное QSO отмечается в своём результате и никогда не мешает сохранению остальных. Запрос отклоняется целиком (400) только если entries отсутствует, пуст или содержит больше 200 элементов.
Параметры
entriesQSO[] (1–200)bodyобязательноQSO для создания или обновления (см. объект QSO).
Тело запроса
{
"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"
}
]
}Ответ
Один результат на каждое QSO. id — идентификатор на сервере или null, если QSO отклонено; тогда error указывает причину. permanent: true означает, что само QSO недопустимо: оно будет отклоняться снова, пока не будет исправлено — не повторяйте его без изменений. Отклонение без permanent временное: повторите попытку позже.
{
"results": [
{ "clientId": "0b9e6c1e-5f3a-4d2b-9c7e-1a2b3c4d5e6f", "id": "cmgd4w1x70003s60e8k2v9qhz", "spottedAt": null },
{ "clientId": "7d1f2a90-3b4c-4e5f-8a6b-9c0d1e2f3a4b", "id": null, "error": "LOCATOR_INVALID", "permanent": true }
]
}Ошибки
400 VALIDATION.FAILEDНеверный запрос: неправильный параметр, clientId не в формате UUID или пакет без допустимого массива entries (от 1 до 200). details описывает проблему.401 API_KEYS.INVALIDКлюч отсутствует, неизвестен или отозван (или учётная запись больше не активна).403 API_KEYS.READ_ONLYКлюч только для чтения: для записи создайте ключ WRITE.429 GENERIC.RATE_LIMITEDСлишком много запросов: немного подождите и повторите.
DELETE/logs/{clientId}
Необходимые права: ключ WRITE
Удаляет QSO из вашего журнала по его clientId. Если оно было отправлено в DX-кластер, спот тоже удаляется.
Параметры
clientIdstring (UUID)pathобязательноclientIdQSO (UUID).
Ответ
Без тела (204). Отправляйте этот запрос без тела и без Content-Type.
HTTP/1.1 204 No ContentОшибки
400 VALIDATION.FAILEDНеверный запрос: неправильный параметр, clientId не в формате UUID или пакет без допустимого массива entries (от 1 до 200). details описывает проблему.401 API_KEYS.INVALIDКлюч отсутствует, неизвестен или отозван (или учётная запись больше не активна).403 API_KEYS.READ_ONLYКлюч только для чтения: для записи создайте ключ WRITE.404 GENERIC.NOT_FOUNDВ вашем журнале нет QSO с таким clientId.429 GENERIC.RATE_LIMITEDСлишком много запросов: немного подождите и повторите.
GET/openapi.json
Необходимые права: не требуются (публично)
Описание этого API в формате OpenAPI 3.1 (JSON). Общедоступно: ключ не нужен.
Объект QSO
Один и тот же объект отправляется в POST /logs/batch и возвращается GET /logs. Поле band выбирает один из двух вариантов:
- 11 m (CB) —
"band": "ELEVEN_METERS":frequencyMhz(необязательно);channel,ctcssToneиdcsCodeдолжны отсутствовать или бытьnull. - PMR446 —
"band": "PMR446":channel(от 1 до 16) обязателен,ctcssTone/dcsCodeнеобязательны;frequencyMhzдолжен отсутствовать или бытьnull. - Обновление заменяет QSO: всегда отправляйте QSO целиком. Пропущенное поле сбрасывается (пустое значение или
falseдляqslSent/qslReceived), кромеoperatorCallsign,dxLat,dxLng,txPowerW,radioиantenna, которые при пропуске сохраняют записанное значение. - Текст обрезается по краям от пробелов, локаторы нормализуются (
jn18DU→JN18du), неизвестные поля игнорируются. Поля только для чтения устанавливает сервер: отправлять их не нужно.
clientIdобязательноstring (UUID)Ваш постоянный идентификатор этого QSO: случайный UUID (например,
crypto.randomUUID()), создаётся один раз и хранится вместе с QSO в вашей программе. Это ключ синхронизации.bandобязательно"ELEVEN_METERS" | "PMR446"Диапазон:
ELEVEN_METERS(11 m / CB) илиPMR446.qsoAtобязательноstring (ISO 8601)Дата и время QSO, ISO 8601 с часовым поясом (
Zили+02:00). Хранится и возвращается в UTC.frequencyMhzнеобязательноstring "27.555" · 26.000–28.000Только 11 m: частота в МГц, строка ровно с 3 знаками после запятой, от 26.000 до 28.000.
channelобязательно для PMR446integer 1–16Только PMR446 (там обязателен): канал, от 1 до 16.
ctcssToneнеобязательноstring "67.0" · ^\d{2,3}\.\d$Только PMR446: тон CTCSS в Гц с одним знаком после запятой (
67.0,103.5).dcsCodeнеобязательноstring "D023N" · ^D?[0-7]{3}[NI]?$Только PMR446: код DCS, 3 восьмеричные цифры с необязательным префиксом
Dи полярностьюN/I(D023N).modeнеобязательно"FM" | "AM" | "SSB" | "USB" | "LSB" | "CW" | "DIGITAL"Вид излучения.
callsignWorkedнеобязательноstring ≤ 20Позывной корреспондента.
operatorCallsignнеобязательноstring ≤ 20Ваш собственный позывной для этого QSO (удобно, если у вас их несколько). Сохраняется, если пропущен при обновлении.
dxLocatorнеобязательноstring "JN18" | "JN18du"Локатор Maidenhead корреспондента, 4 или 6 символов.
dxLatнеобязательноnumber −90…90Точная широта корреспондента, если известна. Карты предпочитают её локатору.
dxLngнеобязательноnumber −180…180Точная долгота корреспондента, если известна.
txPowerWнеобязательноinteger 0–100000Ваша мощность передачи в целых ваттах.
myLocatorнеобязательноstring "JN03" | "JN03ql"Ваш локатор для этого QSO (например, при работе с выезда), 4 или 6 символов.
splitKhzнеобязательноinteger −99999…99999Сплит в кГц (разница между частотами передачи и приёма).
pathнеобязательно"SP" | "LP"Путь распространения: короткий путь (
SP) или длинный путь (LP).qsoStatusнеобязательно"HRD" | "WKD" | "CFM"Статус связи: слышал (
HRD), проведена (WKD), подтверждена (CFM).rstSentнеобязательноstring ≤ 10Переданный рапорт (например,
59).rstReceivedнеобязательноstring ≤ 10Полученный рапорт.
qslSentнеобязательноboolean (false)QSL-карточка отправлена. По умолчанию:
false.qslReceivedнеобязательноboolean (false)QSL-карточка получена. По умолчанию:
false.qslViaнеобязательно"DIRECT" | "BUREAU" | "EQSL" | "ECARD"Как шла QSL:
DIRECT(почтой),BUREAU,EQSL(eQSL.cc) илиECARD(другая электронная карточка).notesнеобязательноstring ≤ 2000Произвольные заметки.
radioнеобязательноstring ≤ 80Использованная радиостанция (свободный текст).
antennaнеобязательноstring ≤ 80Использованная антенна (свободный текст).
sendSpotнеобязательноbooleanТолько запись, никогда не возвращается:
trueдополнительно публикует QSO как спот в DX-кластере PingDX (один раз на QSO). Только для QSO в реальном времени, не для импорта старых.idтолько чтениеstringИдентификатор QSO на сервере.
crossConfirmedAtтолько чтениеstring (ISO 8601) | nullУстанавливается PingDX, когда корреспондент внёс то же QSO в журнал на PingDX (перекрёстное подтверждение).
spottedAtтолько чтениеstring (ISO 8601) | nullКогда QSO было опубликовано как спот кластера, или
null.createdAtтолько чтениеstring (ISO 8601)Когда QSO было впервые сохранено на PingDX.
syncedAtтолько чтениеstring (ISO 8601)Последняя запись QSO на сервере (определяет порядок
GET /logs).
Руководство по синхронизации
Надёжная двусторонняя синхронизация между вашей программой и PingDX в шесть шагов:
- 1
Первая полная загрузка
Вызовите
GET /logs?limit=500безupdatedSince, затем переходите поnextCursor, пока он не станетnull. СохранитеserverTimeпервой страницы. - 2
Инкрементальная загрузка
В следующий раз вызовите
GET /logs?updatedSince=…с сохранённымserverTime, пройдите по страницам так же, а новыйserverTime(снова с первой страницы) сохраните только после обработки всех страниц. Повторное получение QSO безвредно: применяйте его поclientId. - 3
Отправка ваших QSO
Присвойте каждому QSO вашей программы UUID
clientIdраз и навсегда и сохраните его. Отправляйте новые или изменённые QSO черезPOST /logs/batch, не более 200 за запрос. Повторная отправка того же QSO безопасна: оно обновляется, а не дублируется. - 4
Отклонённые QSO
Читайте каждый результат: если
idравенnullприpermanent: true, покажите QSO пользователю для исправления (errorобъясняет причину), а не повторяйте отправку. Безpermanentповторите позже. - 5
Удаления
Чтобы удалить на PingDX:
DELETE /logs/{clientId}. Чтобы найти QSO, удалённые на PingDX:GET /logs/ids, затем удалите из своей копии каждое QSO, чейclientIdбольше не в списке. - 6
Конфликты
Побеждает последняя запись для каждого
clientId: слияния по полям нет. Приложение PingDX подхватывает QSO, записанные через API, при следующей синхронизации (при открытии или в течение нескольких секунд, если есть сеть).
Ошибки и ограничения
Ошибки используют код состояния HTTP и тело JSON со стабильным кодом (никогда не переведённым текстом):
{ "error": { "code": "API_KEYS.READ_ONLY" } }400 VALIDATION.FAILEDНеверный запрос: неправильный параметр, clientId не в формате UUID или пакет без допустимого массива entries (от 1 до 200). details описывает проблему.
401 API_KEYS.INVALIDКлюч отсутствует, неизвестен или отозван (или учётная запись больше не активна).
403 API_KEYS.READ_ONLYКлюч только для чтения: для записи создайте ключ WRITE.
404 GENERIC.NOT_FOUNDВ вашем журнале нет QSO с таким clientId.
429 GENERIC.RATE_LIMITEDСлишком много запросов: немного подождите и повторите.
500 GENERIC.INTERNAL_ERRORОшибка сервера: повторите позже.
Причины отклонения отдельных QSO
В POST /logs/batch отклонённое QSO имеет id: null и код error:
CLIENT_ID_INVALIDclientId не является допустимым UUID.DATETIME_INVALIDqsoAt не является датой и временем ISO 8601 с часовым поясом.LOCATOR_INVALIDdxLocator или myLocator не является 4- или 6-символьным локатором Maidenhead.FREQUENCY_FORMAT_INVALIDfrequencyMhz должен быть строкой с 3 знаками после запятой (27.555).FREQUENCY_OUT_OF_BAND_11MfrequencyMhz вне диапазона 26.000–28.000 МГц.CHANNEL_OUT_OF_RANGEКанал PMR446 вне диапазона 1–16.CTCSS_FORMAT_INVALIDctcssTone должен иметь вид 67.0.DCS_FORMAT_INVALIDdcsCode должен иметь вид D023N.CLIENT_ID_CONFLICTЭтот clientId уже принадлежит QSO другого участника: сгенерируйте новый UUID.
Прочие нарушения схемы (отсутствующее поле, неизвестное значение перечисления, слишком длинный текст…) возвращают сообщение валидатора на английском, например Invalid input: expected string, received undefined, также с permanent: true.
Ограничения
- 300 запросов в минуту с одного IP-адреса, при превышении —
429(заголовокRetry-Afterсообщает, сколько ждать). Группируйте QSO в пакеты, а не отправляйте по одному. - Не более 200 QSO в одном
POST /logs/batch. - От 1 до 500 QSO на страницу
GET /logs(по умолчанию 200). - Не более 10 активных ключей на участника.
Вызов из браузера (CORS)
/api/v1 принимает запросы с любого источника (CORS), без cookie и учётных данных. Поэтому веб-страница может вызывать его напрямую — но только с ключом READ, так как код публичной страницы виден всем.
Примеры
Замените pdx_… своим ключом. Эти примеры готовы к копированию; только комментарии на английском.
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)
Работает в Node.js 18+ и Deno. Храните ключ на стороне сервера (переменная окружения).
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"]}Есть вопрос или программа, которую вы хотели бы подключить? Напишите нам на странице «Контакты».