Tham chiếu API

Receiving Events

Webhook delivery & signature verification

How UnifyPort delivers events to your endpoint over HTTP POST, the X-Device-* headers on every delivery, and how to verify the HMAC-SHA256 signature so you can trust the payload. Also covers the response we expect, retries, and idempotency. Quyền nhận sự kiện gọi do is_beta của không gian làm việc quyết định. Với quyền thử nghiệm, subscribed_events là *, rỗng hoặc bị bỏ qua bao gồm sự kiện gọi; danh sách cụ thể chỉ nhận các sự kiện đã liệt kê. Không gian không có quyền vẫn dùng được wildcard nhưng không nhận sự kiện gọi; đăng ký rõ sự kiện gọi trả về 403 beta_feature_not_enabled. Quyền được kiểm tra lại mỗi lần gửi, thử lại và phát lại dead letter; lỗi tra cứu không cho phép gửi. Bản ghi cuộc gọi message.* thông thường độc lập.

Xác nhận và thử lại

2xx
Lưu sự kiện rồi trả về bất kỳ 2xx nào để xác nhận đã nhận. Nội dung phản hồi không được dùng để xử lý kết quả.
408 / 429 / 5xx
Lỗi kết nối và 408 / 429 / 5xx được thử lại ngay. retry_policy.max_attempts quy định số lần sau yêu cầu đầu tiên: mặc định 3 lần, tổng tối đa 4 yêu cầu. Không có backoff và không đọc Retry-After.
4xx (≠ 408 / 429)
Các lỗi 4xx khác dừng gửi tự động và đánh dấu sự kiện là dead-letter. Sửa cấu hình phía nhận; không chờ hệ thống tự phát lại.

Delivery headers

  • X-Device-Event-Id

    Correlation id for the emitted event. It stays the same across retries of that event, but WhatsApp HistorySync batches must not be globally deduplicated by this header alone.

  • X-Device-Delivery-Id

    Delivery correlation id. It can fall back to X-Device-Event-Id and is therefore not guaranteed to be unique for each HTTP attempt.

  • X-Device-Timestamp

    RFC 3339 UTC time the delivery was signed (e.g. 2026-06-08T12:34:56Z). It is part of the signed string and also lets you reject stale deliveries.

  • X-Device-Signature

    Hex-encoded HMAC-SHA256 of "<X-Device-Timestamp>" + "." + "<raw request body>". Present only when the endpoint has a signing_secret; omitted when signing is disabled.

  • Content-Type

    Always application/json.

Verify the signature

Node.js (Express)

import crypto from 'crypto';
import express from 'express';

const app = express();
const SECRET = process.env.WEBHOOK_SIGNING_SECRET;

// express.raw keeps the body as the exact bytes we signed — never re-serialize.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  let timestamp = req.get('X-Device-Timestamp');
  if (!timestamp) {
    timestamp = '';
  }
  let signature = req.get('X-Device-Signature');
  if (!signature) {
    signature = '';
  }

  const hmac = crypto.createHmac('sha256', SECRET);
  hmac.update(timestamp + '.');
  hmac.update(req.body); // raw Buffer
  const expected = hmac.digest('hex');

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!valid) return res.status(401).end();

  const event = JSON.parse(req.body.toString('utf8'));
  // handle event.type ...
  res.status(200).end(); // any 2xx acknowledges the delivery
});

Python (Flask)

import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SIGNING_SECRET"].encode()

@app.post("/webhook")
def webhook():
    timestamp = request.headers.get("X-Device-Timestamp", "")
    signature = request.headers.get("X-Device-Signature", "")
    body = request.get_data()  # raw bytes, exactly as delivered

    expected = hmac.new(
        SECRET, timestamp.encode() + b"." + body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(signature, expected):
        abort(401)

    event = request.get_json()
    # handle event["type"] ...
    return "", 200

Ghi chú

  • Delivery is at-least-once. For ordinary events, retries reuse X-Device-Event-Id and receivers should deduplicate idempotently. For conversation.history, do not globally deduplicate on the top-level event id alone; merge by provider, account_id, conversation.id, and messages[].id so later chunks are retained.
  • Verify against the raw request bytes, before any JSON parse or re-serialize. Re-encoding the body changes the bytes and breaks the signature.
  • Reject deliveries whose X-Device-Timestamp is too far from your clock to limit replay — the timestamp is covered by the signature.
  • Signing is per-endpoint: set signing_secret on Create or Update webhook endpoint to enable it; leave it empty to disable and drop the X-Device-Signature header.
  • Delivery order is not guaranteed — events from the same conversation can arrive out of order (a read receipt may land before the message it refers to). Order by occurred_at, with the event id as a tiebreaker, before applying state.
  • UnifyPort has no REST API for reading message history and does not guarantee replay of missed payloads. Store events when they arrive. WhatsApp can emit limited best-effort conversation.history data after bootstrap or reconnect, but it is neither complete nor a durable archive.