# OpenAPI Notes 本文件描述 Send Engine 對外 API、Webhook 驗證與與 Member Center 的介接規則。 目標是讓 Member Center 與租戶網站可以清楚交換資料與責任邊界。 ## Auth 與驗證 ### 1. 租戶網站 → Send Engine API 使用 OAuth2 Client Credentials 或 JWT(由 Member Center 簽發)。 必要 claims: - `tenant_id` - `scope`(至少 `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 seconds - `X-Nonce`: UUID - `X-Client-Id`: Auth client UUID(對應 `auth_clients.id`) 驗證規則: - timestamp 在允許時間窗內(例如 ±5 分鐘) - nonce 不可重複(重放防護) - signature 必須匹配 - client 必須存在且為 active - `auth_clients.tenant_id` 必須與 payload `tenant_id` 一致 - 不使用任何 `X-Client-Id` fallback;缺少 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 ## 通用錯誤格式 ```json { "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.activated` - `subscription.unsubscribed` - `preferences.updated` Request Body(示意): ```json { "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`:accepted - `401/403`:驗證失敗 - `409`:event_id 重複或 nonce 重放 - `422`:格式錯誤 ### B. 全量名單同步 用途:由 Member Center 主動推送全量或分批名單,避免 Send Engine 拉取名單。 Endpoint: - `POST /webhooks/lists/full-sync` Scope: - `newsletter:events.write` Request Body(分批示意): ```json { "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`:accepted - `401/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: ```json { "tenant_id": "uuid", "list_id": "uuid", "name": "Weekly Update", "subject": "Weekly Update", "body_html": "
Hello
", "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` 必填,最小長度 1 - `body_html` / `body_text` / `template` 至少擇一 - `window_start` 必須小於 `window_end`(若有提供) Response: ```json { "send_job_id": "uuid", "status": "pending" } ``` ### C-1. Sending Proxy Submit Job(整合規格) 用途:對齊內容網站/會員平台呼叫發信代理的標準接口。 Endpoint: - `POST /v1/send-jobs` Request Body(欄位): - `message_type`:`newsletter` | `transactional` - `from`:發件人 - `to`:收件人陣列 - `subject`:主旨 - `html`:HTML 內容 - `text`:純文字內容 - `headers`:自定義 header(白名單) - `list_unsubscribe.url`:退訂 URL - `list_unsubscribe.mailto`:可選 - `tags.campaign_id` / `tags.site_id` / `tags.list_id` / `tags.segment` - `idempotency_key`:冪等鍵 Response: - `job_id` - `status=queued` 規則: - 必須帶 Configuration Set + Message Tags 後才能呼叫 SES - `newsletter` 類型需帶: - `List-Unsubscribe` - `List-Unsubscribe-Post: List-Unsubscribe=One-Click` ### D. 查詢 Send Job Endpoint: - `GET /api/send-jobs/{id}` Scope: - `newsletter:send.read` Response: ```json { "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: ```json { "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(示意): ```json { "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 Center - `suppression`:設為黑名單(`suppressed`) 回寫 Member Center 條件: - `hard_bounced`:設黑名單後回寫 - `soft_bounced`:達門檻設黑名單後回寫 - `complaint`:立即回寫 - `suppression`:設黑名單後回寫 回寫原因碼(固定): - `hard_bounce` - `soft_bounce_threshold` - `complaint` - `suppression` ## 外部 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(示意): ```json { "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`)