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 to prevent replay attacks.
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.
To rotate without downtime:
- Deploy your endpoint to accept signatures from both the old and new secret
- Rotate the secret in Kit
- Verify deliveries succeed with the new secret
- Remove the old secret from your endpoint
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.
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 not retry until the next event fires.
Automatic Disabling
After 15 consecutive failed deliveries across any events, Kit disables the webhook automatically.
When disabled:
- No further deliveries are attempted
- Status changes to Disabled with a
disabled_attimestamp
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, in_progress, completed, or errored
|
| 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 |
Created, waiting to be sent |
in_progress |
Currently being delivered |
completed |
Endpoint returned a 2xx response |
errored |
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