开发者
日志 API
通过其他日志程序、脚本或您的网站读取和写入您的 PingDX 日志。
本页内容
简介
PingDX API 允许会员将其 PingDX 日志与其他日志程序、脚本或自己的网站保持同步:下载 QSO、添加新的 QSO、修改或删除。它只能访问创建该密钥的会员的日志。
基础 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 请求头中发送密钥。此 API 不接受您的 PingDX 登录会话,也从不使用 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 的 id,不分页。将它们与您的副本比较,即可找出在 PingDX(或其他程序)上被删除的 QSO。
响应
对于同步功能出现之前创建的少数非常旧的 QSO,clientId 可能为 null。
{
"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。clientId 未知的 QSO 会被创建;已存在的 clientId 会更新该 QSO。
每个 QSO 单独验证:无效的 QSO 会在其结果中报告,并且绝不会阻止其他 QSO 被保存。只有当 entries 缺失、为空或超过 200 条时,整个请求才会失败(400)。
参数
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 为服务器 id;若 QSO 被拒绝则为 null,此时 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 密钥
按 clientId 删除您日志中的一个 QSO。如果它曾被发送到 DX 集群,对应的 spot 也会被移除。
参数
clientIdstring (UUID)path必填QSO 的
clientId(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您的日志中没有此 clientId 的 QSO。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。省略的字段会被重置(置空,
qslSent/qslReceived则重置为false),但operatorCallsign、dxLat、dxLng、txPowerW、radio和antenna例外,省略时它们保留已存储的值。 - 文本会去除首尾空白,网格定位会被规范化(
jn18DU→JN18du),未知字段会被忽略。只读字段由服务器设置:无需发送。
clientId必填string (UUID)您为该 QSO 设定的稳定 id:随机 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:频率(MHz),字符串,恰好 3 位小数,介于 26.000 和 28.000 之间。
channelPMR446 下必填integer 1–16仅限 PMR446(在其中必填):信道,1 到 16。
ctcssTone可选string "67.0" · ^\d{2,3}\.\d$仅限 PMR446:CTCSS 亚音频(Hz),一位小数(
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异频(kHz)(发射与接收频率之差)。
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 作为 spot 发布到 PingDX DX 集群(每个 QSO 一次)。仅用于实时 QSO,切勿用于导入旧记录。id只读stringQSO 的服务器 id。
crossConfirmedAt只读string (ISO 8601) | null当对方电台在 PingDX 上记录了同一 QSO 时由 PingDX 设置(交叉确认)。
spottedAt只读string (ISO 8601) | nullQSO 被发布为集群 spot 的时间,否则为
null。createdAt只读string (ISO 8601)QSO 首次保存到 PingDX 的时间。
syncedAt只读string (ISO 8601)QSO 最后一次在服务器端写入的时间(决定
GET /logs的排序)。
同步指南
分六步实现您的程序与 PingDX 之间可靠的双向同步:
- 1
首次完整下载
不带
updatedSince调用GET /logs?limit=500,然后沿nextCursor继续,直到其为null。保存第一页的serverTime。 - 2
增量下载
下次使用保存的
serverTime调用GET /logs?updatedSince=…,以同样方式翻页,并且只有在所有页面都处理完毕后,才保存新的serverTime(同样取自第一页)。重复收到同一个 QSO 无害:按clientId应用即可。 - 3
发送您的 QSO
为程序中的每个 QSO 一次性分配并保存一个 UUID
clientId。使用POST /logs/batch发送新增或修改的 QSO,每个请求最多 200 个。重复发送同一个 QSO 是安全的:它会更新而不是创建重复项。 - 4
被拒绝的 QSO
检查每个结果:如果
id为null且permanent: true,请将该 QSO 展示给用户修正(error说明原因),而不是重试。没有permanent时,请稍后重试。 - 5
删除
在 PingDX 上删除:
DELETE /logs/{clientId}。要找出在 PingDX 上被删除的 QSO:调用GET /logs/ids,然后从您的副本中移除所有clientId已不在列表中的 QSO。 - 6
冲突
按每个
clientId,最后一次写入生效:不会逐字段合并。PingDX 应用会在下次同步时获取通过 API 写入的 QSO(打开时,或联网时在几秒内)。
错误与限制
错误使用 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您的日志中没有此 clientId 的 QSO。
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 MHz 范围。CHANNEL_OUT_OF_RANGEPMR446 信道超出 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。
限制
- 每个 IP 地址每分钟 300 个请求,超出则返回
429(Retry-After请求头会告知需等待多久)。请将 QSO 分批发送,而不是逐个发送。 - 每个
POST /logs/batch最多 200 个 QSO。 GET /logs每页 1 到 500 个 QSO(默认 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"]}有问题,或想接入某个程序?请通过“联系”页面写信给我们。