python 101 lines · 3 tabs

Verifying Stripe Webhook Signatures With a Reusable FastAPI Dependency

Shared by codesnips Jul 2026
3 tabs
import hmac
import hashlib
import time
from fastapi import Request, HTTPException, status


def parse_signature_header(header: str) -> tuple[int, list[str]]:
    timestamp: int | None = None
    signatures: list[str] = []
    try:
        for part in header.split(","):
            key, _, value = part.strip().partition("=")
            if key == "t":
                timestamp = int(value)
            elif key == "v1":
                signatures.append(value)
    except ValueError:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Malformed signature header")

    if timestamp is None or not signatures:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing signature parts")
    return timestamp, signatures


def verify_signature(secret: str, body: bytes, timestamp: int, signatures: list[str]) -> None:
    signed_payload = f"{timestamp}.".encode() + body
    expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
    if not any(hmac.compare_digest(expected, s) for s in signatures):
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Signature verification failed")


class RequireWebhookSignature:
    def __init__(self, secret: str, header_name: str = "Stripe-Signature", tolerance_seconds: int = 300):
        self.secret = secret
        self.header_name = header_name
        self.tolerance_seconds = tolerance_seconds

    async def __call__(self, request: Request) -> bytes:
        header = request.headers.get(self.header_name)
        if not header:
            raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing signature header")

        timestamp, signatures = parse_signature_header(header)
        if abs(time.time() - timestamp) > self.tolerance_seconds:
            raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Signature timestamp outside tolerance")

        body = await request.body()
        verify_signature(self.secret, body, timestamp, signatures)
        return body
3 files · python Explain with highlit

Webhooks arrive over the public internet, so an endpoint must prove that a payload really came from the provider before acting on it. The standard defense is an HMAC signature: the sender hashes the raw request body with a shared secret, and the receiver recomputes the same hash and compares. This snippet packages that verification into a single FastAPI dependency so every webhook route gets the same protection without duplicating crypto code.

The webhook_security module holds the core logic. parse_signature_header splits a Stripe-style t=...,v1=... header into a timestamp and one or more candidate signatures, tolerating malformed input by raising a 401. verify_signature reconstructs the signed payload as "{timestamp}.{body}", computes an HMAC-SHA256 with the endpoint secret, and compares it to each supplied signature using hmac.compare_digest. That constant-time comparison matters: a naive == leaks timing information that can let an attacker recover the signature byte by byte.

Two edge cases are handled explicitly. The timestamp is checked against tolerance_seconds so a captured request cannot be replayed hours later — this is the replay-attack guard. And the dependency reads await request.body() rather than a parsed model, because signatures are computed over the exact bytes on the wire; re-serializing parsed JSON would change whitespace or key order and break verification.

The RequireWebhookSignature class is a small callable configured with a secret and tolerance, which makes it reusable across providers with different secrets. FastAPI treats an instance as a dependency, and Depends injects the verified raw body into the handler.

In webhooks router, the stripe_webhook route declares Depends(stripe_signature) and receives the trusted raw_body. Only after verification does it parse JSON and enqueue work, keeping the signature check strictly ahead of any business logic. The dedupe_key derived from the event id supports idempotent processing, since providers retry deliveries and the same event may arrive twice.

This pattern centralizes a security-critical check, keeps route handlers thin, and makes it trivial to add new verified webhook endpoints by injecting another configured verifier.


Related snips

ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
ruby
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post

data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)

HMAC signed API requests for webhook and partner integrity

hmac api-signing webhooks
by Kai Nakamura 2 tabs
kotlin
package com.example.myapp

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp

Dependency injection with Hilt

kotlin android hilt
by Alex Chen 3 tabs
bash
#!/usr/bin/env bash
set -euo pipefail

export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"

Secrets management with environment isolation and Vault

secrets-management vault environment-variables
by Kai Nakamura 1 tab
php
<?php

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

Laravel service container and dependency injection

laravel dependency-injection service-container
by Carlos Mendez 2 tabs
typescript
import crypto from 'node:crypto';
import jwt from 'jsonwebtoken';

export function signAccessToken(payload: object) {
  return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '15m' });
}

JWT access + refresh token rotation (conceptual)

auth security node
by Mateo Rodriguez 1 tab

Share this code

Here's the card — post it anywhere.

Verifying Stripe Webhook Signatures With a Reusable FastAPI Dependency — share card
Link copied