09bdb8aaad
* feat(python-sdk): add webhook verification and event handling Add webhook support to the Python SDK matching the JS SDK implementation: - Add Webhooks class with verify() and construct_event() methods - Implement HMAC-SHA256 signature verification with timing-safe comparison - Add timestamp validation with configurable tolerance (default 5 minutes) - Add comprehensive webhook event types (18 events: email, contact, domain, test) - Add WebhookVerificationError with typed error codes - Export webhook constants (headers) and types * fix(python-sdk): harden webhook parsing and typing Normalize invalid UTF-8 webhook payloads to INVALID_BODY errors so verify() safely returns false, and narrow base email webhook event types to avoid discriminated-union overlap. Add regression tests for both paths. * chore(python-sdk): bump package version to 0.2.9 * feat(python-sdk): add local webhook test example project Add a runnable Flask receiver and signed webhook sender under packages/python-sdk/example, and link it from the Python SDK README for local verification. --------- Co-authored-by: Claude <noreply@anthropic.com>
64 lines
1.6 KiB
Python
64 lines
1.6 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import time
|
|
import uuid
|
|
|
|
import requests
|
|
|
|
|
|
WEBHOOK_SECRET = os.getenv("USESEND_WEBHOOK_SECRET", "whsec_test")
|
|
WEBHOOK_URL = os.getenv("WEBHOOK_URL", "http://127.0.0.1:8000/webhook")
|
|
|
|
|
|
def _signature(secret: str, timestamp_ms: str, body: str) -> str:
|
|
digest = hmac.new(
|
|
secret.encode("utf-8"),
|
|
f"{timestamp_ms}.{body}".encode("utf-8"),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
return f"v1={digest}"
|
|
|
|
|
|
def main() -> None:
|
|
payload = {
|
|
"id": f"evt_{uuid.uuid4().hex[:8]}",
|
|
"type": "email.bounced",
|
|
"createdAt": "2026-02-08T10:00:00.000Z",
|
|
"data": {
|
|
"id": "email_123",
|
|
"status": "BOUNCED",
|
|
"from": "sender@example.com",
|
|
"to": ["recipient@example.com"],
|
|
"occurredAt": "2026-02-08T10:00:00.000Z",
|
|
"bounce": {
|
|
"type": "Permanent",
|
|
"subType": "General",
|
|
"message": "Mailbox unavailable",
|
|
},
|
|
},
|
|
}
|
|
|
|
body = json.dumps(payload)
|
|
timestamp = str(int(time.time() * 1000))
|
|
signature = _signature(WEBHOOK_SECRET, timestamp, body)
|
|
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"X-UseSend-Signature": signature,
|
|
"X-UseSend-Timestamp": timestamp,
|
|
"X-UseSend-Event": payload["type"],
|
|
"X-UseSend-Call": f"call_{uuid.uuid4().hex[:10]}",
|
|
}
|
|
|
|
response = requests.post(WEBHOOK_URL, data=body, headers=headers, timeout=10)
|
|
print("Status:", response.status_code)
|
|
print("Body:", response.text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|