260 lines
5.8 KiB
Markdown
260 lines
5.8 KiB
Markdown
# 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`(若未來仍需查詢)
|
||
|
||
## 通用欄位
|
||
### 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": "<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` 必填,最小長度 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`:伺服器內部錯誤
|