Logo

Verify Paddle Billing Webhook Signatures in Python

Learn how to verify that webhooks hitting your back-end are from Paddle and not anyone else.

Published: 11 Jan, 2024


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

The Paddle-Signature header is what you want. 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.

Extract 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):
  pass


@app.post("/paddle-webhooks")
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.

Parse the header

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

It contains a timestamp and the HMAC separated by a semicolon.

Write a function to extract those two parts while eliminating everything you don’t need:

def split_signature(sig_header):
  timestamp, signature = sig_header.split(";")
  return timestamp.split("=")[1], signature.split("=")[1]

This function will return a tuple containing only the timestamp and HMAC: (1671552777, eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151)

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

Generate 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 them 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 hmac.new(secret_key, 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.

Compare the signatures

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.

Done

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.


Email me if you have any questions about this post.

Subscribe to the RSS feed to keep updated.