Verify Paddle Billing Webhook Signatures in Python

When setting up your server to receive webhook notifications from Paddle, how do you know the webhook is from Paddle and not anyone else?

When setting up your server to receive webhook notifications from Paddle, how do you know the webhook is from Paddle and not anyone else?

By making use of the Paddle-Signature header of course. Paddle will send this header along with every webhook request to your server.

The signature is simply an HMAC of the request body hashed with SHA256. All you have to do is generate it again using your secret key and then check if the result is the same as the one Paddle sent.

Getting the Header

Most web frameworks will have an object representing requests. You can simply extract the Paddle-Signature header from there.

Here's how you'd do it in FastAPI:

import json
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel

class Webhook(BaseModel):
  event_id: str
  event_type: str
  occurred_at: str
  notification_id: str
  data: dict

def verify_signature(sig, raw_body):
def process_webhook(webhook: Webhook, paddle_signature: Annotated[str | None, Header()] = None):
  verify_signature(paddle_signature, json.dumps(webhook))
  return {"message": "OK"}

FastAPI allows you to deal with headers as regular parameters and will also parse the request body as JSON for you.

However, to verify the signature, you'll need the raw request body, so you dump it as a string using the json library's dumps method.

Frameworks like Django allows you to access the raw bytes as request.body instead, so feel free to adjust the code for your situation.

Parsing the Header

The value of this header will look like ts=1671552777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151

Notice how there are two parts separated by a semicolon:

  1. A timestamp.
  2. The actual HMAC.

Write a function to extract those two parts.

def split_signature(sig_header):
  timestamp, signature = sig_header.split(";")
  return timestamp.lstrip("ts="), signature.lstrip("h1=")

This function will return a tuple containing the timestamp and HMAC without all the extra stuff like so: (1671552777, eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151)

Next, you'll need an HMAC to compare with the one Paddle provided.

Generating the HMAC

Luckily, Python's standard library has everything you need to deal with HMACs and hashes. You'll use the hmac library to generate the signature and the hashlib library to handle SHA256. Isn't Python a great language?

Use it now.

import hmac
import hashlib

def generate_hmac(timestamp, raw_body):
  # For Django users: payload = f"{timestamp}:".encode('utf-8') + raw_body
  payload = f"{timestamp}:{raw_body}".encode("utf-8")
  secret_key = "YOUR SECRET KEY".encode('utf-8')
  return, msg=payload, digestmod=hashlib.sha256).hexdigest()

To get the payload that you need to sign as HMAC, you concatenate the timestamp provided by Paddle, and the raw request body.

Both the payload and the header need to be in bytes before being encoded, so you encode them before passing them to the hmac new constructor.

Paddle doesn't document it, but their signature is in hex format. That's why you use the hexdigest method to return your own as hex as well.

Verifying the Signature

Now that you have these two functions, use them in your verify_signature function.

def verify_signature(sig, raw_body):
  timestamp, paddle_signature = split_signature(sig)
  our_signature = generate_hmac(timestamp, raw_body)
  if paddle_signature != our_signature:
    raise APIException(status_code=403, detail="Invalid signature")

If your signature isn't the same as Paddle's you can raise an exception that will prevent further execution depending on your web framework of choice. In this case, I'm sending a 403 Forbidden status code.


As you can see, Python makes it dead easy to deal with the Paddle signature.

You simply split the header into the parts you're interesting in, generate an HMAC using built-in libraries, and then compare it with Paddle's.

If you found this post helpful, please subscribe to my free newsletter to get notified when I write more like it.