Keila 0.30.3 API 使用详解
Keila 0.30.3 REST API 实测详解:Bearer 认证与项目绑定、data 信封约定、分页与过滤、联系人 CRUD、Campaign 发送、事务性邮件与 Liquid 渲染预览、OpenAPI 规范。
Keila 0.30.3 API 使用详解
一、接入前提
Keila 0.30.3 内置一套 REST API(挂在 /api/v1 下),覆盖联系人、Campaign、表单、分群、模板、单发消息等资源,适合把 Keila 接进既有业务系统:CRM 同步联系人、事务性邮件(订单确认、验证码)直接经 API 单发、定时任务批量投递等。
1.1 创建 API Key
API Key 在 Web 界面的项目设置页创建:Settings → API Keys → Create API Key,起一个名字提交后,界面会展示密钥并提示「The private API Key is displayed only once and cannot be recovered」——密钥只显示一次,关闭页面后无法再查看,只能重新生成。
密钥与项目绑定:持有某项目的 API Key 只能操作该项目下的资源。这在服务端的认证插件的实现非常直接——密钥记录里存着 project_id,请求进来时按密钥解析出项目并校验归属,失败即 403。
实测环境(部署过程见《Debian 12 安装 Keila 0.30.3 指南》):项目 ID np_weLJnLY5,密钥 lkp0Henf2NsXM8Vehf4R6zKdAE23eGzSRB-ftkwqTJQ(实验口令,生产请妥善保管)。
1.2 认证方式
所有请求带 Authorization: Bearer <api_key> 头。不带或密钥错误返回 403:
curl -s -w '\nHTTP %{http_code}\n' -X POST -H "Content-Type: application/json" \
-d '{"data":{"email":"x@y.z"}}' http://192.168.146.131:8139/api/v1/contacts
实测回显:
{"errors":[{"status":"403","title":"Not authorized"}]}
HTTP 403
二、请求约定
四个容易踩坑的约定,全部实测得出:
1. 请求体必须用 data 键包裹业务字段。 Keila 的 API schema(OpenApiSpex 实现)按 {"data": {...}} 校验,平铺字段会解析为空对象进而报错:
// 正确
{"data":{"email":"alice@example.com"}}
// 错误(返回 400 "Bad Request")
{"email":"alice@example.com"}
2. 分页是 deepObject 风格查询参数(paginate[page] / paginate[page_size]),不是单个 page 参数——直接传 ?page=1 会得到 400 Unexpected field:
GET /api/v1/contacts?paginate[page]=0&paginate[page_size]=1
实测回显(page 从 0 起):
meta: {'count': 1, 'page': 0, 'page_count': 1} | ids: ['nc_weLJnLY5']
列表响应统一带 meta 对象(count 本页条数、page 当前页、page_count 总页数)。
3. 列表过滤用 filter 参数,值为 JSON 字符串,操作符是 MongoDB 风格($like、$eq 等):
curl -s -G -H "Authorization: Bearer $KEY" \
--data-urlencode 'filter={"email":{"$like":"%example.com"}}' \
http://192.168.146.131:8139/api/v1/contacts
实测回显:
meta: {'count': 1, 'page': 0, 'page_count': 1} | emails: ['alice@example.com']
4. 单资源寻址支持三种 ID,路径上的值默认按 Keila ID(nc_/nmc_ 等前缀)解析;用邮箱或 external_id 时必须带 ?id_type=email / ?id_type=external_id:
GET /api/v1/contacts/nc_weLJnLY5 → 200
GET /api/v1/contacts/alice@example.com?id_type=email → 200
GET /api/v1/contacts/alice@example.com → 400(缺 id_type)
实测回显(邮箱寻址):
{"data":{"data":{"plan":"pro"},"id":"nc_weLJnLY5","status":"active","email":"alice@example.com","inserted_at":"2026-10-26T06:29:39Z","updated_at":"2026-10-26T06:54:19Z","external_id":null,"first_name":"Alice","last_name":"Zhou"}}
HTTP 200
三、端点总览
写作时实测的 OpenAPI 规范(见下文)共 17 个路径,按资源域整理:
| 资源域 | 方法 | 说明 |
|---|---|---|
/api/v1/contacts | GET / POST | 联系人列表(分页+过滤)/ 创建 |
/api/v1/contacts/{id} | GET / PATCH / DELETE | 查询 / 更新 / 删除 |
/api/v1/contacts/{id}/data | PATCH / POST | 自定义字段浅合并 / 整体替换 |
/api/v1/campaigns | GET / POST | Campaign 列表 / 创建 |
/api/v1/campaigns/{id} | GET / PATCH / DELETE | 查询 / 更新 / 删除 |
/api/v1/campaigns/{id}/actions/send | POST | 触发投递(异步) |
/api/v1/campaigns/{id}/actions/schedule | POST | 定时投递 |
/api/v1/forms | GET / POST + 单资源 | 表单 CRUD |
/api/v1/forms/{id}/actions/submit | POST | 以 API 身份提交表单 |
/api/v1/segments | GET / POST + 单资源 | 分群 CRUD |
/api/v1/templates | GET / POST + 单资源 | 模板 CRUD |
/api/v1/senders | GET | 发件人只读列表(创建走 Web 界面) |
/api/v1/messages | POST | 单发事务性邮件 |
/api/v1/messages/actions/render | POST | 渲染预览(不发送) |
/api/v1/openapi | GET | OpenAPI 3.0.0 规范全文 |
两个设计要点:senders 没有创建/删除端点(发件人涉及 SMTP 凭据,官方只在 Web 界面开放管理,API 可列出用于引用其 sender_id);单发消息没有查询端点(发送即走 Oban 队列,结果以投递为准)。
四、联系人全流程实测
4.1 创建与更新
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"data":{"email":"alice@example.com","first_name":"Alice","last_name":"Chen"}}' \
http://192.168.146.131:8139/api/v1/contacts
实测回显:
{"data":{"data":null,"id":"nc_weLJnLY5","status":"active","email":"alice@example.com","inserted_at":"2026-10-26T06:29:39Z","updated_at":"2026-10-26T06:29:39Z","external_id":null,"first_name":"Alice","last_name":"Chen"}}
创建即激活(status: "active",API 创建不走邮件激活流程)。PATCH 更新单个字段:
curl -s -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"data":{"last_name":"Zhou"}}' \
http://192.168.146.131:8139/api/v1/contacts/nc_weLJnLY5
实测回显(updated_at 变为 06:54:10Z):
{"data":{"data":null,"id":"nc_weLJnLY5","status":"active","email":"alice@example.com","inserted_at":"2026-10-26T06:29:39Z","updated_at":"2026-10-26T06:54:10Z","external_id":null,"first_name":"Alice","last_name":"Zhou"}}
4.2 自定义字段(data)
联系人的 data 字段是自由 JSON(也可在 Web 界面的联系人表单里预定义字段),data 子端点提供两种语义:
- PATCH = 浅合并:保留已有键,覆盖同名键;
- POST = 整体替换。
curl -s -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"data":{"plan":"pro"}}' \
http://192.168.146.131:8139/api/v1/contacts/nc_weLJnLY5/data
实测回显(data 从 null 变为 {"plan":"pro"}):
{"data":{"data":{"plan":"pro"},"id":"nc_weLJnLY5","status":"active","email":"alice@example.com","inserted_at":"2026-10-26T06:29:39Z","updated_at":"2026-10-26T06:54:19Z","external_id":null,"first_name":"Alice","last_name":"Zhou"}}
4.3 删除
curl -s -w '\nHTTP %{http_code}\n' -X DELETE -H "Authorization: Bearer $KEY" \
http://192.168.146.131:8139/api/v1/contacts/nc_BzLMaLXv
curl -s -w '\nHTTP %{http_code}\n' -H "Authorization: Bearer $KEY" \
http://192.168.146.131:8139/api/v1/contacts/nc_BzLMaLXv
实测回显(删除是幂等的,之后查询返回 404):
HTTP 204
{"errors":[{"status":"404","title":"Not found"}]}
HTTP 404
五、Campaign 创建与发送
Campaign 的内容类型由 settings.type 决定,必须与内容字段匹配(text → text_body,html → html_body,mjml → mjml_body):
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"data":{"subject":"Keila B1 验证邮件","text_body":"Hello from Keila 0.30.3 built from source!","sender_id":"nms_weLJnLY5","settings":{"type":"text"}}}' \
http://192.168.146.131:8139/api/v1/campaigns
实测回显(节选):
{"data":{"data":null,"id":"nmc_weLJnLY5","subject":"Keila B1 验证邮件","settings":{"type":"text"},"inserted_at":"2026-10-26T06:32:39Z","text_body":"Hello from Keila 0.30.3 built from source!","sender_id":"nms_weLJnLY5","sent_at":null,"segment_id":null}}
触发投递(异步——进入 Oban 队列后立即返回,逐个联系人渲染并经 SMTP 发出):
curl -s -i -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
http://192.168.146.131:8139/api/v1/campaigns/nmc_weLJnLY5/actions/send
实测回显:
HTTP/1.1 202 Accepted
content-type: application/json; charset=utf-8
{"data":{"campaign_id":"nmc_weLJnLY5","delivery_queued":true}}
投递范围语义:segment_id 为 null 时发送给项目内全部 active 联系人;指定 Segment 则只发给分群命中的联系人。发送完成后列表中可见 sent_at 时间戳:
GET /api/v1/campaigns →
{"data":[{"id":"nmc_weLJnLY5",...,"sent_at":"2026-10-26T06:32:44Z",...}],"meta":{"count":1,"page":0,"page_count":1}}
六、事务性邮件(单发)
POST /api/v1/messages 用于业务系统直接投递一封邮件(验证码、订单通知等),必填字段为 type、sender_id 与收件人(recipient_email / contact_id / external_contact_id 三选一):
curl -s -w '\nHTTP %{http_code}\n' -X POST -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"data":{"type":"text","sender_id":"nms_weLJnLY5","recipient_email":"alice@example.com","subject":"API 单发测试","text_body":"Sent via POST /api/v1/messages"}}' \
http://192.168.146.131:8139/api/v1/messages
实测回显:
{"data":{"id":"nmr_2yLDNVbQ","recipient_email":"alice@example.com","subject":"API 单发测试"}}
HTTP 200
在收信端确认送达(实验环境的收信端为 Mailpit,SMTP 通道配置见安装指南):
total: 3
newsletter@keila.local -> ['alice@example.com'] | API 单发测试
newsletter@keila.local -> ['admin@keila.local'] | Your Account Reset Link
newsletter@keila.local -> ['admin@keila.local'] | Your Account Reset Link
字段名与 Campaign 不同:单发邮件的收件人字段是
recipient_email(Campaign 无收件人字段,投递范围由 segment 决定)。传错字段名会得到精确到字段的错误提示,例如传recipient时返回{"errors":[{"pointer":"/data/recipient","status":"400","title":"Unexpected field"}]}。
七、Liquid 渲染与预览
POST /api/v1/messages/actions/render 对内容做 Liquid 插值渲染但不发送,用于预览或本地测试。必填:type、sender_id、收件人(渲染要取联系人数据)、subject。Liquid 上下文里可访问 contact.*(含自定义字段 contact.data.*):
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"data":{"type":"text","sender_id":"nms_weLJnLY5","recipient_email":"alice@example.com","subject":"渲染测试","text_body":"Hi {{ contact.first_name }}! 你的套餐是 {{ contact.data.plan }}。"}}' \
http://192.168.146.131:8139/api/v1/messages/actions/render
实测回显(联系人 Alice 的自定义字段 plan=pro 被正确插值):
{"data":{"html_body":null,"subject":"渲染测试","text_body":"Hi Alice! 你的套餐是 pro。"}}
漏字段时的错误同样精确:缺收件人返回 {"errors":[{"detail":"one of contact_id, external_contact_id, recipient_email is required",...}]},缺主题返回 {"errors":[{"status":"400","title":"No subject"}]}。
八、OpenAPI 规范端点
Keila 自带机器可读规范,便于生成各语言客户端:
curl -s -o openapi.json -w 'HTTP %{http_code}, %{size_download} bytes\n' \
-H "Authorization: Bearer $KEY" http://192.168.146.131:8139/api/v1/openapi
python3 -c "import json; d=json.load(open('openapi.json')); print('openapi:', d.get('openapi'), '| paths:', len(d.get('paths',{})))"
实测回显:
HTTP 200, 58319 bytes
openapi: 3.0.0 | paths: 17
九、常见问题
9.1 一直 403 "Not authorized"
检查三点:请求头是否为 Authorization: Bearer <key>(Bearer 后有一个空格);密钥是否属于目标项目(密钥绑定创建时的项目,跨项目资源一律 403);密钥是否完整(复制时截断)。
9.2 POST/PUT 返回 400 "Bad Request" 但没有细节
十有八九是没用 {"data": {...}} 信封。Keila 对信封外的平铺字段不做合并,解析结果为空时直接 400。修正后若仍有问题,会得到带 pointer 的字段级错误(如 Unexpected field、Missing field)。
9.3 分页传 page 参数报 "Unexpected field"
列表分页参数是 deepObject 风格的 paginate[page] 与 paginate[page_size],且 page 从 0 开始;单个 page=1 不被接受。
9.4 用邮箱查询联系人返回 400
路径值默认按 Keila ID 解析,邮箱/external_id 寻址必须加 ?id_type=email(或 id_type=external_id)。
9.5 API 无法创建发件人
设计如此:senders 域只有 GET /api/v1/senders 只读列表,创建/编辑发件人(含 SMTP 中继凭据)只能在 Web 界面完成。集成时先在界面建好,再从列表取 sender_id 供 Campaign 与单发邮件引用。
9.6 render 端点报 "No subject" 或要求 contact_id
渲染是"以某联系人的视角"做插值,所以收件人(三选一)与 subject 都是必填,不只是传个 body 模板。只想本地调试 Liquid 的话,同样可以用该端点返回的 text_body/html_body 比对预期。
十、参考链接
- Keila 官方仓库:https://github.com/pentacent/keila
- Keila 官方网站与文档:https://www.keila.io
- 实例内置 OpenAPI 规范:
GET /api/v1/openapi(见上文实测) - 本专栏其他篇目:《Keila 0.30.3 介绍与选型指南》《Debian 12 安装 Keila 0.30.3 指南》(另有配置参数详解与官方打包形态部署指南见专栏内 sort 3/sort 4)