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.
- Get Your Public Key – this can be found in your Seller Dashboard under Developer Tools > Public Key.
- Get the Webhook Signature – the signature is included on each webhook with the attribute
p_signature. Make sure to Base64 decode this. - Remove the signature from the response – the signature should not be included in the array of fields used in verification.
- Sort remaining fields – ensure the fields are listed in a standard order, sorted by key name, e.g. by using
ksort(). - PHP Serialize and sign the array – verify the PHP serialized array against the signature using SHA1 with your public key.
Code examples
Section titled “Code examples”<?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-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAncWOfnvXciow60nwb7teuwbluhc2WLdy8C3E4yf+gQEGjR+EXwDogWAmpJW0V3cRGhe41BBtO0vX39YeEjh3tkCIT4JTkR4yCXiXJ/tYGvsCAwEAAQ==-----END PUBLIC KEY-----'''
import collectionsimport base64
# Crypto can be found at https://pypi.org/project/pycryptodome/from Crypto.PublicKey import RSAtry: from Crypto.Hash import SHA1except ImportError: # Maybe it's called SHA from Crypto.Hash import SHA as SHA1try: from Crypto.Signature import PKCS1_v1_5except ImportError: # Maybe it's called pkcs1_15 from Crypto.Signature import pkcs1_15 as PKCS1_v1_5import hashlib
# PHPSerialize can be found at https://pypi.python.org/pypi/phpserializeimport phpserialize
# Convert key from PEM to DER - Strip the first and last lines and newlines, and decodepublic_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 parameterdel input_data['p_signature']
# Ensure all the data fields are stringsfor field in input_data: input_data[field] = str(input_data[field])
# Sort the datasorted_data = collections.OrderedDict(sorted(input_data.items()))
# and serialize the fieldsserialized_data = phpserialize.dumps(sorted_data)
# verify the datakey = 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-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAncWOfnvXciow60nwb7teuwbluhc2WLdy8C3E4yf+gQEGjR+EXwDogWAmpJW0V3cRGhe41BBtO0vX39YeEjh3tkCIT4JTkR4yCXiXJ/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 parameterdata.delete('p_signature')
# Ensure all the data fields are stringsdata.each {|key, value|data[key] = String(value)}
# Sort the datadata_sorted = data.sort_by{|key, value| key}
# and serialize the fields# serialization library is available here: https://github.com/jqr/php-serializedata_serialized = PHP.serialize(data_sorted, true)
# verify the datadigest = OpenSSL::Digest::SHA1.newpub_key = OpenSSL::PKey::RSA.new(public_key).public_keyverified = pub_key.verify(digest, signature, data_serialized)
if verified puts 'Yay! Signature is valid!'else puts 'The signature is invalid!'end// Node.js & Express implementationconst 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 stringsapp.use(bodyParser.urlencoded({ extended: true }));
// Webhook request handlingapp.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 dashboardconst 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;}