Webhook 設定
Webhook 允許您即時接收郵件狀態更新,包括送達、退信、投訴等事件。
什麼是 Webhook
當郵件狀態發生變化時,Zeabur Email 會向您設定的 URL 發送 HTTP POST 請求,包含事件詳情。
建立 Webhook
登入控制台
造訪 Zeabur 控制台的 Zeabur Email 管理頁面。
建立 Webhook
- 進入「Webhook 管理」
- 點擊「建立 Webhook」
- 輸入 Webhook URL(如
https://yourapp.com/webhooks/zsend) - 選擇要接收的事件類型
- 點擊「儲存」
取得簽章金鑰
建立後,系統會產生一個簽章金鑰(Secret),用於驗證請求來源。請妥善儲存,金鑰僅顯示一次!
驗證 Webhook
點擊「驗證」按鈕,系統會向您的 URL 發送測試請求,確保可以正常接收。
事件類型
Zeabur Email 支援以下事件類型:
| 事件類型 | 說明 |
|---|---|
send | 郵件已發送到郵件伺服器 |
delivery | 郵件已成功送達收件人 |
bounce | 郵件被退回(硬退信或軟退信) |
complaint | 收件人標記為垃圾郵件 |
reject | 郵件被拒絕發送(網域未驗證、內容違規等) |
Webhook 請求格式
請求標頭
POST /your-webhook-endpoint HTTP/1.1
Host: yourapp.com
Content-Type: application/json
X-ZSend-Event: delivery
X-ZSend-Timestamp: 1768812348
X-ZSend-Signature: sha256=dc36b914ecdb7bb6f4ba8714b6ccb04d46e85af5cd1bc52744e0208964f5fb34
User-Agent: ZSend-Webhook/1.0| 標頭 | 說明 |
|---|---|
X-ZSend-Event | 事件類型 |
X-ZSend-Timestamp | Unix 時間戳記 |
X-ZSend-Signature | HMAC-SHA256 簽章 |
請求主體
送達事件 (delivery)
{
"event": "delivery",
"timestamp": "2026-01-19T08:45:48Z",
"email": {
"id": "696def36de644b22ae711500",
"message_id": "0111019bd56e71c1-8ccdb66d-5d71-433f-9a9a-0766822f8955-000000",
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "歡迎使用 Zeabur Email",
"sent_at": "2026-01-19T08:45:42Z"
},
"data": {
"processing_time_millis": 5116,
"recipients": ["[email protected]"],
"smtp_response": "250 OK"
}
}退信事件 (bounce)
{
"event": "bounce",
"timestamp": "2026-01-19T08:45:50Z",
"email": {
"id": "696def36de644b22ae711501",
"message_id": "0111019bd56e71c1-8ccdb66d-5d71-433f-9a9a-0766822f8955-000001",
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "測試郵件"
},
"data": {
"bounce_type": "Permanent"
"bounce_subtype": "General",
"bounced_recipients": [
{
"email_address": "[email protected]",
"status": "5.1.1",
"diagnostic_code": "smtp; 550 5.1.1 user unknown"
}
]
}
}投訴事件 (complaint)
{
"event": "complaint",
"timestamp": "2026-01-19T09:00:00Z",
"email": {
"id": "696def36de644b22ae711502",
"message_id": "0111019bd56e71c1-8ccdb66d-5d71-433f-9a9a-0766822f8955-000002",
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "每週通訊"
},
"data": {
"complaint_feedback_type": "abuse",
"complained_recipients": ["[email protected]"]
}
}簽章驗證
為了確保請求來自 Zeabur Email,您需要驗證 X-ZSend-Signature 簽章。
驗證步驟
- 取得請求標頭中的
X-ZSend-Timestamp和X-ZSend-Signature - 讀取原始請求主體
- 構造簽章訊息:
{timestamp}.{body} - 使用 HMAC-SHA256 和您的 Secret 計算簽章
- 比較計算結果與
X-ZSend-Signature(格式為sha256=xxx)
Node.js 範例
const crypto = require('crypto');
const express = require('express');
const app = express();
// 使用 raw body 儲存原始請求主體(必須在 express.json() 之前)
app.post('/webhooks/zsend',
express.raw({ type: 'application/json' }),
(req, res) => {
const secret = process.env.ZSEND_WEBHOOK_SECRET;
const signature = req.headers['x-zsend-signature'];
const timestamp = req.headers['x-zsend-timestamp'];
// 檢查必要的標頭
if (!signature || !timestamp) {
return res.status(400).json({ error: 'Missing signature or timestamp' });
}
const body = req.body.toString('utf8'); // 原始請求主體
// 構造簽章訊息
const message = `${timestamp}.${body}`;
// 計算 HMAC-SHA256
const hmac = crypto.createHmac('sha256', secret);
hmac.update(message);
const expectedSignature = 'sha256=' + hmac.digest('hex');
// 時間安全比較(先檢查長度避免 RangeError)
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expectedSignature);
const isValid = sigBuf.length === expBuf.length &&
crypto.timingSafeEqual(sigBuf, expBuf);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 解析 JSON 並處理事件
const payload = JSON.parse(body);
const { event, email, data } = payload;
console.log(`Received ${event} event for email ${email.id}`);
// 回傳 200 表示成功接收
res.status(200).json({ received: true });
}
);Python 範例
import hmac
import hashlib
def verify_webhook_signature(request, secret):
signature = request.headers.get('X-ZSend-Signature')
timestamp = request.headers.get('X-ZSend-Timestamp')
body = request.get_data(as_text=True)
# 構造簽章訊息
message = f"{timestamp}.{body}"
# 計算 HMAC-SHA256
expected_signature = 'sha256=' + hmac.new(
secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 時間安全比較
return hmac.compare_digest(signature, expected_signature)
# Flask 範例
@app.route('/webhooks/zsend', methods=['POST'])
def handle_webhook():
secret = os.environ.get('ZSEND_WEBHOOK_SECRET')
if not verify_webhook_signature(request, secret):
return jsonify({'error': 'Invalid signature'}), 401
data = request.get_json()
event_type = data['event']
email_id = data['email']['id']
print(f"Received {event_type} event for email {email_id}")
return jsonify({'received': True}), 200Go 範例
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// WebhookPayload 代表 webhook 載荷結構
type WebhookPayload struct {
Event string `json:"event"`
Timestamp string `json:"timestamp"`
Email struct {
ID string `json:"id"`
MessageID string `json:"message_id"`
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
SentAt string `json:"sent_at"`
} `json:"email"`
Data interface{} `json:"data"`
}
func verifyWebhookSignature(body []byte, signature, timestamp, secret string) bool {
// 構造簽章訊息
message := fmt.Sprintf("%s.%s", timestamp, body)
// 計算 HMAC-SHA256
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(message))
expectedSignature := "sha256=" + hex.EncodeToString(h.Sum(nil))
// 時間安全比較
return subtle.ConstantTimeCompare([]byte(signature), []byte(expectedSignature)) == 1
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
secret := os.Getenv("ZSEND_WEBHOOK_SECRET")
// 讀取原始請求主體
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// 取得請求標頭
signature := r.Header.Get("X-ZSend-Signature")
timestamp := r.Header.Get("X-ZSend-Timestamp")
// 驗證簽章
if !verifyWebhookSignature(body, signature, timestamp, secret) {
http.Error(w, `{"error":"Invalid signature"}`, http.StatusUnauthorized)
return
}
// 解析 JSON
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// 處理事件
fmt.Printf("Received %s event for email %s\n", payload.Event, payload.Email.ID)
// 回傳 200 表示成功接收
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func main() {
http.HandleFunc("/webhooks/zsend", handleWebhook)
http.ListenAndServe(":3000", nil)
}重試機制
如果您的 Webhook 端點:
- 回傳非 2xx 狀態碼
- 請求逾時(10 秒)
- 網路錯誤
Zeabur Email 會自動重試最多 3 次,使用指數退避策略:
- 第 1 次重試:1 秒後
- 第 2 次重試:2 秒後
- 第 3 次重試:4 秒後
⚠️
超過 3 次重試失敗後,該 Webhook 事件將被丟棄。請確保您的端點能夠快速回應。
最佳實踐
1. 快速回應
Webhook 處理程式應該快速回傳 200 回應,然後非同步處理事件:
app.post('/webhooks/zsend',
express.raw({ type: 'application/json' }),
async (req, res) => {
const secret = process.env.ZSEND_WEBHOOK_SECRET;
const signature = req.headers['x-zsend-signature'];
const timestamp = req.headers['x-zsend-timestamp'];
if (!signature || !timestamp) {
return res.status(400).json({ error: 'Missing signature or timestamp' });
}
const body = req.body.toString('utf8');
// 先驗證簽章
const message = `${timestamp}.${body}`;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(message);
const expectedSignature = 'sha256=' + hmac.digest('hex');
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expectedSignature);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 立即回傳 200
res.status(200).json({ received: true });
// 非同步處理事件
const payload = JSON.parse(body);
setImmediate(async () => {
try {
await processWebhookEvent(payload);
} catch (error) {
console.error('Failed to process webhook:', error);
}
});
}
);2. 冪等性處理
同一個事件可能會被發送多次,您的處理邏輯應該是冪等的:
async function processWebhookEvent(event) {
const { email, event: eventType } = event;
// 檢查事件是否已處理
const existing = await db.webhookEvents.findOne({
email_id: email.id,
event_type: eventType,
timestamp: event.timestamp
});
if (existing) {
console.log('Event already processed, skipping');
return;
}
// 處理事件
await updateEmailStatus(email.id, eventType);
// 記錄已處理
await db.webhookEvents.create({
email_id: email.id,
event_type: eventType,
timestamp: event.timestamp
});
}3. 監控和告警
設定監控來追蹤 Webhook 的健康狀態:
const webhookMetrics = {
received: 0,
verified: 0,
processed: 0,
failed: 0
};
app.post('/webhooks/zsend',
express.raw({ type: 'application/json' }),
async (req, res) => {
webhookMetrics.received++;
const secret = process.env.ZSEND_WEBHOOK_SECRET;
const signature = req.headers['x-zsend-signature'];
const timestamp = req.headers['x-zsend-timestamp'];
if (!signature || !timestamp) {
webhookMetrics.failed++;
return res.status(400).json({ error: 'Missing signature or timestamp' });
}
const body = req.body.toString('utf8');
// 驗證簽章
const message = `${timestamp}.${body}`;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(message);
const expectedSignature = 'sha256=' + hmac.digest('hex');
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expectedSignature);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
webhookMetrics.failed++;
return res.status(401).json({ error: 'Invalid signature' });
}
webhookMetrics.verified++;
res.status(200).json({ received: true });
const payload = JSON.parse(body);
try {
await processWebhookEvent(payload);
webhookMetrics.processed++;
} catch (error) {
webhookMetrics.failed++;
// 發送告警
await sendAlert('Webhook processing failed', error);
}
}
);4. 測試 Webhook
在生產環境部署前,使用測試工具驗證 Webhook:
# 使用 ngrok 暴露本機伺服器
ngrok http 3000
# 在 Zeabur Email 控制台設定 ngrok URL
# https://abc123.ngrok.io/webhooks/zsend
# 發送測試郵件並觀察 Webhook 事件建議在 Zeabur Email 控制台查看 Webhook 日誌,了解發送狀態和回應詳情。