Skip to content

Created: 2020-03-27 18:40:22 -0700 Modified: 2020-04-07 10:54:02 -0700 is a payment platform. The decision to use it (and thus write notes about it) comes from my e-commerce note.

  • To test without purchasing - you can do this via several methods (reference). There is no official sandbox.
    • Make a 100%-off coupon
    • Purchase and then issue a refund immediately (this doesn’t cost anything apparently, so hopefully the transaction fees don’t stick)
  • They have authentication keys in Developer Tools → Authentication. These are not for sandboxing or even different data sets; they’re just for granting access to particular apps. So, for example, if you were a game developer and had two different games on the AppStore, you could have keys for each one even though they pull from the same products.
  • Paddle includes the consumption tax (like VAT) in the price. The setting to control this is here.
  • Offering discounts and promotions

There’s a consent box in the normal purchase flow if you don’t prepopulate the email address via the checkout parameters:

As far as I can tell, there’s no way to hide this unless you prepopulate the email address. There’s a parameter called marketingConsent, but this is basically never going to be used since it was for back when Paddle was preparing for GDPR. By specifying true, you are telling Paddle that you already got the user’s consent to for you to send emails, so Paddle can store that consent as well (reference).

However, if you do prepopulate the email address, then the user may want to change it. The pop-up checkout allows this by default with a small link at the bottom that most people probably won’t see:

I think a good solution would be to confirm their email address in your own front-end UI before even spawning the checkout, that way you can show an <input> that has their email on file and have the user confirm it.

Alternatively, you could find the email input and fill that by yourself (or even hide the checkbox). Paddle doesn’t say you can’t do this, just that they “highly recommend not making any changes” (reference). You could use code like this:

$(‘[data-testid=authenticationEmailInput]‘).value = “

A “cart” concept, i.e. purchasing several unrelated products

Section titled A “cart” concept, i.e. purchasing several unrelated products

There are two routes to choose here:

  • You, as a vendor, create bundles of products. These are typically related though (as opposed to arbitrary user choices), e.g. “buy a razor and a razor blade and shaving cream”. Then, when opening a purchase page, specify the bundle ID.
  • Use pay links (reference). These allow you to make custom sets of items for a cart that aren’t necessarily based on a single product. You could also use this to adapt the purchase prices.

Problems caused by standalone checkout links and only having a fulfillment webhook

Section titled Problems caused by standalone checkout links and only having a fulfillment webhook

(rather than try to be generic here, I’ll explain my scenario and why this was a problem for me specifically)

I want to sell tutorials, which are one-time digital products. This means that if you own a tutorial already, you have no need to purchase it a second time. I planned on having each tutorial be its own product in Paddle’s back-end, then setting up webhook fulfillment so that I can properly grant users access in my back-end.

Note that the only webhook that you can create is for fulfillment, not validation. I think that this on its own can present certain problems, but they’re mostly edge cases, so let’s move on to the concrete problem I had:

Products that you make in Paddle’s back-end all automatically generate their own standalone checkout link, e.g. If a user gets to that standalone checkout, then they could specify an email address to Paddle that I don’t have in my system, so when my webhook gets called, I won’t be able to actually grant access to anyone since I don’t know who they are. I also can’t reject the payment since the webhook is just for fulfillment, not validation. That means that a customer would have gotten charged with no way of actually getting a product.

There are a couple of workarounds/mitigations here:

  1. People likely won’t get the direct link in the first place, and if they do, they probably won’t just randomly use it to purchase a product.
  2. If a person does purchase a product, then they’ll probably just contact your site’s support (or even Paddle) to ask for a refund when they find out they didn’t get anything for it.
  3. I believe this is a time when you can use pay links (reference) to sort of work around this. The general idea (at least my unproven idea in my own head right now) is that no products would exist in Paddle’s back-end. Instead, you would generate everything dynamically by involving your own server. By doing this, the server can check preconditions for the client like whether you have the item already, how much it should cost, etc. Then, the server registers the pay link with Paddle via their REST API and has the client open the checkout to that link. Paddle charges the customer, then in the fulfillment webhook, you can verify the pay-link details yourself.

This goes hand-in-hand with the section in my notes here about only having a fulfillment hook, not a validation hook.

These are the solution to pretty much every problem I have with Paddle. I tested them out through Postman:

But guess what! Coupon codes don’t work here! It doesn’t matter that there are only two types of coupons (product and checkout) and only one type (product) could even apply to these, you still get an error saying that it’s not possible:

…that’s why I set the price to 0.00.

Anyway, all of that gets returned as a URL with a JWT in it, meaning the pass-through data needs to be some ID from your server for the fulfillment webhook to be able to correlate it back to some purchase data in your database. You’ll also need this ID to ensure that the link was only used once. Their description of “Set custom attributes for a one-time or subscription checkout” makes it sort of sound like you can only use the link once, but they’re just saying that it works for recurring and non-recurring payments.

The big difference between this and a product that you make in Paddle’s back-end is that the pass-through data here is signed by your own server, whereas for a product in Paddle, the client specifies it via JavaScript, meaning they can just make up whatever they want. I suppose you could have your server specify a particular JWT that it signs so that you get the same benefit as the pay link, but either way, Paddle will charge the customer before your server ever has a chance to validate it, so you can’t really stop invalid purchases no matter what you do.

So, in the end, it sounds like you’ll just have cases where you may have to refund a user’s purchase because they managed to shoot themselves in the foot with your pay links or with your product links, but there’s no stopping the potential for them to shoot themselves in the foot.

To open pay-links on the client, just do this:{

Custom coupon handling from the server (AKA credits)

Section titled Custom coupon handling from the server (AKA credits)

I have a scenario where I want to support the concept of “credits”. These are something that would be applied by the customer toward a particular purchase. For example, let’s say you have 6 credits and you want to make a 10purchase,youcouldinsteadspend10 purchase, you could instead spend 4 and all of your credits.

To accommodate this, I thought of potentially using Paddle’s coupon system, but I don’t think it offers the flexibility that I would need (e.g. it may be tough if you generate a 5couponfora5 coupon for a 3 item). Instead, I think that overriding the checkout prices would be good (reference). The flow would look something like this:

  • Have the client tell the server (i.e. not Paddle’s server) that it wants to use credits.
  • Have the server generate a pay link that has the credit discount applied.
  • Have the client use that pay link.
  • When Paddle tells your server that the purchase went through, validate the pay link and subtract the credits.

Just be careful that if you do this:

  • You make sure not to validate a purchase in your webhook unless the customer actually has the credits, otherwise they’ll have already paid for a discounted rate that they can’t “afford” (via credits).
  • You ensure that you’re not allowing redemption of the same link multiple times
  • That the user who triggered the redemption process is the one to redeem the link (that way someone else doesn’t guess the link, although I don’t know the format of the links)
  • I signed up for an account
  • I made a product via this page
    • I set the fulfillment method to “server notification” since I’m going to have a webhook handling the fulfillment.
    • You have to release a product before it’s available. I am skipping this for now so that I can see what checkout is like.
  • I included their JavaScript
  • I followed the webhook guide (reference)
    • I made a Cloud Function for Firebase to act as my webhook
    • I set the right config value so that my function would have access to my public key:
      • firebase functions:config:set paddle.seller_public_key=“----BEGIN---…-----END-----“
    • I made a function like this:
exports.default = functions.https.onRequest(async (req, res) => {
const jsonObject = req.body;
const isValidSignature = validateWebhook(jsonObject);
if (!isValidSignature) {
console.log('NOT VALID SIGNATURE');
return res.status(400).send();
console.log('Valid signature. jsonObject: ' + JSON.stringify(jsonObject));
res.status(200).send({ hello: 'world' });

…the rest of the code from validateWebhook was taken from here with minimal changes just to fit my environment.

  • Here’s what the “test webhook” button on the “create webhook” page passes to my function (reference):
"p_product_id": 588716,
"p_price": 22.87,
"p_country": "US",
"p_currency": "USD",
"p_sale_gross": 22.87,
"p_tax_amount": 0,
"p_paddle_fee": 1.64,
"p_coupon_savings": 0,
"p_earnings": "{\110966\:\21.2300\}",
"p_order_id": 13547694,
"p_coupon": "",
"p_used_price_override": true,
"passthrough": "Example passthrough",
"p_quantity": 1,
"quantity": 1,
"event_time": "2020-03-31 18:31:32"
  • Finish releasing the product
    • I set the “download” URL to
      • Note: I don’t know if this’ll ever show up anywhere if you later choose “No” for “Deliver download via email”.
  • Once I released the product, I could make a coupon so that I wouldn’t have to pay to test.

Here’s what the checkout looked like:

The checkout message is coming from the specific product → Set Description

You can also customize these via the JavaScript parameters or HTML attributes (reference)

Here’s the email that I ended up with after making a purchase:

From my Google Function, I had this code:

res.status(200).send({ hello: 'world' });

That resulted in the “hello world” text.

The “Instructions & Information” come from the product itself. You can format this with some basic HTML tags if you want to put a link to the site, e.g.

If you choose “No” for “Deliver download via email”, then these instructions won’t show at all.

Error: [PADDLE] The option parameter ‘vendor’ must be an integer.

Section titled Error: [PADDLE] The option parameter ‘vendor’ must be an integer.

They don’t do string conversions for you, so this code is bad:

vendor: "12345",

Just parseInt(vendorId, 10) on it.