# Webhooks Pass `webhook_url` on a request, or set a default for the account at [watermark.ws/developers](https://watermark.ws/developers), and the API posts the finished request to it as JSON: ```json { "id": "evt_1234_done", "object": "event", "type": "request.succeeded", "created": 1791738000, "data": { "id": 1234, "object": "request", "status": "succeeded", "result": { "…": "…" } } } ``` Event types: `request.succeeded`, `request.failed`, `request.aborted`. Answer with any `2xx` within 10 seconds; anything else is retried 3 times with backoff (about 4, 5 and 7 seconds apart). Use the event `id` to ignore a repeat. ## Verifying the signature Every delivery carries a `Watermark-Signature` header: ``` Watermark-Signature: t=1791738000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` `v1` is the HMAC-SHA256 of `"{t}.{raw body}"`, keyed with your account's webhook secret (`whsec_…`, shown in the Developers area). Compare it in constant time, and refuse a `t` more than 5 minutes old so a recorded delivery cannot be replayed. ### PHP ```php [$t, $v1] = sscanf($_SERVER['HTTP_WATERMARK_SIGNATURE'], 't=%d,v1=%s'); $body = file_get_contents('php://input'); $valid = hash_equals(hash_hmac('sha256', "{$t}.{$body}", $secret), $v1) && abs(time() - $t) <= 300; ``` ### Node.js ```js const crypto = require('crypto'); const [, t, v1] = req.headers['watermark-signature'].match(/^t=(\d+),v1=([0-9a-f]+)$/); const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) <= 300; ``` ### Python ```python import hmac, hashlib, re, time t, v1 = re.match(r"t=(\d+),v1=([0-9a-f]+)", signature_header).groups() expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() valid = hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) <= 300 ``` Webhook URLs follow the same rules as sources: public `http` or `https` addresses only.