Logo StartupKit
EN

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:

  1. Deploy your endpoint to accept signatures from both the old and new secret
  2. Rotate the secret in Kit
  3. Verify deliveries succeed with the new secret
  4. 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 500599 (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_at timestamp

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 2xx status 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

Type to search...