Skip to main content

Security

HTTP headers

Event delivery headers

Every webhook event delivery includes the following headers:

HeaderValue
Content-Typeapplication/json
Content-LengthByte length of the body
X-Harvestr-Webhook-IdThe webhook subscription ID
X-Harvestr-Webhook-SignatureHMAC-SHA256 signature of the body (only if a secret token is configured)
User-Agentharvestr-webhook/1.0.0

Challenge validation headers

The challenge request sent during endpoint validation uses a different signature header:

HeaderValue
Content-Typeapplication/json
X-Harvestr-SignatureHMAC-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_ERRORS and deliveries stop.