API v2
API v2
目录
警告:需要 Wavelog 3.1.0 或更高版本
API v2 仅在 Wavelog 3.1.0 及更高版本中可用。如果你的实例运行的是更早的版本,请先升级。
Wavelog 的 REST API v2 是一个现代化的、面向资源的 HTTP API,面向第三方工具——电台、外部日志软件、监控系统以及你自己的脚本。它与旧版 v1 API 并行运行:v2 并不取代 v1,两者可以同时使用。
注意
API v2 需要专用的 v2 令牌(前缀为
wl2_)。旧版 v1 API 密钥在任何/api/v2/…端点上均不会被接受。
与 v1 有何不同
| 方面 | v1(/api/…) |
v2(/api/v2/…) |
|---|---|---|
| URL 风格 | RPC——操作即方法名(/api/qso) |
REST——资源路径加 HTTP 动词(POST /api/v2/qso) |
| HTTP 动词 | 大多为 POST |
GET、POST、PATCH、DELETE |
| 认证 | 密钥放在 JSON 请求体 / URL 中 | Authorization: Bearer <token> 请求头 |
| 令牌格式 | wl… |
wl2_… |
| 令牌存储 | 数据库中明文保存 | 仅保存 SHA-256 哈希 |
| 权限 | 单一的 r / rw 权限 |
按资源划分的细粒度作用域(qso:read、qso:write、…) |
| 响应 | 每个端点各不相同 | 统一的信封结构,语义由 HTTP 状态码承载 |
基础 URL
每个端点都位于 api/v2 前缀之下:
https://<your-wavelog-host>/index.php/api/v2/<resource>[/<id>]
<resource>是单数、小写的名称(qso、station、logbook、radio、statistic、confirmation、lookup、club、token)。<id>在适用情况下是单个条目的数字主键。
这就是完整的 URL 空间:包含超过这两段的路径并不存在,会返回 404 not_found,而不是忽略多余部分给出应答。结尾加一个斜杠是可以的。
/api/v2/qso ✅ list
/api/v2/qso/42 ✅ single item
/api/v2/qso/ ✅ same as /api/v2/qso
/api/v2/qso/42/notes ❌ 404 not_found
取决于你的 Web 服务器配置,你或许可以省略 index.php/ 这一段。本文档中的所有示例都包含它,以获得最大的兼容性。
可用资源
| 资源 | 路径 | 作用域 | 说明 |
|---|---|---|---|
| QSO | /api/v2/qso |
qso:* |
读取、创建、更新和删除日志通联;ADIF 导入/导出 |
| Station | /api/v2/station |
station:* |
管理台站位置(日志配置) |
| Logbook | /api/v2/logbook |
logbook:* |
将台站位置归入日志 |
| Radio | /api/v2/radio |
radio:* |
推送并读取实时 CAT 电台状态 |
| Statistic | /api/v2/statistic |
statistic:read |
供仪表盘与监控使用的只读计数器 |
| Confirmation | /api/v2/confirmation |
confirmation:read |
列出每个 QSO 的 QSL 确认(LoTW、eQSL、QSL 卡、QRZ、Clublog) |
| Contest | /api/v2/contest |
contest:* |
管理比赛会话及其 QSO 关联 |
| Lookup | /api/v2/lookup |
lookup:read |
查询呼号(DXCC + 已通联/已确认)或网格 |
| Club | /api/v2/club |
club:read |
列出集体台的成员(仅管理员) |
| Catalog | /api/v2/catalog |
— | 实例范围的参考数据(比赛、DXCC 实体、分区) |
| Token | /api/v2/token |
— | 当前令牌的元数据(whoami) |
认证
创建令牌
- 登录 Wavelog,从用户菜单打开「API」区域。
- 新建一个 API v2 令牌。你需要选择:
- 一个名称(便于日后识别该令牌),
- 一个或多个作用域(见 作用域),
- 一个有效期:30、90 或 365 天,或永不过期。
- 完整令牌(
wl2_…)只会显示一次,就在创建之后。请立即复制——Wavelog 只保存哈希,之后永远无法再次显示。
你可以随时在同一界面吊销令牌;吊销立即生效。
警告
请像对待密码一样对待令牌。任何持有它的人都能在授予的作用域内操作你的数据。如果令牌泄露,请吊销它并创建新的令牌。
发送令牌
使用 Bearer 方案在 Authorization 请求头中发送令牌:
Authorization: Bearer wl2_your_token_here
作为备选,也接受 X-API-Key 请求头:
X-API-Key: wl2_your_token_here
注意
API 是无会话的。它从不使用你的浏览器登录——所有访问权限与数据归属都来自令牌的所有者。令牌只能查看和修改创建它的用户所拥有的数据。
在切换到某个集体台后创建的令牌归该集体台所有,它能做什么还取决于创建者在那里的权限级别。参见集体台。
作用域
权限是细粒度的,遵循 <resource>:<action> 模式:
| 动作 | 授予的操作 | 需要它的请求 |
|---|---|---|
read |
读取数据 | GET 请求 |
write |
创建和更新数据 | POST、PATCH 请求 |
delete |
删除数据 | DELETE 请求 |
各作用域相互独立,因此你可以签发只读令牌、只写令牌,或任意组合。如果某个请求的动词需要令牌未携带的作用域,该请求会被拒绝并返回 403 insufficient_scope。
创建令牌时提供的作用域全集如下:
| 作用域 | 允许的操作 |
|---|---|
qso:read / qso:write / qso:delete |
读取 / 创建+更新 / 删除 QSO |
station:read / station:write / station:delete |
读取 / 创建+更新 / 删除台站位置 |
logbook:read / logbook:write / logbook:delete |
读取 / 创建+更新 / 删除日志 |
radio:read / radio:write / radio:delete |
读取 / 创建+更新 / 删除电台 |
statistic:read |
读取统计 |
confirmation:read |
读取 QSL 确认 |
contest:read / contest:write / contest:delete |
读取 / 创建+更新 / 删除比赛会话 |
lookup:read |
查询呼号与网格 |
club:read |
读取集体台成员 |
有两个资源完全不需要作用域——任何有效令牌都可以读取它们:Token(whoami,即其自身的元数据)和 Catalog,后者提供的是实例范围的参考数据而非用户数据。
对于个人令牌,作用域是唯一的权限层。集体台令牌还会在其之上通过第二层校验:成员在该集体台的权限级别。如果令牌背后的级别低于管理员,持有 station:write 也无济于事。
请求与响应约定
HTTP 动词
| 动词 | 路径 | 含义 |
|---|---|---|
GET |
/api/v2/<resource> |
列出条目 |
GET |
/api/v2/<resource>/<id> |
获取单个条目 |
POST |
/api/v2/<resource> |
创建条目 |
PATCH |
/api/v2/<resource>/<id> |
部分更新——只修改你发送的字段 |
DELETE |
/api/v2/<resource>/<id> |
删除条目 |
注意(API 没有
PUT)更新永远是部分的。Wavelog 是你的数据的权威来源,而整体替换会让客户端把它根本不知道存在的字段清空——未来版本新增的每个字段都会被旧客户端悄悄抹掉。要完全覆盖某个条目,请在
PATCH中显式发送每一个字段。
并非每个资源都实现所有动词(例如 Radio 没有 PATCH,Statistic 是只读的)。不支持的动词会返回 405 method_not_allowed,并附带 Allow 响应头,准确列出该资源确实实现的动词:
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST, PATCH, DELETE
动词的检查先于作用域,因此向只读资源发送 POST 得到的是 405 而不是 403:它所需的作用域(lookup:write)并不存在,任何令牌都不可能持有它,所以报告缺少作用域会指向错误的问题。
请求体
- 以
Content-Type: application/json发送 JSON 格式的写入载荷。 - 请求体字段必须是标量值(字符串、数字、布尔值或
null)。嵌套对象或数组会被拒绝并返回400 validation_error。 - 格式错误的 JSON 返回
400 invalid_json。
成功响应
成功的响应使用简洁的信封结构。载荷始终位于 data 之下。每个 JSON 成功响应还包含一个 meta 块,给出通用的请求上下文:
| 字段 | 含义 |
|---|---|
timestamp |
UTC 响应时间戳(ISO-8601,例如 2026-07-18T12:34:56+00:00) |
resource |
API v2 的资源段(qso、station、statistic、…;/api/v2 和 /api/v2/status 为 meta) |
method |
HTTP 方法(GET、POST、PATCH、DELETE) |
资源特有的元数据(分页、查询详情、统计概况、…)会添加到同一个 meta 对象中。
示例:
{
"data": { "id": 42, "call": "N9EAT" },
"meta": {
"timestamp": "2026-07-18T12:34:56+00:00",
"resource": "qso",
"method": "GET"
}
}
{
"data": [ { "id": 42 }, { "id": 43 } ],
"meta": {
"timestamp": "2026-07-18T12:34:56+00:00",
"resource": "qso",
"method": "GET",
"page": 1,
"per_page": 50,
"count": 2
}
}
成功的 DELETE 返回 204 No Content,响应体为空。
错误响应
错误使用 error 信封,并配合相应的 HTTP 状态码:
{
"error": {
"code": "insufficient_scope",
"message": "Token is missing the required scope: qso:write",
"details": { "required_scope": "qso:write" }
}
}
| HTTP | code |
何时出现 |
|---|---|---|
| 400 | validation_error |
字段缺失或无效,或值为非标量 |
| 400 | invalid_json |
请求体不是合法 JSON |
| 401 | unauthorized |
未提供令牌 |
| 401 | invalid_token |
未知、已吊销或非 wl2_ 令牌(例如旧版 v1 密钥) |
| 401 | token_expired |
令牌的有效期已过 |
| 403 | insufficient_scope |
令牌缺少该动词所需的作用域 |
| 403 | forbidden |
所引用的条目不属于令牌所有者 |
| 403 | insufficient_club_permission |
集体台令牌的权限级别过低(见 details.required_level) |
| 403 | club_access_revoked |
令牌背后的集体台成员资格已被撤销 |
| 404 | not_found |
未知资源、路径或条目 id——见下方说明 |
| 405 | method_not_allowed |
该资源不支持此动词(见 Allow 响应头) |
| 409 | conflict |
重复,或当前状态不允许该操作 |
| 429 | rate_limited |
超过速率限制(见 details.retry_after,单位为秒) |
| 500 | internal_error |
意外的服务器端错误 |
注意(
404也涵盖你看不到的条目)存在但对你的令牌不可及的条目会报告为
404 not_found,而不是403。确认某一行存在本身就已经泄露了关于其他用户数据的某些信息。这就是为什么集体台成员对另一位操作员记录的 QSO 会得到404。
分页
可能返回大量行的列表端点(目前是 QSO)通过查询参数支持分页:
| 参数 | 默认值 | 说明 |
|---|---|---|
page |
1 |
从 1 开始的页码 |
per_page |
50 |
最大 5000(各格式的默认值见 QSO 资源) |
分页字段是 meta 的一部分,用于报告分页状态:
| 字段 | 含义 |
|---|---|
page |
你请求的页码 |
per_page |
每页条目数 |
count |
本页的条目数 |
total |
当前筛选条件下所有页面的条目总数 |
total_pages |
ceil(total / per_page) |
has_more |
当 page < total_pages 时为 true |
{
"data": [ { "id": 42 } ],
"meta": {
"timestamp": "2026-07-18T12:34:56+00:00",
"resource": "qso",
"method": "GET",
"page": 1,
"per_page": 50,
"count": 50,
"total": 237,
"total_pages": 5,
"has_more": true
}
}
要遍历全集,请逐页请求,直到 has_more 为 false(等价地,直到 page 达到 total_pages)。你永远不需要试探是否存在空页。
速率限制
速率限制是按需启用的:在实例所有者配置它之前并不可用,并且使用滑动窗口。超过限制时,API 返回 429 rate_limited,并附带 details.retry_after(需要等待的秒数)。如果未配置任何限制,则不做限流——但仍请只在你确实有新数据要发送或获取时才调用 API。
限流桶
每个 v2 请求最多经过两个相互独立的计数器:
| 限流桶 | 检查时机 | 计数单位 | 用途 |
|---|---|---|---|
api_v2_auth |
认证之前 | 客户端 IP 地址 | 减缓对令牌密钥空间的暴力破解尝试 |
api_v2_<resource> |
认证成功之后 | 令牌 | 常规使用限制,例如 api_v2_qso、api_v2_station |
持有有效令牌的客户端会通过两者。持有无效令牌的客户端永远过不了第一关——这正是关键:被拒绝的令牌永远到不了按令牌计数的桶,因此如果没有 api_v2_auth,令牌密钥空间就可能被全速探测。
api_v2_auth 故意不按资源划分。否则只要轮换 URL 中的资源(/qso、/station、/radio、…)就能绕过限制。
配置
限制项位于 application/config/config.php(Docker 安装为 application/config/docker/config.php)。每个条目设置在 window 秒内的最大 requests 数:
$config['api_rate_limits'] = [
// API v2 — bucket names carry the "api_v2_" prefix
'api_v2_auth' => ['requests' => 10, 'window' => 120],
'api_v2_qso' => ['requests' => 120, 'window' => 60],
'api_v2_lookup' => ['requests' => 60, 'window' => 60],
'api_v2_statistic' => ['requests' => 30, 'window' => 60],
// v1 endpoints keep their bare method names
'private_lookup' => ['requests' => 60, 'window' => 60],
// applies to every bucket not listed above
'default' => ['requests' => 60, 'window' => 60],
];
警告:没有
default时,未列出的限流桶不受限制限流桶按精确名称查找,然后回退到
default。如果两者都不匹配,该限流桶就完全不施加任何限制。这一点很容易搞错:只配置了
'private_lookup'的实例虽然开启了速率限制,但每一个 v2 端点——包括api_v2_auth——仍然不受限流。要么设置一个
default,要么显式列出api_v2_auth。
v1 与 v2 共用同一个配置数组,但从不冲突:v1 的限流桶以 API 方法命名(lookup、private_lookup、qso),v2 的限流桶始终带 api_v2_ 前缀。两者独立配置。
要完全关闭速率限制,请设置 $config['api_rate_limits'] = null; 或让它保持注释状态。
隐藏的用户数据
描述他人账户的端点会披露个人数据,而不同国家允许实例交出的内容各不相同。因此由实例所有者挑选要隐藏哪些字段:
$config['apiv2_hide_userdata'] = ['user_email', 'user_locator'];
列出的字段会被完全排除在响应之外——它们不会被置空、不是 null,只是键不存在。空数组(默认值)表示披露全部内容。
目前这会影响 Club 资源,它是唯一返回其他用户账户数据的资源。
始终返回且无法隐藏的字段是:
user_id
user_callsign
permission_level
可以隐藏的字段是:
user_firstname
user_lastname
user_locator
user_name
user_email
user_language
警告(致客户端):请以防御性方式处理可选字段
你无法从外部判断某个实例配置了披露哪些字段,也没有任何端点会报告这一点。请在使用某个字段之前先确认它存在,不要构建只有在实例恰好交出电子邮件地址时才有效的功能。
CORS
API 以 204 No Content 应答 OPTIONS 预检请求,并对 GET、POST、PATCH、DELETE 和 OPTIONS 方法允许跨域调用(Access-Control-Allow-Origin: *),允许的请求头为 Authorization、Content-Type 和 X-API-Key。
状态端点
GET /api/v2(以及 GET /api/v2/status)是公开的——无需令牌——很适合用来快速检查可达性:
curl https://<WAVELOG_URL>/index.php/api/v2/status
{
"data": { "name": "Wavelog API", "status": "ok" },
"meta": {
"timestamp": "2026-07-18T12:34:56+00:00",
"resource": "status",
"method": "GET"
}
}
快速开始
列出你的台站位置:
curl https://<WAVELOG_URL>/index.php/api/v2/station \
-H "Authorization: Bearer wl2_your_token_here"
在台站位置 1 上创建一次 QSO:
curl -X POST https://<WAVELOG_URL>/index.php/api/v2/qso \
-H "Authorization: Bearer wl2_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"station_profile_id": 1,
"call": "N9EAT",
"band": "20m",
"mode": "SSB",
"freq": 14075000,
"qso_date": "2026-06-16",
"time_on": "1706",
"rst_sent": "59",
"rst_rcvd": "57"
}'
完整的字段参考见各资源页面: QSO · Station · Logbook · Radio · Statistic · Contest · Lookup · Club · Catalog · Token。
如果你的令牌属于某个集体台而不是你自己的账户,也请阅读集体台——权限级别会改变其中若干资源返回的内容。