Umami 3.4 API 使用详解
完整讲解 Umami 3.4.0 REST API:登录令牌与 API Key 认证、网站管理、统计查询、/api/send 与 /api/batch 埋点上报协议及错误码约定,20 余个端点请求响应逐条实测照录。
Umami 3.4 API 使用详解
目标:面向需要用脚本/程序对接 Umami 的工程师,完整讲清 Umami 3.4.0 的 REST API:认证体系(登录令牌与 API Key)、网站管理、统计数据查询、
/api/send埋点上报协议(含批量与自定义事件)、错误码与通用约定;全部请求与响应 JSON 均在 Ubuntu 22.04 LTS + Umami 3.4.0(监听 0.0.0.0:3000,本文经 http://localhost:13000 访问)+ PostgreSQL 16.15 环境实测,逐条照录。
一、环境信息
- 操作系统:Ubuntu 22.04 LTS(x86_64)
- Umami:3.4.0(官方镜像 ghcr.io/umami-software/umami:postgresql-latest)
- 数据库:PostgreSQL 16.15
- API 基地址:
http://localhost:13000(服务实际监听 0.0.0.0:3000,经端口映射访问) - 演示账号:admin / q1w2e3r4;演示网站:Demo Site(website id
bd69fdbf-f8bd-48c3-93d4-877c51c93fb4,域名 demo.example.com,分享标识 docshare1;站内已写入 14 次页面浏览、8 个独立访客与 2 个自定义事件——数据来自本系列实测上报,故路径/浏览器维度较为杂乱,下文统计回显均为实测时刻快照) - 客户端工具:curl 8.x + python3(格式化 JSON)
二、前置条件
- Umami 3.4.0 实例可访问(心跳确认):
curl -s http://localhost:13000/api/heartbeat
实测回显:
{"ok":true}
- 客户端工具就绪(curl、jq 或 python3 任一即可);
- 认识两类凭据(下文逐一实测):登录令牌(Bearer JWT,管理 API 用,624 字符)与 API Key(
umami_前缀,管理 API 用,适合脚本长期调用)。
三、API 总览
协议与风格:
- 纯 REST/JSON:请求体与响应体均为 JSON(采集端点额外接受
text/plain,见第七章);时间参数用毫秒时间戳(startAt/endAt); - 前缀:管理/统计 API 在
/api/下;采集 API 在/api/send(可改)与/api/batch;分享页面在/share/<shareId>; - 认证方式:
Authorization: Bearer <登录令牌>—— 控制台同源令牌,有效期约 1 天;Authorization: Bearer <API Key>—— API Key 同样走 Bearer 头(实测可用,见 4.4);- 采集接口
/api/send、/api/batch与公开接口(/api/config、/api/share/<id>、心跳)无需认证;
- 错误格式:统一
{"error":{"message","code","status"}},参数校验失败额外带errors/properties明细; - 限流:核心接口未见限流头与限流逻辑(实测高频率顺序调用无 429);生产对接请自行客户端节流。
常用端点一图流:
| 域 | 端点 | 方法 | 认证 |
|---|---|---|---|
| 认证 | /api/auth/login | POST | 无 |
| 认证 | /api/auth/verify | POST | 令牌 |
| 当前用户 | /api/me、/api/me/password、/api/me/api-keys | GET/POST | 令牌 |
| 网站 | /api/websites、/api/websites/{id} | GET/POST/DELETE | 令牌或 API Key |
| 统计 | `/api/websites/{id}/stats | pageviews | metrics |
| 采集 | /api/send、/api/batch | POST | 无(校验 website id 与 UA) |
| 分享 | /api/share/{shareId} | GET | 无 |
| 系统 | /api/config | GET | 无 |
四、认证与首次调用
4.1 登录换取令牌(POST /api/auth/login)
curl -s -X POST http://localhost:13000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}'
实测回显(HTTP 200;令牌实测 624 字符,此处截断):
{"token":"8s9H6W8PYDAe5/lqApPDr11TCJ7LRlJqFwO1ss8BE5iJsAInFgFVEoauqMyv...(共624字符)","user":{"id":"41e2b680-648e-4b09-bcd7-3e2b10c06264","username":"admin","role":"admin","createdAt":"2026-10-17T09:29:10.286Z","isAdmin":true,"teams":[]}}
错误口令实测(HTTP 401):
{"error":{"message":"Unauthorized","code":"incorrect-username-password","status":401}}
4.2 校验令牌(POST /api/auth/verify)
TOKEN=... # 上一步 token
curl -s -X POST http://localhost:13000/api/auth/verify -H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200;注意此端点用 GET 会返回 405,必须 POST):
{"id":"41e2b680-648e-4b09-bcd7-3e2b10c06264","username":"admin","role":"admin","createdAt":"2026-10-17T09:29:10.286Z","twoFactorRequired":false,"isAdmin":true,"teams":[]}
4.3 当前用户与会话信息(GET /api/me)
curl -s http://localhost:13000/api/me -H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200,令牌字段截断):
{"token":"uDoZVJO31CLAZB9i/37dJBY/f08JcUF6UxWCfcjRV7xua1JixVxzlETh6qjuP9V...(截断)","shareToken":null,"user":{"id":"41e2b680-648e-4b09-bcd7-3e2b10c06264","username":"admin","role":"admin","createdAt":"2026-10-17T09:29:10.286Z","twoFactorRequired":false,"isAdmin":true},"authType":"session"}
修改自己密码(POST /api/me/password,改后旧令牌全部失效需重登):
curl -s -X POST http://localhost:13000/api/me/password \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"currentPassword":"umami","newPassword":"q1w2e3r4"}'
实测回显(HTTP 200):
{"id":"41e2b680-648e-4b09-bcd7-3e2b10c06264","username":"admin","role":"admin","createdAt":"2026-10-17T09:29:10.286Z"}
4.4 API Key:创建与调用
创建(POST /api/me/api-keys;响应只返回一次完整 key):
curl -s -X POST http://localhost:13000/api/me/api-keys \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"doc-demo-key"}'
实测回显(HTTP 200;key 即 API Key,umami_ 前缀):
{"id":"f7d4dcdd-e57b-4445-a80c-08772689be5c","name":"doc-demo-key","key":"umami_qtEZCw7dSHP1bVzalFvlNxBcGE0BD48G","userId":"41e2b680-648e-4b09-bcd7-3e2b10c06264","createdAt":"2026-10-17T10:45:58.123Z","updatedAt":"2026-10-17T10:45:58.123Z"}
调用方式实测对比:
KEY=umami_qtEZCw7dSHP1bVzalFvlNxBcGE0BD48G
# 方式一:Bearer 头(实测 200)
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $KEY" http://localhost:13000/api/websites
# 方式二:x-umami-api-key 头(实测 401,3.4.0 不支持该头)
curl -s -o /dev/null -w '%{http_code}\n' -H "x-umami-api-key: $KEY" http://localhost:13000/api/websites
实测回显:
200
401
结论:3.4.0 的 API Key 走 Authorization: Bearer 头,请求头方式(x-umami-api-key)返回 401。API Key 与登录令牌等价用于管理 API(实测 /api/websites 200),适合脚本/CI 长期使用且可随时吊销。
4.5 无效凭据行为
curl -s -H "Authorization: Bearer garbage.token.xxx" http://localhost:13000/api/websites
实测回显(HTTP 401):
{"error":{"message":"Unauthorized","code":"unauthorized","status":401}}
⚠️ 注意:不同 APP_SECRET 的实例之间令牌互不认可(跨实例用 A 实例令牌请求 B 实例返回 401,实测见《Umami 3.4 配置参数详解》5.2)——多实例环境务必统一 APP_SECRET。
4.6 令牌形态与登出(实测)
很多资料把 Umami 的登录令牌描述成标准 JWT(三段式),实测并非如此——解码令牌本体(非 user 信息):
TOKEN=$(cat /tmp/umami_token)
python3 -c "t=open('/tmp/umami_token').read().strip(); print('dots:',t.count('.')); print('len:',len(t))"
实测回显:
dots: 0
len: 624
结论:3.4.0 的登录令牌是一段不透明的 base64 加密串(单段、无 . 分隔符、624 字符),不是标准 JWT,无法本地解码出过期时间,客户端只能按"有效期约 1 天"处理,过期后重新走 /api/auth/login。这与 cache(上报回执里的 JWT,三段式,可解码出 websiteId/sessionId/visitId/iat)形成鲜明对比——只有采集缓存令牌是 JWT。
登出实测(POST /api/auth/logout):
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:13000/api/auth/logout \
-H "Authorization: Bearer $TOKEN"
实测回显:200。登出后该令牌立即失效(再请求 /api/me 返回 401),需重新登录;API Key 不受 logout 影响,可继续使用。
五、网站管理 API
网站管理是脚本对接最常用的域:查列表、建站、改配置、重置数据、删站。以下全部经 Authorization: Bearer $TOKEN(或 API Key,二者等价)实测。
5.1 网站列表(GET /api/websites)
curl -s "http://localhost:13000/api/websites" -H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200,实例内现有 2 站):
{"data":[{"id":"bd69fdbf-f8bd-48c3-93d4-877c51c93fb4","name":"Demo Site","domain":"demo.example.com","resetAt":null,"userId":"41e2b680-648e-4b09-bcd7-3e2b10c06264","teamId":null,"createdBy":"41e2b680-648e-4b09-bcd7-3e2b10c06264","createdAt":"2026-10-17T11:00:12.086Z","updatedAt":"2026-10-17T12:53:21.759Z","deletedAt":null,"recorderEnabled":false,"replayConfig":null,"user":{"username":"admin","id":"41e2b680-648e-4b09-bcd7-3e2b10c06264"},"shareId":"docshare1"},{"id":"a9e10b5a-f4c0-41ec-834f-0775781b064a","name":"IP Test","domain":"ip.example.com","resetAt":null,"userId":"41e2b680-648e-4b09-bcd7-3e2b10c06264","teamId":null,"createdBy":"41e2b680-648e-4b09-bcd7-3e2b10c06264","createdAt":"2026-10-17T10:48:04.960Z","updatedAt":"2026-10-17T10:48:04.960Z","deletedAt":null,"recorderEnabled":false,"replayConfig":null,"user":{"username":"admin","id":"41e2b680-648e-4b09-bcd7-3e2b10c06264"},"shareId":null}],"count":2,"page":1,"pageSize":20,"orderBy":"name"}
要点:
- 响应为分页信封:
data(数组)+count(总数)+page/pageSize/orderBy; - 分页/排序参数实测:
?page=1&pageSize=2正常生效(默认pageSize=20、按name排序); - 列表项关键字段:
id(后续统计/上报都要用)、domain、shareId(有值即开启了公开分享)、createdAt/updatedAt、recorderEnabled(会话回放开关)。
5.2 创建网站(POST /api/websites)
curl -s -X POST http://localhost:13000/api/websites \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"API Doc Site","domain":"apidoc.example.com"}'
实测回显(HTTP 200——注意 Umami 创建成功返回 200 而非 201):
{"id":"8e2c9df9-2df4-47c4-bed2-6d50b64c7409","name":"API Doc Site","domain":"apidoc.example.com","resetAt":null,"userId":"41e2b680-648e-4b09-bcd7-3e2b10c06264","teamId":null,"createdBy":"41e2b680-648e-4b09-bcd7-3e2b10c06264","createdAt":"2026-10-17T13:32:15.817Z","updatedAt":"2026-10-17T13:32:15.817Z","deletedAt":null,"recorderEnabled":false,"replayConfig":null,"shareId":null}
必填参数校验实测(漏传 domain,HTTP 400):
{"error":{"message":"Bad request","code":"bad-request","status":400,"errors":[],"properties":{"domain":{"errors":["Invalid input: expected string, received undefined"]}}}}
5.3 网站详情(GET /api/websites/{id})
curl -s http://localhost:13000/api/websites/bd69fdbf-f8bd-48c3-93d4-877c51c93fb4 \
-H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200,字段同创建响应,多了 shareId":"docshare1"):
{"id":"bd69fdbf-f8bd-48c3-93d4-877c51c93fb4","name":"Demo Site","domain":"demo.example.com","resetAt":null,"userId":"41e2b680-648e-4b09-bcd7-3e2b10c06264","teamId":null,"createdBy":"41e2b680-648e-4b09-bcd7-3e2b10c06264","createdAt":"2026-10-17T11:00:12.086Z","updatedAt":"2026-10-17T12:53:21.759Z","deletedAt":null,"recorderEnabled":false,"replayConfig":null,"shareId":"docshare1"}
⚠️ 实测踩坑:查询不存在的网站 id 返回 HTTP 200 + null,而不是 404:
curl -s -w '\nHTTP %{http_code}\n' http://localhost:13000/api/websites/00000000-0000-0000-0000-000000000000 \
-H "Authorization: Bearer $TOKEN"
实测回显:
null
HTTP 200
脚本对接务必判空响应体,不能只判状态码。
5.4 更新网站(POST /api/websites/{id})——不是 PATCH/PUT!
这是 3.x 相对 2.x 的一个破坏性变化,实测三种方法:
WID=8e2c9df9-2df4-47c4-bed2-6d50b64c7409
# PATCH 与 PUT 均被拒绝(HTTP 405)
curl -s -o /dev/null -w 'PATCH %{http_code}\n' -X PATCH http://localhost:13000/api/websites/$WID \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"name":"x","domain":"x.com"}'
curl -s -o /dev/null -w 'PUT %{http_code}\n' -X PUT http://localhost:13000/api/websites/$WID \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"name":"x","domain":"x.com"}'
# 正确姿势:POST
curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:13000/api/websites/$WID \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"API Doc Site v2","domain":"apidoc2.example.com"}'
实测回显:
PATCH 405
PUT 405
{"id":"8e2c9df9-2df4-47c4-bed2-6d50b64c7409","name":"API Doc Site v2","domain":"apidoc2.example.com",...,"updatedAt":"2026-10-17T13:32:34.446Z","shareId":null}
shareId(公开分享标识)也可以通过同一接口设置,实测:
curl -s -X POST http://localhost:13000/api/websites/$WID \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"shareId":"apidocshare1"}'
实测回显(节选,HTTP 200):..."shareId":"apidocshare1"}。设置后即可经 /share/<shareId> 公开访问、经 /api/share/<shareId> 换取分享令牌。
5.5 重置网站数据(POST /api/websites/{id}/reset)
curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:13000/api/websites/$WID/reset \
-H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200;网站配置保留,resetAt 被写为当前时刻,后续统计从该时间起算):
{"ok":true}
5.6 删除网站(DELETE /api/websites/{id})
curl -s -w '\nHTTP %{http_code}\n' -X DELETE http://localhost:13000/api/websites/$WID \
-H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200;再次 GET /api/websites 确认列表只剩 ['Demo Site', 'IP Test']):
{"ok":true}
⚠️ 删除是软删(记录
deletedAt)但效果上立即可见地"消失",且响应只有{"ok":true}无二次确认——脚本删站前请先 GET 详情留档。
六、统计数据查询 API
统计域端点全部挂在 /api/websites/{id}/ 下,共用一组时间参数:
startAt/endAt:毫秒时间戳(必填,缺省报 400);unit:时间聚合粒度(year|month|day|hour|minute,pageviews 用);timezone:如Asia/Shanghai,影响聚合边界;comparison:previous(环比上一周期)等。
时间范围构造示例(本文取 2026-10-17 当天,Asia/Shanghai):
WID=bd69fdbf-f8bd-48c3-93d4-877c51c93fb4
START=$(python3 -c 'import datetime;print(int(datetime.datetime(2026,10,17,0,0).timestamp()*1000))')
END=$(python3 -c 'import datetime;print(int(datetime.datetime(2026,10,18,0,0).timestamp()*1000))')
6.1 汇总指标(GET /api/websites/{id}/stats)
curl -s "http://localhost:13000/api/websites/$WID/stats?startAt=$START&endAt=$END" \
-H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200):
{"pageviews":14,"visitors":8,"visits":8,"bounces":4,"totaltime":97,"comparison":{"pageviews":0,"visitors":0,"visits":0,"bounces":0,"totaltime":0}}
字段含义:pageviews 页面浏览数、visitors 独立访客(按设备指纹/会话去重)、visits 会话次数、bounces 跳出次数、totaltime 总停留时长(秒);comparison 为对比周期数据(加 &comparison=previous 后填充)。
6.2 时序页面浏览(GET /api/websites/{id}/pageviews)
curl -s "http://localhost:13000/api/websites/$WID/pageviews?startAt=$START&endAt=$END&unit=hour&timezone=Asia/Shanghai" \
-H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200,pageviews 与 sessions 两条等长时序,点格式 {"x":时间,"y":数值}):
{
"pageviews": [
{"x": "2026-10-17T19:00:00Z", "y": 10},
{"x": "2026-10-17T20:00:00Z", "y": 4}
],
"sessions": [
{"x": "2026-10-17T19:00:00Z", "y": 6},
{"x": "2026-10-17T20:00:00Z", "y": 2}
]
}
注意:虽然传了 timezone=Asia/Shanghai,回显 x 仍是 UTC ISO 串(19:00Z 即北京 03:00,属前一日测试时段),画图时客户端需自行换算时区。
6.3 维度指标(GET /api/websites/{id}/metrics)——必须带 type
metrics 是"按维度 TopN"接口,type 为必填,漏传直接 400:
{"error":{"message":"Bad request","code":"bad-request","status":400,"errors":[],"properties":{"type":{"errors":["Invalid input: expected string, received undefined"]}}}}
实测枚举出全部合法 type(13 个):
| type | 实测回显示例 |
|---|---|
path | [{"x":"/b1","y":2},{"x":"/batch-1","y":1},...] |
title | [{"x":"文档","y":1},{"x":"首页","y":1}] |
referrer | [{"x":"baidu.com","y":1},{"x":"google.com","y":1}] |
browser | [{"x":"chrome","y":5},{"x":"firefox","y":1},{"x":"safari","y":1}] |
os | [{"x":"Windows 10","y":4},{"x":"Android OS","y":1},...] |
device | [{"x":"laptop","y":4},{"x":"desktop","y":3},{"x":"mobile","y":1}] |
country / region / city | [](本实例无地理库数据,接口正常) |
language | [{"x":"zh","y":6}] |
event | [{"x":"cta-click","y":1},{"x":"doc-download","y":1}] |
screen | [{"x":"1920x1080","y":4},...] |
hostname | [{"x":"demo.example.com","y":8},{"x":"a.com","y":1}] |
⚠️ 实测踩坑:老资料常用的 type=url、type=page 在 3.4.0 不是合法值,会返回 400(Zod 枚举校验);页面路径请用 path。host、utm_source 等同样 400。可用 &limit=N 控制 TopN 条数。
6.4 事件明细(GET /api/websites/{id}/events)
curl -s "http://localhost:13000/api/websites/$WID/events?startAt=$START&endAt=$END" \
-H "Authorization: Bearer $TOKEN"
实测回显(节选,HTTP 200,逐条事件记录):
{"data":[{"id":"e45921c0-d78a-4057-8be6-bf7ba633809e","websiteId":"bd69fdbf-f8bd-48c3-93d4-877c51c93fb4","sessionId":"51903f6c-2b27-5f2d-86f2-0f254afa54a5","createdAt":"2026-10-17T12:51:54.927Z","hostname":"demo.example.com","urlPath":"/cache-reuse","device":"desktop","os":"Linux","browser":"firefox","eventType":1,"eventName":null,"hasData":false}, ...]}
要点:eventType 1 为页面浏览、2 为自定义事件(自定义事件记录 eventName,页面浏览为 null);event_data 落在 hasData/关联字段中,明细查询需进一步按会话下钻。
6.5 会话列表(GET /api/websites/{id}/sessions)
3.x 新增的会话域端点,实测:
curl -s "http://localhost:13000/api/websites/$WID/sessions?startAt=$START&endAt=$END&pageSize=2" \
-H "Authorization: Bearer $TOKEN"
实测回显(节选,HTTP 200,信封结构与网站列表一致):
{"data":[{"id":"51903f6c-2b27-5f2d-86f2-0f254afa54a5","websiteId":"bd69fdbf-f8bd-48c3-93d4-877c51c93fb4","hostname":"demo.example.com","browser":"firefox","os":"Linux","device":"desktop","screen":null,"language":null,"country":null,"firstAt":"2026-10-17T12:51:54.927Z","lastAt":"2026-10-17T12:51:54.927Z","visits":1,"views":1,"events":0,"createdAt":"2026-10-17T12:51:54.927Z"}, ...]}
6.6 实时在线(GET /api/websites/{id}/active)
curl -s "http://localhost:13000/api/websites/$WID/active" -H "Authorization: Bearer $TOKEN"
实测回显(HTTP 200;无需时间参数,统计最近 5 分钟活跃):
{"visitors":0}
6.7 统计端点鉴权实测
统计域端点同样接受 API Key(Authorization: Bearer umami_...,实测 200),也接受无认证但会 401:
curl -s -o /dev/null -w '%{http_code}\n' \
"http://localhost:13000/api/websites/$WID/stats?startAt=$START&endAt=$END"
实测回显:401(响应体 {"error":{"message":"Unauthorized","code":"unauthorized","status":401}};用 API Key 实测 200)。
七、采集上报:/api/send 与 /api/batch
采集端点无需认证,但 3.x 的协议相对 2.x 有破坏性变化,实测要点先行:
- 请求体必须带顶层
type,合法值"event" | "identify" | "performance"(缺省/非法报 400,错误信息里会列出枚举);页面浏览与自定义事件都用event; payload内website/link/pixel三选一(都缺报 400:"Exactly one of website, link, or pixel must be provided");- 请求需携带浏览器 User-Agent——无 UA 或爬虫 UA 会被 bot 识别直接跳过(实测见 7.5);
Content-Type: application/json与text/plain均可(后者用于绕过广告拦截器,实测见 7.6)。
7.1 页面浏览上报(type=event,无 event_name)
WID=bd69fdbf-f8bd-48c3-93d4-877c51c93fb4
curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:13000/api/send \
-H 'Content-Type: application/json' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36' \
-d '{"type":"event","payload":{"website":"'$WID'","hostname":"demo.example.com","language":"zh-CN","referrer":"","screen":"1920x1080","title":"API测试页","url":"/api-send-test","ip":"10.0.0.9"}}'
实测回显(HTTP 200):
{"cache":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3ZWJzaXRlSWQiOiJiZDY5ZmRiZi1mOGJkLTQ4YzMtOTNkNC04NzdjNTFjOTNmYjQiLCJzZXNzaW9uSWQiOiIwMjA5NDczMy03MzcyLTU1NTEtOGFiNS1lNmQzZjQ5MzAxZTkiLCJ2aXNpdElkIjoiMTZiZjk3OGUtMWU2NC01ZGFhLWIxYTEtZjc0ODI4MmIzOWI0IiwiaWF0IjoxNzkyMjQ0MDQyLCJ0eXBlIjoiY2FjaGUifQ.cM_FrYhJKX-NHYWb8tSR5kRgsjOPiQlubXmDoeGzFV0","sessionId":"02094733-7372-5551-8ab5-e6d3f49301e9","visitId":"16bf978e-1e64-5daa-b1a1-f748282b39b4"}
要点:
cache是一个三段式 JWT(可解码出websiteId/sessionId/visitId/iat,见 4.6),下一次上报带上它即可复用会话(payload.cache),这是服务端会话归并的机制;url、website必填;hostname/language/screen/title/referrer可选;服务端按 UA 解析 browser/os/device,按ip(或 X-Forwarded-For)解析地理。
7.2 自定义事件(type=event + event_name + event_data)
curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:13000/api/send \
-H 'Content-Type: application/json' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) Chrome/120' \
-d '{"type":"event","payload":{"website":"'$WID'","hostname":"demo.example.com","screen":"1920x1080","url":"/api-send-test","event_name":"api-doc-event","event_data":{"kind":"click","value":42}}}'
实测回显(HTTP 200,结构同 7.1,sessionId 按同一会话复用):
{"cache":"eyJ...(同上结构)","sessionId":"a72a6413-2c66-5797-b593-31cdeaba0c53","visitId":"83557c6f-7286-55a1-8f28-5442582592cd"}
带上 event_name 即为自定义事件(会上报到 events 域并在 event 维度可见);event_data 接受任意 JSON 对象(数值/字符串/嵌套均可,用于报表里的属性分析)。
7.3 identify 上报(type=identify)
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:13000/api/send \
-H 'Content-Type: application/json' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) Chrome/120' \
-d '{"type":"identify","payload":{"website":"'$WID'","url":"/api-send-test","data":{"nickname":"tester","plan":"pro"}}}'
实测回显:200(响应结构同 7.1;data 里的用户属性会挂到当前会话,用于后续按用户属性分析)。
7.4 参数错误行为(实测)
缺 website(HTTP 400):
{"error":{"message":"Bad request","code":"bad-request","status":400,"errors":[],"properties":{"payload":{"errors":[],"properties":{"website":{"errors":["Exactly one of website, link, or pixel must be provided"]}}}}}}
website id 不存在(HTTP 400,注意不是 401/404):
{"error":{"message":"Website not found.","code":"bad-request","status":400}}
7.5 bot 识别:无 User-Agent 直接跳过
curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:13000/api/send \
-H 'Content-Type: application/json' \
-d '{"type":"event","payload":{"website":"'$WID'","url":"/no-ua"}}'
实测回显(HTTP 200):
{"beep":"boop"}
⚠️ 这是一个经典踩坑:无 UA 请求返回 200 {"beep":"boop"},但数据并没有入库(该响应是 Umami 对识别为 bot/非法 UA 请求的固定回执)。服务端脚本直连上报时必须设置真实浏览器 UA,并回查统计确认数据落库。
7.6 text/plain 上报
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:13000/api/send \
-H 'Content-Type: text/plain' -H 'User-Agent: Mozilla/5.0 Chrome/120' \
-d '{"type":"event","payload":{"website":"'$WID'","url":"/plain-test"}}'
实测回显:200(正常入库)。text/plain 主要用于绕过浏览器插件的 ad-blocker 对 application/json 采集请求的拦截。
7.7 批量上报(POST /api/batch)
/api/batch 接受顶层数组(不是 {"batch":[...]} 包装,包对象会报 "Invalid input: expected array, received object",实测):
curl -s -w '\nHTTP %{http_code}\n' -X POST http://localhost:13000/api/batch \
-H 'Content-Type: application/json' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) Chrome/120' \
-d '[{"type":"event","payload":{"website":"'$WID'","hostname":"demo.example.com","url":"/batch-a","event_name":"batch-test-a","event_data":{"n":1}}},{"type":"event","payload":{"website":"'$WID'","hostname":"demo.example.com","url":"/batch-b","event_name":"batch-test-b"}}]'
实测回显(HTTP 200):
{"size":2,"processed":2,"errors":0,"details":[],"cache":"eyJ..."}
每个元素的结构与 /api/send 完全一致;size 提交数、processed 成功数、errors 失败数、details 逐条错误明细(全成功为 [])。适合离线补传/低频批量场景。
八、公开接口与错误码
8.1 分享数据接口(GET /api/share/{shareId})
无需认证,配合 /share/<shareId> 公开页面使用:
curl -s http://localhost:13000/api/share/docshare1
实测回显(节选,HTTP 200):
{"shareId":"a5ac93f4-f40d-4e51-a803-49ce35eafa79","shareType":1,"parameters":{"events":true,"overview":true},"websiteId":"bd69fdbf-f8bd-48c3-93d4-877c51c93fb4","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaGFyZUlkIjoiYTVhYzkzZjQtZjQwZC00ZTUxLWE4MDMtNDljZTM1ZWFmYTc5Iiwic2hhcmVUeXBlIjoxLCJwYXJhbW..."}
要点:返回的 token 是三段式 JWT(分享页后续请求用它鉴权);shareType 1 为统计分享;注意 shareId 是随机 UUID 形态,与网站记录里自定义的 shareId":"docshare1" 不同——URL 里用的是自定义标识,接口返回的是内部分享 id。
8.2 实例配置(GET /api/config)
无需认证,返回部署级开关(脚本可据此判断行为):
{"cloudMode":false,"privateMode":false,"sessionDeletionEnabled":true,"telemetryDisabled":false,"updatesDisabled":false}
字段对应环境变量:cloudMode ↔ CLOUD_MODE、privateMode ↔ PRIVATE_MODE、sessionDeletionEnabled ↔ 会话删除功能、telemetryDisabled ↔ TELEMETRY_DISABLED、updatesDisabled ↔ DISABLE_UPDATES(详见《Umami 3.4 配置参数详解》)。
8.3 错误码与响应约定汇总(实测)
| 场景 | 状态码 | code | 响应特征 |
|---|---|---|---|
| 登录口令错误 | 401 | incorrect-username-password | 统一错误信封 |
| 令牌无效/过期/跨实例 | 401 | unauthorized | 统一错误信封 |
| API Key 用错请求头 | 401 | — | x-umami-api-key 头不被 3.4.0 认可 |
| 参数缺失/枚举非法 | 400 | bad-request | properties.<字段>.errors 逐字段明细 |
| 上报 website 不存在 | 400 | bad-request | message 为 "Website not found."(非 404) |
| 方法不允许(PATCH/PUT 更新网站) | 405 | — | 空响应体 |
| 查询不存在的网站详情 | 200 | — | 响应体为 null,务必判空 |
| 创建/更新/删除成功 | 200 | — | 全部 200,无 201;写操作回显实体或 {"ok":true} |
统一错误信封结构:
{"error":{"message":"Bad request","code":"bad-request","status":400}}
8.4 通用约定与对接建议(实测归纳)
- 状态码语义宽松:创建 200、写操作 200、查询不存在 200+null——脚本判断成败要"状态码 + 响应体"双检;
- 时间一律毫秒时间戳(
startAt/endAt),返回时间一律 UTC ISO 8601 串(时区换算留给客户端); - 分页信封:
{data, count, page, pageSize, orderBy},列表接口通用; - 认证:管理/统计域 Bearer 头统一(登录令牌与 API Key 等价),采集域免认证;
x-umami-api-key头不生效; - 更新用 POST(2.x 的 PATCH/PUT 已废弃为 405),版本升级对接前先探方法;
- 服务端上报必须带浏览器 UA,否则
{"beep":"boop"}静默丢弃;批量走/api/batch顶层数组; - 多实例统一 APP_SECRET,否则登录令牌跨实例 401;
- 限流未见(实测高频顺序调用无 429),生产请客户端自限。
九、小结
本文以 Umami 3.4.0 实例为准,逐条实测了认证(登录令牌/verify/me/API Key)、网站管理(列表/创建/详情/更新/重置/删除)、统计查询(stats/pageviews/metrics/events/sessions/active)、采集上报(/api/send 单条与自定义事件/identify、/api/batch 批量、text/plain)、公开接口(share/config/heartbeat)共 20+ 端点,并沉淀了四组实测踩坑:更新必须 POST(PATCH/PUT 405)、metrics 必填 type 且合法枚举 13 个(url/page 已不合法)、登录令牌是非 JWT 的加密串、无 UA 上报被 {"beep":"boop"} 静默吞掉。照抄本文请求即可复现全部结果;与官方在线文档冲突时,以你部署版本的实际响应为准。