Webhook Security & Delivery
Verify webhook signatures, understand retry behavior, and troubleshoot delivery issues.
Why It Matters
Signature verification proves payloads are authentic and unmodified. Understanding retry and auto-disable behavior helps you build resilient integrations.
Verifying Signatures
Every delivery is signed with your endpoint’s signing secret using HMAC-SHA256. The signature covers the timestamp and request body. Preventing replay attacks also requires the request-age check described below.
The signed content is the ISO 8601 timestamp and JSON body joined with a period:
<timestamp>.<body>
Ruby
timestamp = request.headers["X-Webhook-Timestamp"]
signature = request.headers["X-Webhook-Signature"]
body = request.body.read
expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, "#{timestamp}.#{body}")
if ActiveSupport::SecurityUtils.secure_compare(expected, signature)
# Signature is valid
else
head :unauthorized
end
Node.js
const crypto = require("crypto");
function verifySignature(signingSecret, timestamp, body, signature) {
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Python
import hmac
import hashlib
def verify_signature(signing_secret, timestamp, body, signature):
expected = hmac.new(
signing_secret.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Preventing Replay Attacks
Compare X-Webhook-Timestamp against the current time and reject requests older than 5 minutes.
Rotating Your Signing Secret
Click Rotate Secret on the webhook detail page to generate a new signing secret. The old secret is immediately invalidated.
The new secret is only available after rotation. Prepare to update your endpoint configuration promptly:
- Rotate the secret in Kit
- Copy the new secret into your endpoint configuration
- Send a test and verify that signature validation succeeds
- Remove the old secret from your endpoint configuration if it is still stored
Between rotation and the endpoint update, deliveries may be rejected. This procedure does not provide zero-downtime rotation.
Delivery & Retries
Kit POSTs the JSON payload to your endpoint with these timeouts:
| Setting | Value |
|---|---|
| Connect timeout | 10 seconds |
| Read timeout | 15 seconds |
A 2xx response marks the delivery as completed. Anything else is a failure, and how Kit handles it depends on whether the failure is transient or permanent.
flowchart TD
post["POST payload to endpoint"] --> ok{"2xx response?"}
ok -- "Yes" --> completed["Delivery completed"]
ok -- "No" --> kind{"Transient failure? (5xx, 408, 429, or network error)"}
kind -- "No: any other 4xx (permanent)" --> errored["Delivery errored, not retried"]
kind -- "Yes" --> attempts{"Fewer than 5 attempts?"}
attempts -- "Yes" --> backoff["Wait with polynomial backoff"]
backoff --> post
attempts -- "No: retries exhausted" --> errored
errored --> threshold{"15 consecutive failed deliveries?"}
threshold -- "Yes" --> disabled["Webhook disabled automatically"]
threshold -- "No" --> nextevent["Waits for the next event"]
disabled -- "Resume: failure counter reset" --> active["Webhook active again"]
Transient failures (retried)
Transient failures are problems that are likely to resolve on their own: your endpoint was briefly overloaded, timed out, or couldn’t be reached. Kit retries these up to 5 attempts with polynomial backoff (each wait longer than the last).
A failure is treated as transient when the response is:
- HTTP
500–599(server errors) - HTTP
408(Request Timeout) - HTTP
429(Too Many Requests) - A network error or timeout (connection refused, DNS failure, read timeout)
| Attempt | Behavior |
|---|---|
| 1 | Immediate |
| 2–5 | Polynomially increasing wait times |
Permanent failures (not retried)
Permanent failures indicate the request itself is unacceptable to your endpoint and retrying won’t help. Kit marks the delivery as errored immediately and does not retry.
A failure is treated as permanent when the response is any other 4xx status, for example 400, 401, 404, or 410. If your endpoint is returning one of these by mistake, fix the route or authentication on your side; Kit will attempt delivery of the next event, but will not retry this failed delivery.
Automatic Disabling
After 15 consecutive failed deliveries across any events, Kit disables the webhook automatically.
When disabled:
- No further deliveries are attempted
- The endpoint shows a red Disabled badge, with a note that it was shut off after too many consecutive failures
Click Resume to re-enable. This resets the failure counter and restores the webhook to active.
Delivery Logs
Each webhook endpoint has a Deliveries tab showing the 50 most recent deliveries. Each entry includes:
| Field | Description |
|---|---|
| Event | Event type that triggered the delivery |
| Status | Pending, Delivered, or Failed |
| Attempts | Number of delivery attempts |
| Request headers | Headers sent with the request |
| Response | HTTP status code or error message |
| Timestamp | When the delivery was created |
Delivery Statuses
| Status | Meaning |
|---|---|
| Pending | Queued or currently being sent; both share this badge, so there is no separate in-progress state |
| Delivered | Endpoint returned a 2xx response |
| Failed | All retries exhausted, or the endpoint returned a permanent (4xx) error |
Data Retention
Delivery logs are retained for 30 days and then automatically deleted. Store payloads on your end if you need longer retention.
URL Requirements
- HTTPS required: HTTP endpoints are rejected
- Public IPs only: URLs resolving to private addresses (localhost, 10.x.x.x, 192.168.x.x, etc.) are blocked
- URLs are validated at creation time and before each delivery
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Signature mismatch | Wrong signing secret or body modified before verification | Verify against the raw request body with the current secret |
| Deliveries timing out | Endpoint takes longer than 15 seconds | Return 200 immediately and process asynchronously |
| Delivery errored with no retry | Endpoint returned a permanent 4xx (e.g. 404, 410) |
Fix the route, auth, or method; only transient errors (5xx, 408, 429, network/timeout) are retried |
| Webhook auto-disabled | 15 consecutive failures | Fix your endpoint, then click Resume |
| URL rejected | Private IP or HTTP URL | Use an HTTPS URL that resolves to a public IP |
| Events not firing | Webhook paused or event type not subscribed | Check webhook status and subscribed events |
Quick Checklist
- Copy your signing secret from the webhook detail page
- Implement HMAC-SHA256 signature verification in your endpoint
- Reject requests with timestamps older than 5 minutes
-
Return a
2xxstatus code within 15 seconds - Handle retries idempotently (you may receive the same event more than once)
- Monitor the delivery logs for errors
- Set up alerts for when a webhook is auto-disabled