7.7 KiB
OpenAPI Notes
本文件描述 Send Engine 對外 API、Webhook 驗證與與 Member Center 的介接規則。 目標是讓 Member Center 與租戶網站可以清楚交換資料與責任邊界。
Auth 與驗證
1. 租戶網站 → Send Engine API
使用 OAuth2 Client Credentials 或 JWT(由 Member Center 簽發)。
必要 claims:
tenant_idscope(至少newsletter:send.write)
規則:
tenant_id只能取自 token,不接受 body 覆寫list_id必須屬於該 tenant
2. Member Center → Send Engine Webhook
使用簽名 Webhook(HMAC)或 OAuth2 Client Credentials(建議簽名)。
Header 建議:
X-Signature:hex(hmac_sha256(secret, body))X-Timestamp: Unix epoch secondsX-Nonce: UUIDX-Client-Id: Auth client UUID(對應auth_clients.id)
驗證規則:
- timestamp 在允許時間窗內(例如 ±5 分鐘)
- nonce 不可重複(重放防護)
- signature 必須匹配
- client 必須存在且為 active
auth_clients.tenant_id必須與 payloadtenant_id一致- 不使用任何
X-Client-Idfallback;缺少 tenant 對應 client 時應由來源端跳過發送 - 預設拒絕
auth_clients.tenant_id = NULL的通用 client(可透過Webhook__AllowNullTenantClient=true明確開啟)
3. Send Engine → Member Center 回寫
使用 OAuth2 Client Credentials(Send Engine 作為 client)
scope 最小化:
newsletter:events.write(停用回寫)newsletter:list.read(若未來仍需查詢)
實作約定:
- 優先走 token endpoint(client credentials)
ApiToken僅作暫時 fallback(建議逐步移除)
通用欄位
Timestamp
- 欄位:
occurred_at - 格式:RFC3339,例如
2026-02-10T09:30:00Z
ID
tenant_id、list_id、subscriber_id、event_id皆為 UUID
通用錯誤格式
{
"error": "string_code",
"message": "human readable message",
"request_id": "uuid"
}
Webhook:Member Center → Send Engine
A. 訂閱事件(增量)
用途:同步新增/取消/偏好變更。
Endpoint:
POST /webhooks/subscriptions
Scope:
newsletter:events.write
事件型別:
subscription.activatedsubscription.unsubscribedpreferences.updated
Request Body(示意):
{
"event_id": "uuid",
"event_type": "subscription.activated",
"tenant_id": "uuid",
"list_id": "uuid",
"subscriber": {
"id": "uuid",
"email": "user@example.com",
"status": "active",
"preferences": { "topic": "news" }
},
"occurred_at": "2026-02-10T09:30:00Z"
}
Response:
200 OK:accepted401/403:驗證失敗409:event_id 重複或 nonce 重放422:格式錯誤
B. 全量名單同步
用途:由 Member Center 主動推送全量或分批名單,避免 Send Engine 拉取名單。
Endpoint:
POST /webhooks/lists/full-sync
Scope:
newsletter:events.write
Request Body(分批示意):
{
"sync_id": "uuid",
"batch_no": 1,
"batch_total": 10,
"tenant_id": "uuid",
"list_id": "uuid",
"subscribers": [
{ "id": "uuid", "email": "a@example.com", "status": "active" },
{ "id": "uuid", "email": "b@example.com", "status": "unsubscribed" }
],
"occurred_at": "2026-02-10T09:30:00Z"
}
Response:
200 OK:accepted401/403:驗證失敗409:sync_id + batch_no 重複422:格式錯誤
API:租戶網站 → Send Engine
C. 建立 Send Job
用途:租戶網站送入內容,建立 Campaign/Send Job 並排程。
Endpoint:
POST /api/send-jobs
Scope:
newsletter:send.write
Request Body:
{
"tenant_id": "uuid",
"list_id": "uuid",
"name": "Weekly Update",
"subject": "Weekly Update",
"body_html": "<p>Hello</p>",
"body_text": "Hello",
"template": null,
"scheduled_at": "2026-02-11T02:00:00Z",
"window_start": "2026-02-11T02:00:00Z",
"window_end": "2026-02-11T05:00:00Z",
"tracking": { "open": true, "click": false }
}
欄位規則:
subject必填,最小長度 1body_html/body_text/template至少擇一window_start必須小於window_end(若有提供)
Response:
{
"send_job_id": "uuid",
"status": "pending"
}
C-1. Sending Proxy Submit Job(整合規格)
用途:對齊內容網站/會員平台呼叫發信代理的標準接口。
Endpoint:
POST /v1/send-jobs
Request Body(欄位):
message_type:newsletter|transactionalfrom:發件人to:收件人陣列subject:主旨html:HTML 內容text:純文字內容headers:自定義 header(白名單)list_unsubscribe.url:退訂 URLlist_unsubscribe.mailto:可選tags.campaign_id/tags.site_id/tags.list_id/tags.segmentidempotency_key:冪等鍵
Response:
job_idstatus=queued
規則:
- 必須帶 Configuration Set + Message Tags 後才能呼叫 SES
newsletter類型需帶:List-UnsubscribeList-Unsubscribe-Post: List-Unsubscribe=One-Click
D. 查詢 Send Job
Endpoint:
GET /api/send-jobs/{id}
Scope:
newsletter:send.read
Response:
{
"id": "uuid",
"tenant_id": "uuid",
"list_id": "uuid",
"campaign_id": "uuid",
"status": "running",
"scheduled_at": "2026-02-11T02:00:00Z",
"window_start": "2026-02-11T02:00:00Z",
"window_end": "2026-02-11T05:00:00Z"
}
E. 取消 Send Job
Endpoint:
POST /api/send-jobs/{id}/cancel
Scope:
newsletter:send.write
Response:
{
"id": "uuid",
"status": "cancelled"
}
Webhook:SES → Send Engine
F. SES 事件回報
用途:接收 bounce/hard_bounced/soft_bounced/complaint/suppression/delivery/open/click 等事件。
推薦架構(正式):
SES Configuration Set -> SNS -> SQS -> ECS Worker- 由 Worker 消費事件,不要求對外公開 webhook
相容模式(可選):
POST /webhooks/ses
驗證:
- 依 SES/SNS 規格驗簽(可用
Ses__SkipSignatureValidation=true暫時略過)
Request Body(示意):
{
"event_type": "bounce",
"message_id": "ses-id",
"tenant_id": "uuid",
"email": "user@example.com",
"bounce_type": "hard",
"occurred_at": "2026-02-10T09:45:00Z"
}
Response:
200 OK
事件對應規則(固定):
hard_bounced:立即設為黑名單(suppressed)soft_bounced:累計達門檻後設為黑名單(suppressed)complaint:取消訂閱並回寫 Member Centersuppression:設為黑名單(suppressed)
回寫 Member Center 條件:
hard_bounced:設黑名單後回寫soft_bounced:達門檻設黑名單後回寫complaint:立即回寫suppression:設黑名單後回寫
回寫原因碼(固定):
hard_bouncesoft_bounce_thresholdcomplaintsuppression
外部 API:Send Engine → Member Center
以下為 Member Center 端提供的 API,非 Send Engine 的 OpenAPI 規格範圍。
G. 停用訂閱回寫
用途:因 hard bounce / complaint 停用訂閱,並在 Member Center 註記來源。
Endpoint(Member Center 提供):
POST /subscriptions/disable
Scope:
newsletter:events.write
Request Body(示意):
{
"tenant_id": "uuid",
"subscriber_id": "uuid",
"list_id": "uuid",
"reason": "hard_bounce",
"disabled_by": "send_engine",
"occurred_at": "2026-02-10T09:45:00Z"
}
狀態碼與錯誤
通用錯誤:
401/403:Auth 或 scope 不符409:重放或事件重複(nonce / event_id)422:資料格式錯誤500:伺服器內部錯誤
Retry 策略(整合規格)
- Throttle:指數退避重試
- Temporary network error:重試
- Hard failure:不重試
- Retry 上限可設定(例如 5 次)
相關環境參數
Bounce__SoftBounceThreshold:soft bounce 轉黑名單門檻(預設5)