Verify Eventually webhook signatures
Every webhook delivery is an HTTP POST signed with HMAC-SHA256 so you can confirm it came from Eventually and wasn't tampered with. Verify each delivery against your endpoint's signing secret before you process it.
Delivery headers
Each delivery is an HTTP POST with a JSON body and these headers:
Content-Type: application/jsonUser-Agent: Eventually-Webhooks/1.0X-Eventually-Event— The event identifier, for exampleregistration.created.X-Eventually-Delivery-Id— A stable ID for this delivery, unchanged across retries.X-Eventually-Signature— The signature, in the formt=<unix timestamp>,v1=<hex HMAC-SHA256>.
The signature's HMAC is computed over "<timestamp>.<raw request body>" with your endpoint's signing secret.
Verify the signature
Always verify against the raw request body, before any JSON parsing or re-serialization. Re-serializing the JSON produces a different body and a signature that won't match.
Ruby:
def verify_eventually_signature(request, secret, tolerance: 300)
header = request.headers["X-Eventually-Signature"].to_s
parts = header.split(",").to_h { |kv| kv.split("=", 2) }
timestamp = parts["t"].to_i
return false if (Time.now.to_i - timestamp).abs > tolerance
body = request.body.read
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{body}")
ActiveSupport::SecurityUtils.secure_compare(expected, parts["v1"].to_s)
endNode:
const crypto = require("crypto")
function verifyEventuallySignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.split("=")))
const timestamp = parseInt(parts.t, 10)
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""))
}Reject the delivery if the signature doesn't match, and respond with a non-2xx status so Eventually retries it.
Deduplicate on the delivery ID
Use X-Eventually-Delivery-Id to avoid processing the same event twice. The ID stays the same across retries and across redeliveries, so store the IDs you've already handled and skip any you've seen. When a redelivery arrives with an ID you've seen, drop the duplicate only if the first copy was fully processed — a redelivery usually means the original attempt failed on your side.
Endpoint requirements
Your endpoint must respond with a 2xx status within 10 seconds. Redirects and other non-2xx responses count as failed attempts. Respond first, then process the payload.
Webhook payloads can contain attendee and event information, so secure your receiver and control who can access it.
Retries and automatic disablement
A failed delivery retries up to 5 times with increasing backoff. After 10 consecutive exhausted deliveries, the endpoint is automatically disabled. Fix your server, then re-enable the endpoint from Settings — re-enabling resets the failure counter. See Set up outbound webhooks for how to re-enable an endpoint.