| 项 | 值 |
|---|
| Base URL | http://kira-notify.internal:8084(内网专用) |
| 鉴权 | /notify、/emails 需 header X-Internal-Key: <INTERNAL_KEY>;/health 无需 |
| Content-Type | application/json |
服务无公网入口。只能从 Fly org 私网(kira-be / kira-queue)或 WireGuard 调用。X-Internal-Key 缺失或不匹配 → 401;服务端未配置 INTERNAL_KEY → 500。
POST /notify
高层语义事件入口(推荐)。声明 type + userId,由 notify 解析用户、判定门槛、渲染并投递。
{
"type": "daily_digest",
"userId": "uuid",
"data": {},
"contact": { "email": "user@example.com", "locale": "en-US" }
}
| 字段 | 类型 | 必填 | 说明 |
|---|
type | string | ✅ | notification type,见 Notification Types |
userId | string | ✅ | 用户 UUID,用于从 Supabase 解析联系方式 |
data | object | — | handler 专属载荷(当前 handler 未使用) |
contact | { email?, locale? } | — | 用户记录不可用时的兜底(如 account_deletion.deleted,用户已删)。提供 email 时跳过 Supabase 查询 |
缺 type 或 userId → 400:`type` and `userId` are required。未注册的 type → 400:Unknown notification type: <type>。
响应:已投递
{
"delivered": [
{ "channel": "email", "providerId": "<resend-id>" }
]
}
delivered 是 DeliveryResult[]——每个元素含投递渠道与 provider 返回的 ID(email 即 Resend message id)。
响应:已跳过
门槛 / 偏好不满足或用户找不到时,不是错误,返回 200 + skip 标记:
{ "skipped": true, "reason": "disabled_by_user" }
reason | 触发条件 |
|---|
disabled_by_user | 用户 is_open_notification = false(关闭了通知) |
plan_gate | 不满足 plan 门槛(如 daily_digest 要求 plan === "free") |
user_not_found | Supabase 查不到该用户(且未提供 contact.email 兜底) |
skip 在代码里通过 handler 抛 NotificationSkipped(reason) 实现,路由层捕获后转成 { skipped: true, reason }。SkipReason union 即上表三值。
curl -X POST http://kira-notify.internal:8084/notify \
-H "X-Internal-Key: $INTERNAL_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "account_deletion.scheduled", "userId": "<uuid>" }'
POST /emails
低层 Resend 兼容原始邮件发送。用于不值得注册为 notification type 的一次性邮件。不查用户、不做门槛判断、不走 i18n/模板——调用方自备 subject 与正文。
{
"from": "Kira Art <support@kira.art>",
"to": "user@example.com",
"subject": "Hello",
"html": "<p>Hi</p>"
}
| 字段 | 类型 | 必填 | 说明 |
|---|
from | string | ✅ | 发件人 |
to | string | string[] | ✅ | 收件人 |
subject | string | ✅ | 主题 |
html | string | ⚠️ | HTML 正文 |
text | string | ⚠️ | 纯文本正文 |
cc | string | string[] | — | 抄送 |
bcc | string | string[] | — | 密送 |
reply_to | string | string[] | — | 回复地址(映射到 Resend replyTo) |
from / to / subject 任一缺失 → 400。html 与 text 至少提供其一,否则 channel 抛 Either \html` or `text` must be provided`。
id 即 Resend 返回的 message id。Resend 返回 error 或无 id 时,channel 抛错 → 500。
curl -X POST http://kira-notify.internal:8084/emails \
-H "X-Internal-Key: $INTERNAL_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Kira Art <support@kira.art>",
"to": "user@example.com",
"subject": "Hello",
"html": "<p>Hi</p>"
}'
GET /health
健康检查,无需鉴权。
错误约定
| 状态 | 场景 |
|---|
200 | 投递成功(delivered)或已跳过(skipped) |
400 | 缺必填字段 / 未知 notification type |
401 | X-Internal-Key 缺失或不匹配 |
500 | INTERNAL_KEY 未配置 / Resend 发送失败 / 其他未捕获异常 |
onError 对 < 500 的 HTTPException 会清掉 c.error,避免 @hono/otel 把 4xx span 误标 ERROR 污染 Dash0 错误率;只有真 5xx 才 captureException 并记 ERROR span。