How to Receive HubSpot Webhooks in FastAPI

Read request.body() before any json() call

FastAPI is the modern Python web framework of choice for new projects in 2026. Pydantic-style request models are great for application APIs but actively get in the way of webhooks — you need the raw bytes the sender signed, not a parsed Pydantic object. This guide walks through the FastAPI setup for HubSpot webhooks end to end: capturing the raw body, verifying the signature, handling retries idempotently, and iterating locally without redeploying. Cross-reference the HubSpot Webhooks overview for the event catalog and sample payload.

HubSpot Official Webhook Docs

1. Set Up the FastAPI Endpoint

The endpoint needs to do three things, in this order: read the raw body, verify the signature against those exact bytes, and only then parse the JSON for your business logic.

# main.py
from fastapi import FastAPI, Request, HTTPException
import json

app = FastAPI()


@app.post("/api/webhooks/{service}")
async def webhook(service: str, request: Request):
    # IMPORTANT: read the raw body FIRST.
    # Once you call request.json() the body is consumed.
    raw_body = await request.body()
    signature = request.headers.get("x-signature-header", "")

    # 1. Verify HMAC over raw_body
    # 2. Parse JSON only after verification passes
    # 3. Process the event idempotently (use the event id as your key)

    event = json.loads(raw_body)
    print("Verified webhook:", event.get("type") or event)

    return {"status": "ok"}
Raw body, every time
Don't declare the body as a Pydantic model on the webhook route — FastAPI will deserialize it eagerly and the raw bytes used by the signing algorithm are gone. Take a fastapi.Request and call await request.body() yourself. If you need the parsed payload AFTER verification, run json.loads(raw_body) in the handler.

2. Verify the HubSpot Signature

Signing details
Algorithm
HMAC-SHA256
Header
X-HubSpot-Signature-v3
Encoding
base64

HubSpot v3 signs the concatenation of `{HTTP_METHOD}{request_URI}{raw_request_body}{X-HubSpot-Request-Timestamp}` with your app's Client Secret. URL-encoded characters in the URI must be decoded before signing (except the `?` that begins the query string). Reject requests older than 5 minutes.

Python verification

import hmac, hashlib, base64, os, time
from urllib.parse import unquote
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/webhooks/hubspot")
async def hubspot_webhook(request: Request):
    body = await request.body()
    signature = request.headers.get('x-hubspot-signature-v3', '')
    timestamp = request.headers.get('x-hubspot-request-timestamp', '0')

    if abs(int(time.time() * 1000) - int(timestamp)) > 1000 * 60 * 5:
        raise HTTPException(status_code=401, detail='stale request')

    uri = unquote(str(request.url))
    string_to_sign = (request.method + uri + body.decode() + timestamp).encode()
    expected = base64.b64encode(
        hmac.new(
            os.environ['HUBSPOT_CLIENT_SECRET'].encode(),
            msg=string_to_sign,
            digestmod=hashlib.sha256,
        ).digest()
    ).decode()
    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=403, detail='invalid signature')
    return {'ok': True}

Wire this verification call into the FastAPI handler from section 1. The pattern is identical across FastAPI versions: read raw body, verify, parse JSON, dispatch.

Watch out: HubSpot v3 includes the HTTP method and full URI in the signed string — proxies that change the host or path break verification. Use the Client Secret of your app, not your private app's API key. v1 and v2 algorithms exist but are legacy; new integrations must use v3.

See HubSpot's official signing docs for the canonical reference, or the cross-service signature verification guide for the same pattern in Ruby and other languages.

3. Make the Handler Idempotent

HubSpot can — and will — send the same event twice. Network blips, your server returning a 5xx mid-processing, deploy windows: any of these triggers a retry, and your handler will see the same event id again. Build for that on day one rather than chasing duplicate-charge bugs in production.

The simplest pattern is a unique constraint on the event id in your database. The handler does the work inside a transaction, and the insert into the events table is the last step — if a retry arrives, the unique-constraint violation tells you the event already committed and you can return 200 without re-running the side effects.

Pattern in any framework:

  1. Read raw body, verify signature.
  2. Begin transaction.
  3. Apply business logic (charge, fulfil, notify, etc.).
  4. Insert event id into processed_events with a unique constraint.
  5. Commit. Return 200.
  6. On unique-constraint violation, return 200 — the event was already processed by a prior delivery.

4. HubSpot Retry Behaviour

Retry policy
Max attempts
10
Total window
Up to ~8 hours
Backoff
Exponential, starts after a few minutes
Retries on
5xx, 429, timeouts (5s)
Stops on
Any 2xx response within 5s

Combine the retry numbers above with the idempotency pattern in section 3: aim to acknowledge fast (return 200 under the timeout) and let the idempotency table absorb any duplicates from in-flight retries. The full pattern, including dead-letter queues and replay-from-capture, lives in the Webhook Retry Strategies guide.

5. Test Locally Without Deploying

The fastest iteration loop for any webhook handler is: capture a real HubSpot event with HookRay, then replay that captured request against your local FastAPI server until the verification + business logic both pass. No need to retrigger the event in HubSpot, no need to redeploy.

  1. Get a free webhook URL at hookray.com — no signup.
  2. Paste the URL into your HubSpotdashboard's webhook settings.
  3. Trigger a test event. HookRay shows the headers, raw body, and parsed payload in real time.
  4. Use HookRay's replay feature to send the captured request against http://localhost:3000/api/webhooks/hubspot (or wherever your FastAPI app is listening) — iterate on your code without re-poking the HubSpot dashboard.

Deploying the FastAPI Handler

FastAPI is typically deployed behind Uvicorn or Gunicorn + Uvicorn workers. For webhook handlers specifically, ensure your reverse proxy (Nginx, Cloud Load Balancing, etc.) doesn't buffer or rewrite request bodies — some default configurations strip trailing whitespace or normalise charset, which silently breaks HMAC.

Need a host that boots quickly enough to absorb webhook bursts? DigitalOcean droplets stay warm, support raw-body proxies cleanly, and avoid the cold-start traps of some serverless runtimes.

Capture a real HubSpot webhook in 30 seconds

Free webhook URL, real-time payload inspection, one-click replay. No signup required.

Start Testing — Free