Skip to content
Learn Agentic AI
Learn Agentic AI12 min read5 views

Webhook Signature Verification: Securing Inbound Events for AI Agent Systems

Implement webhook signature verification to secure inbound events for AI agents. Covers HMAC-SHA256 signatures, timestamp validation, replay attack prevention, and production-ready FastAPI middleware.

Why Webhook Security Is Non-Negotiable

AI agent systems often receive events from external services — a payment processed via Stripe, a commit pushed to GitHub, a ticket created in Jira. These events arrive as HTTP POST requests to your webhook endpoint. Without verification, an attacker can send fabricated events to trigger agent actions: fake payment confirmations, spoofed deployment triggers, or forged customer messages.

Webhook signature verification ensures that every inbound event genuinely originated from the expected sender and has not been modified in transit. This is a foundational security requirement for any AI agent that takes actions based on external events.

How HMAC Signatures Work

The sender and receiver share a secret key. When the sender dispatches a webhook, it computes an HMAC (Hash-based Message Authentication Code) over the request body using the shared secret and includes the resulting signature in a header. The receiver recomputes the HMAC using the same secret and compares the signatures. If they match, the payload is authentic and unmodified.

flowchart LR
    CLIENT(["Client SDK"])
    GW["API Gateway<br/>auth plus rate limit"]
    APP["FastAPI app<br/>handlers and DI"]
    VAL["Pydantic validation"]
    SVC["Service layer<br/>business logic"]
    DB[(Database)]
    QUEUE[(Background queue)]
    OBS[(Tracing)]
    CLIENT --> GW --> APP --> VAL --> SVC
    SVC --> DB
    SVC --> QUEUE
    SVC --> OBS
    SVC --> CLIENT
    style GW fill:#4f46e5,stroke:#4338ca,color:#fff
    style APP fill:#f59e0b,stroke:#d97706,color:#1f2937
    style DB fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b

The standard algorithm is HMAC-SHA256, which provides both authentication (the sender knows the secret) and integrity (the payload has not been altered).

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →

Building the Verification Module

Here is a reusable webhook signature verification module:

# webhooks/verification.py
import hmac
import hashlib
import time
from fastapi import HTTPException, Request

MAX_TIMESTAMP_AGE_SECONDS = 300  # 5 minutes

def compute_signature(payload: bytes, secret: str, timestamp: str) -> str:
    """Compute HMAC-SHA256 signature over timestamp + payload."""
    message = f"{timestamp}.".encode() + payload
    return hmac.new(
        secret.encode(),
        message,
        hashlib.sha256,
    ).hexdigest()

def verify_signature(
    payload: bytes,
    secret: str,
    received_signature: str,
    timestamp: str,
) -> bool:
    """Verify webhook signature with timing-safe comparison."""
    expected = compute_signature(payload, secret, timestamp)
    return hmac.compare_digest(expected, received_signature)

Two critical details in this code. First, the timestamp is included in the signed message, binding the signature to a specific moment in time. Second, hmac.compare_digest performs a constant-time comparison that prevents timing attacks — an attacker cannot deduce the correct signature by measuring response times.

Timestamp Validation to Prevent Replay Attacks

Even with valid signatures, an attacker who intercepts a webhook can replay it later. Timestamp validation prevents this by rejecting events that are too old:

def validate_timestamp(timestamp: str) -> None:
    """Reject webhooks with timestamps older than the threshold."""
    try:
        event_time = int(timestamp)
    except (ValueError, TypeError):
        raise HTTPException(status_code=400, detail="Invalid timestamp format")

    current_time = int(time.time())
    age = abs(current_time - event_time)

    if age > MAX_TIMESTAMP_AGE_SECONDS:
        raise HTTPException(
            status_code=403,
            detail=f"Webhook timestamp too old: {age}s exceeds {MAX_TIMESTAMP_AGE_SECONDS}s limit",
        )

FastAPI Dependency for Webhook Verification

Wrap the verification logic into a reusable FastAPI dependency:

from fastapi import Depends, Header
from typing import Annotated

class WebhookVerifier:
    def __init__(self, secret_env_var: str):
        import os
        self.secret = os.environ[secret_env_var]

    async def __call__(
        self,
        request: Request,
        x_webhook_signature: Annotated[str, Header()],
        x_webhook_timestamp: Annotated[str, Header()],
    ) -> bytes:
        # Read the raw body
        body = await request.body()

        # Validate timestamp
        validate_timestamp(x_webhook_timestamp)

        # Verify signature
        if not verify_signature(body, self.secret, x_webhook_signature, x_webhook_timestamp):
            raise HTTPException(
                status_code=403,
                detail="Invalid webhook signature",
            )

        return body

# Create verifiers for each provider
verify_stripe = WebhookVerifier("STRIPE_WEBHOOK_SECRET")
verify_github = WebhookVerifier("GITHUB_WEBHOOK_SECRET")

Using the Verifier in Agent Webhook Endpoints

Apply the dependency to any webhook handler:

import json
from fastapi import APIRouter, Depends

router = APIRouter(prefix="/webhooks")

@router.post("/stripe")
async def handle_stripe_webhook(
    body: bytes = Depends(verify_stripe),
):
    event = json.loads(body)
    event_type = event.get("type")

    if event_type == "invoice.paid":
        await agent_billing.process_payment(event["data"]["object"])
    elif event_type == "customer.subscription.deleted":
        await agent_provisioning.deactivate_tenant(event["data"]["object"])

    return {"status": "processed"}

@router.post("/github")
async def handle_github_webhook(
    body: bytes = Depends(verify_github),
):
    event = json.loads(body)
    action = event.get("action")

    if action == "opened" and "pull_request" in event:
        await code_review_agent.review_pr(event["pull_request"])

    return {"status": "processed"}

Idempotency for Webhook Processing

Webhook providers retry on failure, which means your endpoint may receive the same event multiple times. Use an idempotency key to ensure each event is processed exactly once:

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

async def process_webhook_idempotently(
    event_id: str, processor, event_data: dict,
):
    # Check if already processed
    cache_key = f"webhook_processed:{event_id}"
    already_processed = await redis_client.get(cache_key)
    if already_processed:
        return {"status": "already_processed"}

    # Process the event
    result = await processor(event_data)

    # Mark as processed with a TTL (e.g., 72 hours)
    await redis_client.setex(cache_key, 72 * 3600, "1")
    return result

Sending Signed Webhooks from Your Platform

When your AI agent platform sends webhooks to customers, sign them the same way:

import httpx

async def send_webhook(url: str, payload: dict, secret: str):
    body = json.dumps(payload).encode()
    timestamp = str(int(time.time()))
    signature = compute_signature(body, secret, timestamp)

    async with httpx.AsyncClient() as client:
        response = await client.post(
            url,
            content=body,
            headers={
                "Content-Type": "application/json",
                "X-Webhook-Signature": signature,
                "X-Webhook-Timestamp": timestamp,
            },
            timeout=10.0,
        )
    return response.status_code

FAQ

Why include the timestamp in the signature instead of just signing the body?

Signing the body alone means the signature is valid forever. An attacker who intercepts a legitimate webhook can replay it at any time — days, weeks, or months later. By including the timestamp in the signed message, the signature is bound to a specific time window. Even if intercepted, the event can only be replayed within the tolerance window (typically five minutes).

How do I handle webhook signature verification for providers like Stripe that use their own format?

Major providers use slightly different signing schemes. Stripe uses whsec_ prefixed secrets and a specific header format. GitHub uses X-Hub-Signature-256. Write provider-specific verifier classes that inherit from a base verifier but override the header names and signature computation. Most providers document their signing algorithm, so adaptation is straightforward.

What should I do if webhook verification fails?

Return an appropriate HTTP error (401 or 403) with a generic message — never reveal which part of the verification failed. Log the failure with the source IP, headers, and timestamp for security monitoring. If you see repeated verification failures from the same source, consider rate limiting or blocking that IP. Alert your security team if failure rates spike, as it may indicate an attack.


#Webhooks #HMAC #Security #FastAPI #AIAgents #EventDriven #AgenticAI #LearnAI #AIEngineering

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

Agentic AI

Input and Output Guardrails in the OpenAI Agents SDK: A Production Pattern (2026)

Stop the agent BEFORE it does the wrong thing. How to wire input and output guardrails in the OpenAI Agents SDK with cheap classifiers and an eval suite that proves they work.

Agentic AI

Safety Evaluation for Agents: Jailbreak, Prompt Injection, and Tool-Misuse Test Suites in 2026

How to build a safety eval pipeline that runs known jailbreak corpora, prompt-injection attacks, and tool-misuse scenarios on every release — and gates merges on it.

Agentic AI

Building Your First Agent with the OpenAI Agents SDK in 2026: A Hands-On Walkthrough

Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.

Agentic AI

LangGraph Checkpointers in Production: Durable, Resumable Agents with Eval Replay

Use LangGraph's checkpointer to make agents resumable across crashes and human-in-the-loop pauses, then replay any checkpoint into your eval pipeline.

Agentic AI

LangGraph State-Machine Architecture: A Principal-Engineer Deep Dive (2026)

How LangGraph's StateGraph, channels, and reducers actually work — with a working multi-step agent, eval hooks at every node, and the patterns that survive production.

Agentic AI

Multi-Agent Handoffs with the OpenAI Agents SDK: The Pattern That Actually Scales (2026)

Handoffs done right — when one agent should hand control to another, how to preserve context, and how to evaluate the handoff decision itself.