# 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 ### 3. Send Engine → Member Center 回寫 使用 OAuth2 Client Credentials(Send Engine 作為 client) scope 最小化: - `newsletter:events.write`(停用回寫) - `newsletter:list.read`(若未來仍需查詢) ## 通用欄位 ### 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" } ``` ### 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/complaint/delivery/open/click 等事件。 Endpoint: - `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` ## 外部 API:Send Engine → Member Center 以下為 Member Center 端提供的 API,非 Send Engine 的 OpenAPI 規格範圍。 ### G. 停用訂閱回寫 用途:因 hard bounce / complaint 停用訂閱,並在 Member Center 註記來源。 Endpoint(Member Center 提供): - `POST /api/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`:伺服器內部錯誤