Dittofeed v0.24.0-alpha.21 REST API 使用详解

逐组实测 Dittofeed v0.24.0-alpha.21 的 REST API:写键/管理密钥/会话 Cookie 三套鉴权、track/identify/page/screen/group/batch 上报、用户与事件查询、管理密钥生命周期、工作区作用域与错误码,附 curl 与真实响应。

DittofeedDittofeed营销自动化Node.jsClickHouse2026/10/27

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 鉴权失败语义

场景状态码响应体(原文)
未带 Authorization401{"message":"No authorization header"}
方案不是 Bearer401{"message":"Invalid authorization header"}
密钥不存在/已删除401{"message":"API key not valid"}
workspaceId 非 UUID400{"message":"Invalid workspaceId, not a valid UUID."}
body/query 与 header 的 workspaceId 不一致400{"message":"Mismatched workspaceIds in request."}
多租户模式完全不带 workspaceId400{"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/eventsGET{"events":[...],"count":2},按 userId 过滤
/api/admin/events/traitsGET{"traits":["email","firstName"]}
/api/admin/events/propertiesGET{"properties":{"DFGroupUserAssignment":["assigned","userId"],"async_probe":["mode"],"doc_test_track":["plan"]}}
/api/admin/segmentsGET{"segments":[...]},初始化后自带 4 个订阅组相关分段
/api/admin/journeysGET{"journeys":[]}
/api/admin/broadcastsGET[]
/api/admin/deliveriesGET{"workspaceId":"...","items":[]}
/api/admin/deliveries/countGET{"count":0}
/api/admin/content/templatesGET返回内置 Welcome Email 等模板(含 MJML 正文)
/api/admin/settings/data-sourcesGET{"dataSourceConfigurations":[]}
/api/admin/subscription-groupsGET返回 Default - Email、Default - SMS 两个 OptOut 组
/api/admin/user-propertiesGET返回 phone、language、email 等属性定义,status":"Running"
/api/admin/analysis/summaryGET{"summary":{"deliveries":0,"sent":0,"opens":0,"clicks":0,"bounces":0}}
/api/admin/groups/user-groupsGET{"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;三者出现多个且不一致时报 400 Mismatched 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 不是 GETGET /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 合并身份
删除密钥返回 200schema 标注 204,实测 200 空体脚本按 2xx 判断
密钥名可重复同名 doc-key 再次创建返回既有 id 与 apiKey视为幂等查询,不要期待新密钥
空 workspaceId 触发 500{"message":"invalid input syntax for type uuid: \"\""}创建密钥前先确定工作区 ID
缺 authorization 头报 400 而非 401headers must have required property 'authorization'客户端按 400/401 双分支处理
无工作区 CRUDRoute 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 格式化

九、参考