7.8 KiB
7.8 KiB
OpenAPI 草案(完整)
已補上完整端點與資料結構,並提供 docs/openapi.yaml 作為可直接擴充的版本。
其中 /webhooks/* 為 Member Center 對外發送時遵循的整合契約(實際由 Send Engine 提供端點)。
版本
- OpenAPI: 3.1.0
- 檔案:
docs/openapi.yaml
核心資源
- OAuth2/OIDC:授權、token、discovery、JWKS
- Auth:註冊、登入(password grant)、刷新、登出、忘記/重設密碼、Email 驗證
- User:個人資料
- Newsletter:訂閱/確認/退訂/偏好
- Admin:Tenants/Lists/OAuth Clients(MVP CRUD)
Security Schemes
- OAuth2 (Authorization Code + PKCE、Client Credentials)
- Bearer JWT(API 使用)
補充說明
/oauth/token、/auth/login、/auth/refresh使用application/x-www-form-urlencoded- Access token 以 JWT(JWS)簽發,建議驗證
iss與aud /auth/email/verify需要token+email/newsletter/subscribe會回傳confirm_token/newsletter/unsubscribe-token需要list_id + email才能申請unsubscribe_token/newsletter/one-click-unsubscribe-token提供 Send Engine 發信前取得 one-click 退訂 token(tenant_id + list_id + subscriber_id)/newsletter/one-click-unsubscribe-tokens提供 Send Engine 批次取得 one-click 退訂 token(tenant_id + list_id + subscriber_ids[])/newsletter/preferences(GET/POST)需要list_id + email,避免跨租戶資料讀取/更新
通用欄位
occurred_at:RFC3339(例:2026-02-10T09:30:00Z)event_id、request_id:UUID
通用錯誤格式
{
"error": "string_code",
"message": "human readable message",
"request_id": "uuid"
}
多租戶資料隔離原則
- 與訂閱者資料(preferences、unsubscribe token)相關的查詢與寫入,一律必須帶
list_id + email做租戶邊界約束。 - 不提供僅靠
email或單純subscription_id的公開查詢/操作端點。
Webhook Auth(Member Center -> Send Engine)
- Header(對齊 Send Engine 規格):
X-SignatureX-TimestampX-NonceX-Client-Id
X-Client-Id來源:- 由 Send Engine 的
auth_clients.id(UUID)提供 - Member Center 以 DB 設定(tenant 設定欄位)保存每個租戶的對應值
- 可由管理 UI(Tenant 編輯)或整合 API
POST /integrations/send-engine/webhook-clients/upsert更新
- 由 Send Engine 的
- 簽章建議:
HMAC-SHA256(secret, "{raw_body}")(對齊 Send Engine 驗證器)
- 驗證規則:
- timestamp 在允許時間窗(例如 ±5 分鐘)
- nonce 不可重複(防重放)
X-Client-Id必須存在且 active,且auth_clients.tenant_id與 payloadtenant_id一致- 不使用
X-Client-Idfallback;缺少 tenant 對應 client 時應略過發送 - 預設拒絕
auth_clients.tenant_id = NULL的通用 client(除非 Send Engine 明確開啟) - signature 必須匹配
OAuth Client 用途分離(強制)
usage=tenant_api:- 供租戶站台拿 token 呼叫 Member Center / Send Engine API
- scope 僅給業務所需(如
newsletter:events.write)
usage=send_api:- 供租戶站台呼叫 Send Engine 發信流程
- 內建 scope:
newsletter:send.write、newsletter:send.read
usage=webhook_outbound:- 供 Member Center 內部標記「對外 webhook 用」的租戶憑證用途
- 不可用於租戶 API 呼叫
X-Client-Id仍以 Send Engineauth_clients.id為準- 需設定
redirect_uris(Authorization Code 流程)
usage=platform_service:- 供平台級 S2S(例如 SES 聚合事件回寫)
- 可不綁定
tenant_id,scope 使用newsletter:events.write.global
tenant_api/send_api/platform_service建議(且實作要求)client_type=confidentialredirect_uris僅webhook_outbound需要;其他 usage 可為空- 管理規則:
- 每個 tenant 至少 2 組憑證(
tenant_api/webhook_outbound) - 平台級流程另建
platform_service憑證 - secret 分開輪替,禁止共用
- 每個 tenant 至少 2 組憑證(
整合 API / Auth(狀態)
API
GET /newsletter/subscriptions?list_id=...:已實作(供發送引擎同步)POST /webhooks/subscriptions:已實作(Member Center 發送;Send Engine 接收)POST /webhooks/lists/full-sync:規格已定義(Member Center 發送;Send Engine 接收)POST /subscriptions/disable:已實作(Send Engine 回寫黑名單)POST /integrations/send-engine/webhook-clients/upsert:已實作(Send Engine 回填 tenant webhook client id)POST /newsletter/one-click-unsubscribe-token:已實作(Send Engine 發信前申請 one-click token)POST /newsletter/one-click-unsubscribe-tokens:已實作(Send Engine 批次申請 one-click token)
Auth / Scope
tenant_api/send_api/webhook_outbound類型需綁定tenant_idplatform_service可不綁定tenant_id- 新增 scope:
newsletter:list.readnewsletter:send.writenewsletter:send.readnewsletter:events.readnewsletter:events.writenewsletter:events.write.global
- 發送引擎僅能用上述 scope,禁止 admin 權限
POST /subscriptions/disable需 Bearer token 且包含下列其一:newsletter:events.write(tenant-scoped)newsletter:events.write.global(platform-scoped,SES 回寫用)
- 建議 Send Engine 使用 client credentials 取 token,不建議使用長效固定 token
- Send Engine 建議以 JWKS 驗簽 JWT(JWS),並驗證
scope/tenant_id/expiss由Auth:Issuer設定(例:http://localhost:7850/)aud預設:- Send Engine 流程:
send_engine_api(可用Auth:SendEngineAudience覆寫) - Member Center API 流程:
member_center_api(可用Auth:MemberCenterAudience覆寫)
- Send Engine 流程:
回寫原因碼(Send Engine -> Member Center)
hard_bouncesoft_bounce_thresholdcomplaintsuppression
處理規則:
hard_bounce/soft_bounce_threshold/suppression:- 取消該 email 的所有訂閱(跨租戶)
- 加入全域黑名單
complaint:- 僅取消該筆訂閱(依
subscriber_id + list_id) - 不加入黑名單
- 僅取消該筆訂閱(依
/subscriptions/disable 請求欄位(Send Engine -> Member Center)
tenant_id(UUID)subscriber_id(UUID)list_id(UUID)reason(hard_bounce | soft_bounce_threshold | complaint | suppression)disabled_by(建議固定send_engine)occurred_at(RFC3339)
Member Center 會用 subscriber_id + list_id 查詢訂閱,再驗證 tenant_id 邊界;驗證通過後才寫入全域 email 黑名單。
One-Click 退訂 Token 介接方式(Send Engine)
用途:Send Engine 在寄信前批次向 Member Center 取得每位收件者的 one-click 退訂 token。
步驟:
- 以 client credentials 取得 access token(scope:
newsletter:events.write或newsletter:events.write.global) - 呼叫
POST /newsletter/one-click-unsubscribe-tokens - 將回傳
status=issued的unsubscribe_token寫入每封信的List-UnsubscribeURL
Request(批次):
{
"tenant_id": "c9034414-43d6-404e-8d41-e80922420bf1",
"list_id": "a92fdeda-29bb-42ca-9c05-e7df3983288a",
"subscriber_ids": [
"33333333-3333-3333-3333-333333333333",
"44444444-4444-4444-4444-444444444444"
]
}
Response(批次):
{
"items": [
{
"subscriber_id": "33333333-3333-3333-3333-333333333333",
"unsubscribe_token": "token-xxx",
"status": "issued"
},
{
"subscriber_id": "44444444-4444-4444-4444-444444444444",
"unsubscribe_token": null,
"status": "blacklisted"
}
]
}
狀態說明:
issued:可直接用於 one-click 退訂連結not_found:找不到對應訂閱者(tenant/list/subscriber 邊界不匹配)blacklisted:已在黑名單,不提供 token