Hi! I’m Michal. I’ve been programming and running UNIX servers my whole life. Now I’m running a business. I’m the maker behind Syften and Code Dog.

How Not to Run a Saas - Paddle.com Woes with Solutions

Posted at — Aug 20, 2019

Developer Blog

Paddle.com is a British startup handling customer payments for you. They take care of taxes, card charge retries, currency conversions, and they send you a reverse invoice at the end of the month. Very convenient for an indie hacker! Unfortunately not so convenient for a programmer.

It’s Written in PHP

And it expects you to do things the PHP way. To verify a webhook you need the ksort() function that sorts a dictionary. Difficult to do in other languages, as shown by the fact that all implementations cook up their own phpserialize() function.

If you use Go save yourself a day of work and check out my implementation.

The API Is Crappy

Even though the API is versioned as 2.0 and the company has been around for 7 years now it’s still immature.

Lousy Documentation

The custom checkout endpoint documentation does not tell you what types the different parameters are. This requires some trial and error, especially for parameters like recurring_prices which it turns out is a list of strings. Check out my implementation to save yourself some time, but keep in mind that I might not have gotten it right in all cases. As of 15.09.2019 this is no longer true, they documented the types.

Lousy Design

Setting up webhooks for one time payments is even worse. It’s not obvious which webhook type will be sent (is it Fulfillment Webhook or Payment Succeeded?). By analyzing the received payload I learned it’s the former - a webhook type that’s missing the alert_name field needed to differentiate between payload types. Did they forget to include it? A significant road bump - but you’re clever! You want to work around it and add a custom field with the missing data. Not so fast - custom fields get ignored by the Test Webhook dialog. What can you do here, other than specifying a different URL for this one particular webhook type?

PS. Remember that quantity is a string, not an int. And if your webhook handler returns a 400 don’t get mislead by the test dialog telling you it got aPage Not Found error.

Lousy Consistency

Some API requests will go to vendors.paddle.com/api/2.0/, while others to checkout.paddle.com/api/2.0/. The former expects arrays to be in the HTTP Post format (e.g. ?arr=1&arr=2). The later expects arrays to be a string (e.g. ?arr=1,2).

Lousy Practices

Oh, and did you add links to the documentation in your code’s comments, or a blog post such as this one? Tough luck, the Paddle team likes to change and break them from time to time.

They May Block Your Account

You may find a few comments on the internet from angry customers stating that their account was blocked and their money locked. Make sure to check out their unsupported products page before starting.

To be super safe I reached out to Paddle Support and asked them to verify that my product is OK.

It’s Designed for a Company with Just One Product

You cannot have two accounts registered for the same legal entity. That is fine, as I can just create multiple subscriptions for each of my products. However, as it turns out, I cannot set different webhook URLs for them! Annoying, especially given the circumstances under which the company was founded:

Christian founds Paddle from his bedroom in Corby, United Kingdom (the glamour!), he is 18, and this is his third business. As a software and app developer he encountered the frustrations of selling software globally and decided to do something about it.

Because I use Paddle for Syften, Code Dog and GeekMail I needed a solution. Having reached out to support and hunted for an answer on the internet I now believe that the best way to use Paddle for multiple products is to run a webhook proxy (or rather a demultiplexer). Luckily, with Go and AppEngine the job is easy. The full code follows:

package main

import (  


var (  
        App1 = &url.URL{  
                Scheme: "https",  
                Host:   "app1.example.com",  
        App2 = &url.URL{  
                Scheme: "https",  
                Host:   "app2.example.com",  

func handler(w http.ResponseWriter, r \*http.Request) {  
        ctx := appengine.NewContext(r)

        // It's complicated because we base our request on Form data. See:  
        // [https://stackoverflow.com/questions/49745252/reverseproxy-depending-on-the-request-body-in-golang](https://stackoverflow.com/questions/49745252/reverseproxy-depending-on-the-request-body-in-golang)  
        director := func(r \*http.Request) {  
                var target \*url.URL

                body, err := ioutil.ReadAll(r.Body)  
                if err != nil {  
                        LogErrorfd(ctx, "err=%v", err)  

                // Reassign the now empty body  
                r.Body = ioutil.NopCloser(bytes.NewBuffer(body))  
                planID := r.FormValue("subscription\_plan\_id")  
                switch planID {  
                case "1000":  
                        target = App1

                case "2000": // plan 1  
                        target = App2  
                case "2001": // plan 2  
                        target = App2

                        target = App1  

                // Reassign the body again  
                r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

                r.URL.Scheme = target.Scheme  
                r.URL.Host = target.Host  

        rp := &httputil.ReverseProxy{  
                Director:  director,  
                Transport: &urlfetch.Transport{Context: ctx},  

        rp.ServeHTTP(w, r)  

func main() {  
        http.HandleFunc("/", handler)


Now set your webhook URL to the host running the proxy and you’re set.

They Don’t Want Help

In January 2019 I contacted Paddle and offered to write a Golang library for them. They kindly declined, stating that they’re working on it internally. Almost one year later it’s still not released…


There is a lesson to be learned here. If your tool solves a real need for real people they will use it, even if it’s crap. Polishing your stealth mode product and adding “just one more feature” changes nothing.

So stop polishing, ship it, and market it. You have a product and you're almost done setting up a payment system for it. But nobody is paying you yet. And they never will unless you do something about it. Get out there and start spreading the word! Look For Actionable Content Marketing Opportunities