Nick George
all/

Listening to Paddle Webhooks

First, create a sandbox account at https://sandbox-login.paddle.com/signup and a live account at https://login.paddle.com/signup. Yes, there are two separate accounts for testing and live, I know.

Use different emails for these two accounts (I think), but keep your business details the same.

Since Paddle is a merchant of record, they are more careful about who they approve than payment providers like stripe. It will take time and a few manual verification steps before they activate your main account, and you will need things like terms of service + refund policy, a privacy policy and a pricing/products page. To follow along here, you will be working with the test account.

API server

You need some way to be notified that someone has purchased a product from you. Paddle provides webhooks (callbacks) that will notify your endpoints when something happens within Paddle.

You could design a serverless function for this, but I really hate developing those and I’ll want to expand this to do other tasks soon so I chose to go with a custom Go server that responds to /webhooks/paddle.

func main() {
    // loads config variables
	config := LoadConfig()
	http.HandleFunc("/", StatusOK)
	// Paddle webhook endpoint
	http.Handle("/webhooks/paddle", paddlePurchase)
	// Health check endpoint
	http.HandleFunc("/health", StatusOK)
	port := ":8081"
	if err := http.ListenAndServe(port, nil); err != nil {
		log.Fatal("Server failed to start:", err)
	}
}

Don’t worry about the details for now, just make sure paddlePurchase accepts posts and responds with a 200 OK to valid Paddle webhooks. The endpoint names are not important, you’ll just have to tell Paddle about them later.

You can run this server like so:

go build . -o build/my-api && go run ./build/my-api

To respond to Paddle webhooks, we need to have an internet accessible address and we need to validate that the request came from Paddle.

Development with hookdeck

Paddle recommends developing with a tool called Hookdeck. Hookdeck has a tutorial on Paddle development integration you should check out. But the long and short of it is that you need to setup the Hookdeck CLI and point it to your port and endpoint. My typical development command looks like so:

Terminal 1:

CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/my-api && ./build/my-api

Terminal 2:

./hookdeck listen 8081 paddle --path /webhooks/paddle

I just Ctrl-C to quit and edit my go, then re-run in Terminal 1 when I am messing with my handling code and leave hookdeck running.

Now that hookdeck is running, try sending a request to the URL it spits out to make sure you are seeing a response in your terminal and ensure everything is working properly.

Hookdeck is only required in development. In production, you’d use your prod server’s address.

Validating Paddle Requests

Now, we need to set the hookdeck URL as a notification destination to receive a paddle validation key. Navigate to Paddle > developer tools > notifications and add your hookdeck url as the receiver for notifications. You can click “all” or click just transactions (which is what we focus on for digital products).

screenshot of editing hookdeck image to get secret key

Now click the three dots and “edit” and you should see your unique secret for this notification, which allows you to validate the webhooks from paddle. Copy that to an environment file (I use .env.sandbox) as the variable PADDLE_WEBHOOK_SECRET_KEY. Note that you’ll have a separate .env.prod for the production account once everything is approved. Now in Developer Tools/authentication create a new API key. We just need Read for this one as we will use it to gather customer details after the purchase. Save that in PADDLE_API_KEY in the same file for your server.

Validate the Signature

Paddle provides SDK’s which have functions to validate webhooks.

For go, we add a middleware function that takes our PADDLE_WEBHOOK_SECRET_KEY as an argument:

// holds the secrets
type Config struct {
	Sandbox             bool
	PaddleAPIKey        string
	PaddleWebhookSecret string
}


// ...also have a function to read the secrets depending on if SANDBOX=1 is provided.


func main() {
    // load the config by reading appropriate .env file
	config := LoadConfig()
    // Configure webhook validator
	verifier := paddle.NewWebhookVerifier(config.PaddleWebhookSecret)
	http.HandleFunc("/", StatusOK)
	// Paddle webhook endpoint
	http.Handle("/webhooks/paddle", verifier.Middleware(http.HandlerFunc(paddlePurchase)))
	// Health check endpoint
	http.HandleFunc("/health", StatusOK)
	port := ":8081"
	if err := http.ListenAndServe(port, nil); err != nil {
		log.Fatal("Server failed to start:", err)
	}
}

Testing the Backend with Paddle Simulations

Ensure everything is running, use your SANDBOX variable if needed:

# Terminal 1:
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/my-api && SANDBOX='1' ./build/my-api

# Terminal 2:
./hookdeck listen 8081 paddle --path /webhooks/paddle

Now we should be able to test that the backend is responding to paddle and parsing API messages via the paddle simulations.

Select the Paddle > Developer Tools > Simulations, then create a new simulation for a completed transaction with your hookdeck url as the target and your server running. Have your paddlePurchase endpoint just print the body for now.

Run the simulation, you should see it hit your backend! We can use this for testing parsing, it send a quite complex transaction but we will refine it later.

Let me know if you want to see/learn more details about the go code.