Dittofeed v0.24.0-alpha.21 REST API 使用详解
逐组实测 Dittofeed v0.24.0-alpha.21 的 REST API:写键/管理密钥/会话 Cookie 三套鉴权、track/identify/page/screen/group/batch 上报、用户与事件查询、管理密钥生命周期、工作区作用域与错误码,附 curl 与真实响应。
Dittofeed v0.24.0-alpha.21 REST API 使用详解
Dittofeed 的服务端是一个 Fastify 应用,所有接口挂在 /api、/api/admin、/api/public、/internal-api 四个前缀下,并按前缀采用三套不同的鉴权方式。本文基于 Ubuntu 22.04 源码部署实例(api 监听 3101,PostgreSQL 15、ClickHouse 24.12.6.70、Temporal 1.22.4)逐组实测,给出可直接执行的 curl 与真实响应。
安装见 《Ubuntu 22.04 安装 Dittofeed v0.24.0-alpha.21 指南》,参数含义见 《Dittofeed v0.24.0-alpha.21 配置参数详解》,产品概览见 《Dittofeed v0.24.0-alpha.21 介绍与选型指南》。
一、路由分层与鉴权总览
| 前缀 | 鉴权方式 | 典型用途 | 是否要求 workspaceId |
|---|---|---|---|
/api/ | 会话 Cookie(single-tenant 登录后) | 控制台内部调用:users、events、segments、journeys、secrets、settings、admin-keys | 单租户可省略(回落首个工作区) |
/api/admin/ | Authorization: Bearer <admin api key> | 面向自动化的管理接口:users、events、segments、journeys、broadcasts、deliveries、content、settings、analysis | 建议显式传 |
/api/public/ | 写键(Basic)或无需鉴权 | 事件上报 track/identify/page/screen/group/batch、订阅管理、单租户登录 | 写键隐含工作区 |
/internal-api/debug | 无 | 调试用(仅建议内网开放) | 否 |
关键实现位置:
- 路由注册:
packages/api/src/buildApp/router.ts - 管理密钥校验:
packages/api/src/buildApp/adminAuth.ts - 工作区解析:
packages/api/src/workspace.ts - 公开上报:
packages/api/src/controllers/publicAppsController.ts
二、鉴权体系
2.1 公开写键(Public Write Key)
写键是 Basic base64("<secretId>:<writeKeyValue>"),由初始化时写入 "WriteKey" 与 "Secret" 表。实例初始化后的默认写键为:
secretId : 8623bae3-c74c-4aa4-96ab-7cecaccbea2a
writeKeyValue: a60b8aa27a1ad698
Authorization: Basic ODYyM2JhZTMtYzc0Yy00YWE0LTk2YWItN2NlY2FjY2JlYTJhOmE2MGI4YWEyN2ExYWQ2OTg=
生成命令:
python3 -c "import base64;print(base64.b64encode(b'<secretId>:<writeKeyValue>').decode())"
写键只能写入事件,不能读取任何数据。
2.2 管理密钥(Admin API Key)
管理密钥通过 POST /api/admin-keys(会话鉴权)创建,请求体必须带 workspaceId 与 name:
WK_PASS=q1w2e3r4
curl -s -c /srv/ck.txt -X POST http://127.0.0.1:3101/api/public/single-tenant/login \
-H 'content-type: application/json' -d "{\"password\":\"$WK_PASS\"}"
curl -s -b /srv/ck.txt -X POST http://127.0.0.1:3101/api/admin-keys \
-H 'content-type: application/json' \
-d '{"workspaceId":"7bd6a519-18d7-4d26-8f4b-66b4788afa63","name":"doc-key"}'
真实响应(200):
{"workspaceId":"7bd6a519-18d7-4d26-8f4b-66b4788afa63","id":"da732ed6-a56e-4fcd-b8f8-3f0ddbf07b41","name":"doc-key","createdAt":1793107226746,"apiKey":"eb57d937a431815e14774ad5127968f6fd428ae621b71e38cd5df58877314e6b"}
随后管理接口带:
curl -s -X POST http://127.0.0.1:3101/api/admin/users \
-H 'authorization: Bearer eb57d937a431815e14774ad5127968f6fd428ae621b71e38cd5df58877314e6b' \
-H 'content-type: application/json' \
-d '{"workspaceId":"7bd6a519-18d7-4d26-8f4b-66b4788afa63","limit":5}'
也可以用命令行创建(不依赖会话):
node /srv/dittofeed/packages/admin-cli/dist/scripts/cli.js admin-api-key create \
-w 7bd6a519-18d7-4d26-8f4b-66b4788afa63 -n cli-key
2.3 会话 Cookie(单租户)
POST /api/public/single-tenant/login 只校验口令,无用户名:
curl -s -i -c /srv/ck.txt -X POST http://127.0.0.1:3101/api/public/single-tenant/login \
-H 'content-type: application/json' -d '{"password":"q1w2e3r4"}'
HTTP/1.1 200 OK
set-cookie: session=...; Max-Age=1209600; Path=/; HttpOnly
错误口令:
{"error":"Invalid password."} code=401
退出:GET /api/public/single-tenant/signout 会删除会话并 302 跳回控制台登录页。
2.4 鉴权失败语义
| 场景 | 状态码 | 响应体(原文) |
|---|---|---|
| 未带 Authorization | 401 | {"message":"No authorization header"} |
| 方案不是 Bearer | 401 | {"message":"Invalid authorization header"} |
| 密钥不存在/已删除 | 401 | {"message":"API key not valid"} |
| workspaceId 非 UUID | 400 | {"message":"Invalid workspaceId, not a valid UUID."} |
| body/query 与 header 的 workspaceId 不一致 | 400 | {"message":"Mismatched workspaceIds in request."} |
| 多租户模式完全不带 workspaceId | 400 | {"message":"body must have required property 'workspaceId'"} |
| 路由不存在 | 404 | {"message":"Route GET:/api/admin/workspaces?... not found","error":"Not Found","statusCode":404} |
| 缺少必填字段 | 400 | {"statusCode":400,"code":"FST_ERR_VALIDATION",...,"message":"body must have required property 'messageId', ..."} |
| 请求体超限 | 413 | {"statusCode":413,"code":"FST_ERR_CTP_BODY_TOO_LARGE",...} |
单租户模式下不带 workspaceId 时,服务端会执行 SELECT ... FROM "Workspace" LIMIT 1 回落,管理密钥也按该工作区校验。
三、公开上报 API
所有上报接口为 POST /api/public/apps/<method>,成功一律返回 204 且无响应体(幂等键为 messageId)。
先准备两个变量,后续 curl 直接复用:
BASE=http://127.0.0.1:3101
WS=7bd6a519-18d7-4d26-8f4b-66b4788afa63
WK=$(python3 -c "import base64;print(base64.b64encode(b'8623bae3-c74c-4aa4-96ab-7cecaccbea2a:a60b8aa27a1ad698').decode())")
KEY=eb57d937a431815e14774ad5127968f6fd428ae621b71e38cd5df58877314e6b
3.1 track
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:3101/api/public/apps/track \
-H "authorization: Basic $WK" -H 'content-type: application/json' \
-d '{"messageId":"11111111-1111-4111-8111-111111111111",
"userId":"u-c1-001","event":"doc_test_track",
"properties":{"plan":"pro"},"context":{}}'
204
写入后可用管理接口核对:
curl -s -H "authorization: Bearer $KEY" \
"http://127.0.0.1:3101/api/admin/events?workspaceId=$WS&userId=u-c1-001"
{"events":[{"messageId":"11111111-1111-4111-8111-111111111111","eventType":"track","event":"doc_test_track","userId":"u-c1-001","anonymousId":"","processingTime":"2026-10-27 13:18:10.154","eventTime":"2026-10-27 13:20:00.000","traits":"{\"plan\":\"pro\"}"}],"count":1}
messageId 必填;缺省时 400:
body must have required property 'messageId', body must match a schema in anyOf
3.2 identify
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:3101/api/public/apps/identify \
-H "authorization: Basic $WK" -H 'content-type: application/json' \
-d '{"messageId":"22222222-2222-4222-8222-222222222222","userId":"u-c1-001",
"traits":{"email":"u001@example.com","firstName":"C1"},"context":{}}'
204
traits 会落入 user_property_idx_* 物化视图,管理端的 events/traits 可查询:
{"traits":["email","firstName"]}
3.3 page / screen
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:3101/api/public/apps/page \
-H "authorization: Basic $WK" -H 'content-type: application/json' \
-d '{"messageId":"66666666-6666-4666-8666-666666666666","userId":"u-d5-001",
"name":"Pricing","properties":{"path":"/pricing"},"context":{}}'
204
screen 参数同构,用于移动端。
3.4 group
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:3101/api/public/apps/group \
-H "authorization: Basic $WK" -H 'content-type: application/json' \
-d '{"messageId":"77777777-7777-4777-8777-777777777777","userId":"u-d5-001",
"groupId":"acme","traits":{"plan":"pro"},"context":{}}'
204
用户所属组可通过 GET /api/admin/groups/user-groups 读取:
{"groups":["acme"]}
3.5 batch
一次提交多条事件,batch 内每项需带 type:
{"batch":[
{"type":"track","messageId":"88888888-8888-4888-8888-888888888888","userId":"u-d5-001","event":"batch_track","context":{}},
{"type":"identify","messageId":"99999999-9999-4999-8999-999999999999","userId":"u-d5-001","traits":{"city":"Shanghai"},"context":{}}
],"context":{}}
204
3.6 alias(未实现)
POST /api/public/apps/alias
{"message":"Not yet implemented."} code=400
源码中该路由只记 Client is calling unimplemented endpoint /alias 日志并返回 400,身份合并请改用 identify 覆盖 anonymousId。
3.7 写键错误
POST /api/public/apps/track (Basic Zm9vOmJhcg==)
{"message":"Invalid write key."} code=401
完全不带 authorization 头时更早的 schema 校验会拦截:
{"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"headers must have required property 'authorization'"}
四、用户与事件查询(管理接口)
4.1 用户列表
管理接口的用户列表是 POST(不是 GET),workspaceId 放在 body:
curl -s -X POST http://127.0.0.1:3101/api/admin/users \
-H 'authorization: Bearer eb57d937a431815e14774ad5127968f6fd428ae621b71e38cd5df58877314e6b' \
-H 'content-type: application/json' \
-d '{"workspaceId":"7bd6a519-18d7-4d26-8f4b-66b4788afa63","limit":1}'
{"users":[{"id":"u-c1-001","properties":{"c5a6719f-a26f-4d03-b621-2c4378a09b23":{"name":"firstName","value":"C1"},"663f3b8d-c6e8-4377-8a22-3ea7d349467d":{"name":"id","value":"u-c1-001"},"58665505-dc68-4a5a-9bc5-e65335e97b2b":{"name":"email","value":"u001@example.com"}},"segments":[]}],"userCount":0,"nextCursor":"eyJ1IjoidS1jMS0wMDEifQ=="}
分页用 nextCursor 原样回传;limit 缺省为 1000。注意列表里的 userCount 字段实测恒为 0,精确总数要用 /count:
{"userCount":3}
4.2 用户订阅
curl -s -H "authorization: Bearer $KEY" \
"http://127.0.0.1:3101/api/admin/users/subscriptions?workspaceId=$WS&userId=u-c1-001"
{"workspaceId":"7bd6a519-18d7-4d26-8f4b-66b4788afa63","userId":"u-c1-001","subscriptionGroups":[{"id":"b95aea40-e523-5b32-8449-ee7213cd5f11","name":"Default - Email","isSubscribed":true,"channel":"Email"},{"id":"98dd4e12-f71a-56fa-b7c7-888561f939de","name":"Default - SMS","isSubscribed":true,"channel":"Sms"}]}
4.3 删除用户数据
DELETE /api/admin/users/v2 用 query 传 userIds[](body 版为 DELETE /api/admin/users):
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE \
"http://127.0.0.1:3101/api/admin/users/v2?workspaceId=$WS&userIds%5B%5D=u-del-001" \
-H "authorization: Bearer $KEY"
204
删除前 events?userId=u-del-001 返回 1 条(del_probe),2 秒后再次查询返回:
{"events":[],"count":0}
4.4 其他只读接口
| 接口 | 方法 | 实测响应摘要 |
|---|---|---|
/api/admin/events | GET | {"events":[...],"count":2},按 userId 过滤 |
/api/admin/events/traits | GET | {"traits":["email","firstName"]} |
/api/admin/events/properties | GET | {"properties":{"DFGroupUserAssignment":["assigned","userId"],"async_probe":["mode"],"doc_test_track":["plan"]}} |
/api/admin/segments | GET | {"segments":[...]},初始化后自带 4 个订阅组相关分段 |
/api/admin/journeys | GET | {"journeys":[]} |
/api/admin/broadcasts | GET | [] |
/api/admin/deliveries | GET | {"workspaceId":"...","items":[]} |
/api/admin/deliveries/count | GET | {"count":0} |
/api/admin/content/templates | GET | 返回内置 Welcome Email 等模板(含 MJML 正文) |
/api/admin/settings/data-sources | GET | {"dataSourceConfigurations":[]} |
/api/admin/subscription-groups | GET | 返回 Default - Email、Default - SMS 两个 OptOut 组 |
/api/admin/user-properties | GET | 返回 phone、language、email 等属性定义,status":"Running" |
/api/admin/analysis/summary | GET | {"summary":{"deliveries":0,"sent":0,"opens":0,"clicks":0,"bounces":0}} |
/api/admin/groups/user-groups | GET | {"groups":["acme"]} |
分析类接口需要时间窗参数:
curl -s -H "authorization: Bearer $KEY" \
"http://127.0.0.1:3101/api/admin/analysis/summary?workspaceId=$WS&startDate=2026-10-01&endDate=2026-10-28"
4.5 会话版等价接口
同一套 usersController、eventsController 等同时挂在 /api(会话鉴权)与 /api/admin(Bearer)下,路径与请求体一致。会话版实测同样可用:
GET /api/events?workspaceId=...&userId=u-c1-001 -> 200(返回同一事件)
GET /api/segments?workspaceId=... -> 200
GET /api/journeys?workspaceId=... -> 200
GET /api/analysis/summary?workspaceId=...&startDate=...&endDate=... -> 200
GET /api/settings/data-sources?workspaceId=... -> 200
不带 Cookie 时 POST /api/users 返回 401(空响应体)。
五、管理密钥生命周期
创建:见 2.2。同名密钥重复创建时不会报冲突,而是返回既有的密钥(200):
POST /api/admin-keys {"name":"doc-key"} -> 200,返回首次创建的 id/apiKey
删除(会话鉴权,query 传 id 与 workspaceId):
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE \
"http://127.0.0.1:3101/api/admin-keys?workspaceId=$WS&id=fb303932-9a82-47a8-903f-443d2fc928d9" \
-b /srv/ck.txt
200
删除后原密钥立即失效:
POST /api/admin/users/count -> 401 {"message":"API key not valid"}
注意源码 schema 标注删除响应为 204,实测返回 200 空体,脚本判断请按 2xx 而非精确 204。
另有一个易踩坑:创建密钥时 workspaceId 传空串会直接 500:
{"message":"invalid input syntax for type uuid: \"\""}
六、工作区管理与作用域
本版本没有工作区的 HTTP 增删改查接口:
GET /api/admin/workspaces?workspaceId=... -> 404
{"message":"Route GET:/api/admin/workspaces?workspaceId=... not found","error":"Not Found","statusCode":404}
工作区只能通过命令行初始化(同名幂等):
node /srv/dittofeed/packages/admin-cli/dist/scripts/cli.js bootstrap \
-n Tenant2 -t Parent -d tenant2.example.com
实测结果:"Workspace" 表新增一行 Tenant2 | Parent | tenant2.example.com,同时为新工作区生成一整套默认密钥(default-write-key、各邮件/短信提供商占位、subscription-key)。
作用域规则:
- 单租户模式:管理密钥只需属于唯一工作区,请求可省略
workspaceId; - 多租户模式:
workspaceId必填,可用 body、query 或请求头x-workspace-id;三者出现多个且不一致时报 400Mismatched workspaceIds in request.; - 父工作区(
Parent)的密钥可以管理其子工作区,Root工作区密钥只能管理自身。
七、最小可用调用序列
BASE=http://127.0.0.1:3101
WS=7bd6a519-18d7-4d26-8f4b-66b4788afa63
# 1) 单租户登录拿会话
curl -s -c /srv/ck.txt -X POST $BASE/api/public/single-tenant/login \
-H 'content-type: application/json' -d '{"password":"q1w2e3r4"}'
# 2) 建管理密钥
KEY=$(curl -s -b /srv/ck.txt -X POST $BASE/api/admin-keys \
-H 'content-type: application/json' -d "{\"workspaceId\":\"$WS\",\"name\":\"auto\"}" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["apiKey"])')
# 3) 上报事件
WK=$(python3 -c "import base64;print(base64.b64encode(b'8623bae3-c74c-4aa4-96ab-7cecaccbea2a:a60b8aa27a1ad698').decode())")
curl -s -o /dev/null -w '%{http_code}\n' -X POST $BASE/api/public/apps/track \
-H "authorization: Basic $WK" -H 'content-type: application/json' \
-d '{"messageId":"cccccccc-cccc-4ccc-8ccc-cccccccccccc","userId":"u-auto-001","event":"signup","context":{}}'
# 4) 后台查询
curl -s -H "authorization: Bearer $KEY" "$BASE/api/admin/events?workspaceId=$WS&userId=u-auto-001"
八、实测发现的问题
| 问题 | 现象(证据原文) | 影响与处理 |
|---|---|---|
| 用户列表用 POST 不是 GET | GET /api/admin/users → Route GET:/api/admin/users not found | 列表用 POST /api/admin/users,请求体带 workspaceId |
列表里的 userCount 恒为 0 | {"users":[...],"userCount":0},而 /users/count 返回 {"userCount":3} | 总数走 /api/admin/users/count |
| 新建用户不立即可查 | 刚 track 的 u-d5-001 用 userIds 过滤返回 {"users":[],"userCount":0},但事件已可查 | 用户索引依赖计算属性异步任务,需等 worker 处理 |
alias 未实现 | {"message":"Not yet implemented."} | 用 identify + anonymousId 合并身份 |
| 删除密钥返回 200 | schema 标注 204,实测 200 空体 | 脚本按 2xx 判断 |
| 密钥名可重复 | 同名 doc-key 再次创建返回既有 id 与 apiKey | 视为幂等查询,不要期待新密钥 |
| 空 workspaceId 触发 500 | {"message":"invalid input syntax for type uuid: \"\""} | 创建密钥前先确定工作区 ID |
缺 authorization 头报 400 而非 401 | headers must have required property 'authorization' | 客户端按 400/401 双分支处理 |
| 无工作区 CRUD | Route GET:/api/admin/workspaces... not found | 用 admin-cli bootstrap 管理工作区 |
events 字段是 ClickHouse 原值 | eventTime":"2026-10-27 13:20:00.000"(非 ISO 8601) | 解析时按 YYYY-MM-DD HH:MM:SS.fff 格式化 |
九、参考
- 《Dittofeed v0.24.0-alpha.21 配置参数详解》
- 《Ubuntu 22.04 安装 Dittofeed v0.24.0-alpha.21 指南》
- 《Dittofeed v0.24.0-alpha.21 介绍与选型指南》
- 路由与鉴权源码:
packages/api/src/buildApp/router.ts、packages/api/src/buildApp/adminAuth.ts、packages/api/src/workspace.ts - 控制器源码:
packages/api/src/controllers/publicAppsController.ts、usersController.ts、eventsController.ts、apiKeyController.ts