Skip to content
You're viewing guides for Paddle Classic, which is no longer available for new signups. Head to developer.paddle.com for Paddle Billing guides.

Verifying webhooks

We send a signature field with each webhook that can be used to verify that the webhook was sent by Paddle.

We use public/private key encryption to allow you to verify these requests. Follow the step-by-step guide below to verify a Paddle signature.

  1. Get Your Public Key – this can be found in your Seller Dashboard under Developer Tools > Public Key.
  2. Get the Webhook Signature – the signature is included on each webhook with the attribute p_signature. Make sure to Base64 decode this.
  3. Remove the signature from the response – the signature should not be included in the array of fields used in verification.
  4. Sort remaining fields – ensure the fields are listed in a standard order, sorted by key name, e.g. by using ksort().
  5. PHP Serialize and sign the array – verify the PHP serialized array against the signature using SHA1 with your public key.
<?php
// Your Paddle 'Public Key'
$public_key_string =
"-----BEGIN PUBLIC KEY-----" . "\n" .
"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAncWOfnvXciow60nwb7te" . "\n" .
"uwbluhc2WLdy8C3E4yf+gQEGjR+EXwDogWAmpJW0V3cRGhe41BBtO0vX39YeEjh3" . "\n" .
"tkCIT4JTkR4yCXiXJ/tYGvsCAwEAAQ==" . "\n" .
"-----END PUBLIC KEY-----";
$public_key = openssl_get_publickey($public_key_string);
// 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);
// Verify 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!';
}
?>
# Your Paddle public key.
public_key = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAncWOfnvXciow60nwb7te
uwbluhc2WLdy8C3E4yf+gQEGjR+EXwDogWAmpJW0V3cRGhe41BBtO0vX39YeEjh3
tkCIT4JTkR4yCXiXJ/tYGvsCAwEAAQ==
-----END PUBLIC KEY-----'''
import collections
import base64
# Crypto can be found at https://pypi.org/project/pycryptodome/
from Crypto.PublicKey import RSA
try:
from Crypto.Hash import SHA1
except ImportError:
# Maybe it's called SHA
from Crypto.Hash import SHA as SHA1
try:
from Crypto.Signature import PKCS1_v1_5
except ImportError:
# Maybe it's called pkcs1_15
from Crypto.Signature import pkcs1_15 as PKCS1_v1_5
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 = SHA1.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!')
require 'base64'
require 'php_serialize'
require 'openssl'
public_key = %q(-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAncWOfnvXciow60nwb7te
uwbluhc2WLdy8C3E4yf+gQEGjR+EXwDogWAmpJW0V3cRGhe41BBtO0vX39YeEjh3
tkCIT4JTkR4yCXiXJ/tYGvsCAwEAAQ==
-----END PUBLIC KEY-----)
# '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: https://github.com/jqr/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
// Node.js & Express implementation
const crypto = require("crypto");
const Serialize = require("php-serialize");
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
// Parses urlencoded webhooks from paddle to JSON with keys sorted alphabetically ascending and values as strings
app.use(bodyParser.urlencoded({ extended: true }));
// Webhook request handling
app.post("/", (req, res) => {
if (validateWebhook(req.body)) {
console.log("WEBHOOK_VERIFIED");
res.status(200).end();
} else {
res.sendStatus(403);
console.log("WEBHOOK_NOT_VERIFIED");
}
});
app.listen(8080, () => console.log("Node.js server started on port 8080."));
// Public key from your paddle dashboard
const pubKey = `-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----`;
function ksort(obj) {
const keys = Object.keys(obj).sort();
let sortedObj = {};
for (let i in keys) {
sortedObj[keys[i]] = obj[keys[i]];
}
return sortedObj;
}
function validateWebhook(jsonObj) {
// Grab p_signature
const mySig = Buffer.from(jsonObj.p_signature, "base64");
// Remove p_signature from object - not included in array of fields used in verification.
delete jsonObj.p_signature;
// Need to sort array by key in ascending order
jsonObj = ksort(jsonObj);
for (let 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]);
}
}
}
// Serialise remaining fields of jsonObj
const serialized = Serialize.serialize(jsonObj);
// verify the serialized array against the signature using SHA1 with your public key.
const verifier = crypto.createVerify("sha1");
verifier.update(serialized);
verifier.end();
const verification = verifier.verify(pubKey, mySig);
// Used in response if statement
return verification;
}