Verifying Webhooks

With each webhook (for both Alerts/Events and webhooks used fulfilment) we send a signature field that can be used to verify the webhook was sent by Paddle.

We use public/ private key encryption to allow you to verify these requests. The information below should be a step-by-step guide on how to verify a Paddle signature.


1 Getting Your Public Key

We list your public key on your account settings page under the “Public Key” tab.

Your public key should look similar to the key below. (Note the key below is not your key.)


-----BEGIN PUBLIC KEY-----
3jiasSIDJojosda/asjdnFJSUHISABIiansfisauIUSH93hjiuJSNFiuhn3akmsf
8F/pZDYQDZeS/LZvWnorXTb7uamCsNuYOgh0/bmBDYOAIkoYvUrSadWHcBQj1hDy
BTScQKi/yY5J3aEv9syDgZcfxMdsjewiDJdhfIWnsnj4o0mRlNNQTb4QMWzShSSR
DrmoD5qelV/CJCZH9wmYy3oOpAZffFVTK+g+ouqVyBKViVH5Zum+opvx9Jz85XXN
IMVD+nkOUJTgC+MLftEF+8J6u7h8LBHaMyFzaY+O2gWhyPn7rkg5GJvXFmTpuXFc
6DZCxyzDwKG7YliJmgwcFRJaFJ6EPeeG+qUTsHoSVIikI0w2yunVFoFWCxKASVJM
6mYTM0Vtrhb/96mXY5jJQ4d3Zp3VL/o6SxDPkIo+bWAh4kt66/BPvUFY5bxItLmE
M6Z6ulQwNdAIz2TuIgj0efxGIzkXAvpzYYD/kUB789ObkDmfMKp/APW5vZT1UhqL
yV9xtIn7gIwAZxcQP6IxxvrZeZMAlF6vJpJkf1qyZgwlJmpWtJdXArA4gIU7VXe4
sYmVatElS/gM/V+83uDHisokqnc382uJshud802801jKNsshuhu9u09iJSDNuwjs
moil4WBGxur9OPADy8JGcOl4OdTETMoMMR1157iD8bTep6ZGD268ijPfpL6Kt7hB
rxovOHTh9+jrBNnnsIhbFp8CAwEAAQ==
-----END PUBLIC KEY-----

2 Getting the Signature

Each webhook sends the signature that you’ll be verifying as a parameter called p_signature.

3 Verifying the Signature

The process of verifying the signature is simple. The signature is comprised of a signed set of all of the fields and their values within the request you have been sent. The first step is to remove the p_signature field, as that isn’t included within the signature verification. We then ksort() the fields so they’re in the same order, followed by serializing and signing the resulting array.


<?php
  // Your Paddle 'Public Key'
  $public_key = '-----BEGIN PUBLIC KEY-----
3jiasSID...';
  
  // Get the p_signature parameter & base64 decode it.
  $signature = base64_decode($_POST['p_signature']);
  
  // Get the fields sent in the request, and remove the p_signature parameter
  $fields = $_POST;
  unset($fields['p_signature']);
  
  // ksort() and serialize the fields
  ksort($fields);
  foreach($fields as $k => $v) {
	  if(!in_array(gettype($v), array('object', 'array'))) {
		  $fields[$k] = "$v";
	  }
  }
  $data = serialize($fields);
  
  // Veirfy the signature
  $verification = openssl_verify($data, $signature, $public_key, OPENSSL_ALGO_SHA1);
  
  if($verification == 1) {
	  echo 'Yay! Signature is valid!';
  } else {
	  echo 'The signature is invalid!';
  }
?>

require 'base64'
require 'php_serialize'
require 'openssl'


public_key = '-----BEGIN PUBLIC KEY-----
MIICIjANBgkqh...'

# 'data' represents all of the POST fields sent with the request.
# Get the p_signature parameter & base64 decode it.
signature = Base64.decode64(data['p_signature'])

# Remove the p_signature parameter
data.delete('p_signature')

# Ensure all the data fields are strings
data.each {|key, value|data[key] = String(value)}

# Sort the data
data_sorted = data.sort_by{|key, value| key}

# and serialize the fields
# serialization library is available here: http://www.aagh.net/projects/ruby-php-serialize
data_serialized = PHP.serialize(data_sorted, true)

# verify the data
digest    = OpenSSL::Digest::SHA1.new
pub_key   = OpenSSL::PKey::RSA.new(public_key).public_key
verified  = pub_key.verify(digest, signature, data_serialized)

if verified
	puts "Yay! Signature is valid!"
else
	puts "The signature is invalid!"
end

# Your Paddle public key.
public_key = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgk...'''

import collections
import base64

# Crypto can be found at https://pypi.python.org/pypi/pycrypto
from Crypto.Signature import PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
import hashlib

# PHPSerialize can be found at https://pypi.python.org/pypi/phpserialize
import phpserialize

# Convert key from PEM to DER - Strip the first and last lines and newlines, and decode
public_key_encoded = public_key[26:-25].replace('\n', '')
public_key_der = base64.b64decode(public_key_encoded)

# input_data represents all of the POST fields sent with the request
# Get the p_signature parameter & base64 decode it.
signature = input_data['p_signature']

# Remove the p_signature parameter
del input_data['p_signature']

# Ensure all the data fields are strings
for field in input_data:
	input_data[field] = str(input_data[field])

# Sort the data
sorted_data = collections.OrderedDict(sorted(input_data.items()))

# and serialize the fields
serialized_data = phpserialize.dumps(sorted_data)

# verify the data
key = RSA.importKey(public_key_der)
digest = SHA.new()
digest.update(serialized_data)
verifier = PKCS1_v1_5.new(key)
signature = base64.b64decode(signature)
if verifier.verify(digest, signature):
	print("Yay! Signature is valid!");
else:
	print("The signature is invalid!");

// Node.js & Express implementation
const express = require('express');
const querystring = require('querystring');
const crypto = require('crypto');
const Serialize = require('php-serialize');

const router = express.Router();
const pubKey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`

function ksort(obj){
    let keys = Object.keys(obj).sort();
    let sortedObj = {};
  
    for (var i in keys) {
      sortedObj[keys[i]] = obj[keys[i]];
    }
  
    return sortedObj;
  }

function validateWebhook(jsonObj) {
    const mySig = Buffer.from(jsonObj.p_signature, 'base64');
    delete jsonObj.p_signature;
    // Need to serailise array and assign to data object
    jsonObj = ksort(jsonObj);
    for (var property in jsonObj) {
        if (jsonObj.hasOwnProperty(property) && (typeof jsonObj[property]) !== "string") {
            if (Array.isArray(jsonObj[property])) { // is it an array
                jsonObj[property] = jsonObj[property].toString();
            } else { //if its not an array and not a string, then it is a JSON obj
                jsonObj[property] = JSON.stringify(jsonObj[property]);
            }
        }
    }
    const serialized = Serialize.serialize(jsonObj);
    // End serailise data object
    const verifier = crypto.createVerify('sha1');
    verifier.update(serialized);
    verifier.end();

    let verification = verifier.verify(pubKey, mySig);

    if (verification) {
        return 'Yay! Signature is valid!';
    } else {
        return 'The signature is invalid!';
    }
}

/* Validate a Paddle webhook to this endpoint, or wherever in your app you are listening for Paddle webhooks */
router.post('/', function(req, res, next) {
    res.send(validateWebhook(req.body));
});

module.exports = router;

Karl has kindly shared a Java helper class for webhook verification on Github and thanks to Drew for sharing a server side Swift gist.

Questions about Paddle?

If you need any help regarding your Paddle integration, please get in touch with our Customer Success team using the form below.