A webhook is an HTTP POST to an endpoint you control, triggered by an event at a third-party service. Stripe sends a payment.succeeded event. GitHub sends a push event. PagerDuty sends an alert.triggered event. Your endpoint receives the POST and acts on it. The security question: how do you know the POST actually came from Stripe or GitHub and not from an attacker who knows your webhook URL?
Signature Verification
Every serious webhook provider signs their payloads with HMAC-SHA256 using a shared secret. Stripe includes a Stripe-Signature header; GitHub includes an X-Hub-Signature-256 header. Your endpoint must verify this signature before processing the event.
import hmac
import hashlib
def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(f"sha256={expected}", signature_header)Timestamp Validation and Replay Prevention
Signature verification alone does not prevent replay attacks — an attacker who intercepts a legitimate signed webhook can re-submit it hours later. Include timestamp validation: reject any webhook with a timestamp older than 5 minutes. This limits the replay window to a period where the signature is still valid but the event has already been processed.
Idempotent Processing
Webhooks are delivered at-least-once — the sending service will retry on failure. Your endpoint will receive the same event multiple times. Make processing idempotent: use the event ID as a deduplication key, store processed event IDs in a cache or database, and skip processing for events you have already handled.
G8KEPR validates outbound webhook signatures for all event deliveries and includes an event ID and timestamp in every payload. Inbound webhook verification for connected integrations (Slack, PagerDuty, GitHub) is handled at the gateway layer.
