Changes

Comparing empty160dc49.

@@ -1,0 +1,79 @@
1+---
2+title: Webhooks
3+sort: 4
4+---
5+
6+# Webhooks
7+
8+ZephyrCart pushes events to endpoints you register. Endpoints must respond `2xx` within 10 seconds
9+to count as delivered.
10+
11+## Register an endpoint
12+
13+```bash
14+curl -sS https://api.zephyrcart.io/v1/webhook_endpoints \
15+ -H "Authorization: Bearer $ZEPHYR_API_KEY" \
16+ -d '{
17+ "url": "https://hooks.example.com/zephyr",
18+ "events": ["order.created", "order.refunded"],
19+ "description": "ERP bridge — production"
20+ }'
21+```
22+
23+The response includes a `signing_secret` starting `whsec_…`. Store it; you'll need it to verify
24+signatures.
25+
26+## Verify signatures
27+
28+Every delivery carries:
29+
30+- `X-ZephyrCart-Delivery-Id` — unique ULID per delivery
31+- `X-ZephyrCart-Delivery-Attempt` — `1` on first send, increments on retry
32+- `X-ZephyrCart-Timestamp` — Unix seconds, used in the signature
33+- `X-ZephyrCart-Signature` — `t=<ts>,v1=<hex-hmac-sha256>`
34+
35+The signed string is `<timestamp>.<raw-request-body>`. Compute HMAC-SHA256 with the signing secret
36+and compare in constant time:
37+
38+```python
39+import hmac, hashlib, time
40+
41+def verify(payload: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
42+ parts = dict(p.split("=", 1) for p in header.split(","))
43+ ts, sig = int(parts["t"]), parts["v1"]
44+ if abs(time.time() - ts) > tolerance_seconds:
45+ return False
46+ expected = hmac.new(
47+ secret.encode(),
48+ f"{ts}.".encode() + payload,
49+ hashlib.sha256,
50+ ).hexdigest()
51+ return hmac.compare_digest(expected, sig)
52+```
53+
54+A request that fails verification must be rejected with HTTP `401`. Do not log the body.
55+
56+## Retries
57+
58+Deliveries retry on any `non-2xx` or timeout, with exponential backoff capped at 24 hours total —
59+roughly `1m, 5m, 15m, 1h, 3h, 6h, 12h`, then give up. Persistent failures put the endpoint in
60+`status=failing` after 24 hours; the dashboard shows the last 100 delivery attempts.
61+
62+## Event catalogue
63+
64+| Event | When fired |
65+|--------------------------------|--------------------------------------------------|
66+| `cart.created` | New cart |
67+| `cart.updated` | Line items, addresses, or coupons changed |
68+| `cart.abandoned` | No activity for 60 minutes |
69+| `checkout.session.created` | Hosted checkout URL minted |
70+| `checkout.session.completed` | Payment authorised |
71+| `order.created` | Placed; first webhook after `checkout.completed` |
72+| `order.updated` | Mutable-field change |
73+| `order.refunded` | Each refund, full or partial |
74+| `order.cancelled` | Before fulfilment |
75+| `customer.created` | |
76+| `customer.merged` | `target` is folded into `source` |
77+| `product.created` | |
78+| `product.archived` | Soft-delete |
79+| `inventory.low` | Configurable threshold per product |