## 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

```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

```javascript
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

```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 attempts delivery up to **5 times** when your endpoint is unreachable or returns an error.

| Attempt | Behavior |
|---------|----------|
| 1 | Immediate |
| 2–5 | Polynomially increasing wait times |

| Setting | Value |
|---------|-------|
| Connect timeout | 10 seconds |
| Read timeout | 15 seconds |

A `2xx` response marks the delivery as **completed**. Any other status code or a timeout marks it as **errored** and triggers a retry.

## 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 endpoint returned an 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 |
| 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