Security
HTTP headers
Event delivery headers
Every webhook event delivery includes the following headers:
| Header | Value |
|---|---|
Content-Type | application/json |
Content-Length | Byte length of the body |
X-Harvestr-Webhook-Id | The webhook subscription ID |
X-Harvestr-Webhook-Signature | HMAC-SHA256 signature of the body (only if a secret token is configured) |
User-Agent | harvestr-webhook/1.0.0 |
Challenge validation headers
The challenge request sent during endpoint validation uses a different signature header:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Harvestr-Signature | HMAC-SHA256 signature of the challenge body |
Note: the challenge uses X-Harvestr-Signature, while event deliveries use X-Harvestr-Webhook-Signature.
HMAC signature verification
If you configured a secret token when creating your webhook, every delivery will include an X-Harvestr-Webhook-Signature header.
The signature is computed as:
HMAC-SHA256(body, secret_token) → hex digest
Where body is the raw JSON string of the request body and secret_token is the secret you provided.
Verification example (Node.js)
const crypto = require("crypto");
function verifyWebhookSignature(rawBody, signature, secretKey) {
const expected = crypto
.createHmac("sha256", secretKey)
.update(rawBody, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
// In your HTTP handler:
const signature = req.headers["x-harvestr-webhook-signature"];
const isValid = verifyWebhookSignature(req.rawBody, signature, YOUR_SECRET_KEY);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
Verification example (Python)
import hmac
import hashlib
def verify_webhook_signature(raw_body: bytes, signature: str, secret_token: str) -> bool:
expected = hmac.new(
secret_token.encode("utf-8"),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your HTTP handler:
signature = request.headers.get("X-Harvestr-Webhook-Signature")
is_valid = verify_webhook_signature(request.body, signature, YOUR_SECRET_KEY)
if not is_valid:
return Response("Invalid signature", status=401)
Timeout and retries
- Timeout: 5 seconds. If your endpoint does not respond within 5 seconds, the delivery is considered failed.
- Success: Any response with a 2xx status code (200-208) is considered successful.
- Failure: Non-2xx responses or network errors are counted as failures. After repeated failures, the webhook status is automatically set to
TOO_MANY_ERRORSand deliveries stop.