# Paddle Payments for GoCommerce — Open-Source Module

> Sell through Paddle Billing as merchant of record from a GoCommerce store: API key, notification secret, sandbox, refunds, and why tax rates stay empty.

- Canonical: https://kitcommerce.store/integrations/payments-paddle/
- Last updated: 2026-09-25

---

Payments

## Paddle payments for GoCommerce

A Go package that creates a Paddle Billing transaction at checkout, sends the shopper to Paddle’s checkout, and marks the GoCommerce order paid when a signed transaction.completed notification arrives. Paddle is the merchant of record, and that changes how you set up tax.

- **Payments** module
- **6** settings
- **7** tests
- **ext/payments-paddle**

### What it does

The module registers “paddle” as a payment method. At checkout it creates a Paddle transaction with one ad-hoc line — “Order” and the order number, at the order’s total — and answers with a redirect intent carrying the transaction’s checkout URL.

Paddle then calls POST /api/checkout/paddle/webhook. The module verifies the Paddle-Signature header; transaction.completed asks the engine to mark the order paid, and transaction.payment_failed to mark the payment failed. transaction.paid is deliberately not enough: the package notes that it fires first and can still be reversed.

The package’s central point is that Paddle sells to the shopper, not the store, and decides the tax on the sale itself. A store on Paddle should leave GoCommerce’s tax rates empty and let the total it sends be the price the shopper pays; configuring both charges tax twice. That is documented rather than enforced, because the engine cannot tell which of the two a store intends.

### Configuration

Two settings are required: the API key and the notification destination’s signing secret. Set them in Config from your own main(), or leave Config empty and fill them in under Settings › Payment methods.

*Paddle module settings — 6 settings, 2 required*

| Setting | Environment variable | Required | What it does |
| --- | --- | --- | --- |
| `APIKey` API key | `PADDLE_API_KEY` | Yes | Sent as a bearer token. It needs transaction.write to create a transaction and adjustment.write to refund one. |
| `WebhookSecret` Notification signing secret | `PADDLE_NOTIFICATION_SECRET` | Yes | Verifies the Paddle-Signature header. Required because, as the code puts it, without it any caller could mark orders paid. |
| `Sandbox` Sandbox | — | No | Points the module at sandbox-api.paddle.com. Paddle issues separate keys for the two, so this is a deliberate flag rather than something read from the key. |
| `BaseURL` API base URL | — | No | Overrides both hosts, for tests. |
| `WebhookTolerance` | — | No | How old a signed notification may be. Five minutes by default; Paddle’s own SDK helpers default to five seconds. Go only. |
| `Client` | — | No | Replaces the HTTP client, which otherwise times out after 20 seconds. Go only; not in the panel. |

The module reads its Config struct, not the environment. The variable names are the ones the package’s own example or the reference binary uses; in your own `main()` you choose where each value comes from. Where a setting has a panel label, the admin’s settings drawer can hold it too, and a value typed there wins over Config.

### Setting it up

1. **Add a notification destination** — In Paddle, send notifications to https://your-api-host/api/checkout/paddle/webhook — transaction.completed and transaction.payment_failed at least — and keep its secret.
2. **Empty the tax rates** — Paddle works out the tax. Leave GoCommerce’s tax rates empty so the order total is the price the shopper pays.
3. **Install and configure** — Pass paddle.New to gocommerce.New as below, or run the reference binary with -gateways and fill in Settings › Payment methods. Tick Sandbox while testing.
4. **Take a payment** — POST /api/checkout/paddle answers with a redirect intent whose url is Paddle’s checkout. transaction.completed marks the order paid.

main.go

```
import (
	"os"

	"github.com/itswadesh/gocommerce/core"
	paddle "github.com/itswadesh/gocommerce/ext/payments-paddle"
)

app, err := gocommerce.New(cfg,
	paddle.New(paddle.Config{
		APIKey:        os.Getenv("PADDLE_API_KEY"),
		WebhookSecret: os.Getenv("PADDLE_NOTIFICATION_SECRET"),
		Sandbox:       true,
	}),
)
```

The package doc’s own example, with its imports; cfg is your gocommerce.Config. It points at the sandbox; drop Sandbox for live keys. Import path `github.com/itswadesh/gocommerce/ext/payments-paddle`.

### How it works

- **Signed and timed**

  Paddle-Signature’s ts=…;h1=… is checked as an HMAC-SHA256 over the timestamp, a colon and the raw body. Several h1 values pass during a secret rotation; a timestamp outside the tolerance, early or late, is refused.

- **Each event once**

  Claims are keyed on Paddle’s event_id with INSERT … ON CONFLICT DO NOTHING, atomic across instances. A redelivery is answered 200 and changes nothing.

- **A failed update is retried**

  If marking the order paid fails, the claim is released and the webhook answers 500, so Paddle’s retry finds work to do.

- **One line per order**

  Paddle’s items belong to its own catalogue, so the module sends one ad-hoc line carrying the order total rather than mirroring the order into Paddle products. The order keeps the breakdown.

- **Refunds as adjustments**

  A refund reads the transaction back to find its line, then creates a refund adjustment for exactly the amount asked — always typed partial, so Paddle cannot turn a partial refund into a full one. The adjustment id is recorded; a rejected one comes back as an error.

- **Amounts as strings**

  Amounts go to Paddle as integer strings in minor units, never as floats, so nothing picks up a rounding error on the way through JSON.

### What it does not do

Read these before an order depends on it. No store is known to run GoCommerce in production yet, so these come from the code, not from anyone’s experience.

- **Tax is Paddle’s** — Leave GoCommerce’s tax rates empty. The module does not check, and a store that configures both charges tax twice.
- **One line, one tax category** — Every order is sent as a single line with Paddle’s standard tax category, whatever the products in it are.
- **Needs a checkout URL** — A transaction that comes back without one is refused, with a note to check that the Paddle account has an approved default payment link.
- **Adjustments are only logged** — adjustment.created and adjustment.updated are logged, not applied. A refund made in Paddle’s dashboard does not change the GoCommerce order.
- **Orders, not subscriptions** — The module sells an order once. Events for anything this store did not start, such as a renewal billed in Paddle, are acknowledged and ignored.

FAQ

### Questions about the Paddle module

**Why should GoCommerce’s tax rates be empty?**

Because Paddle is the merchant of record: it sells to the shopper and adds the tax it decides on. If GoCommerce has already added tax to the total, the shopper pays it twice. The package documents this rather than zeroing tax itself, because it cannot know what a store intends.

**How do I test against Paddle’s sandbox?**

Switch on Sandbox and use sandbox keys. Paddle issues separate keys for sandbox and live, so the module takes an explicit flag instead of guessing from the key.

**Why does it wait for transaction.completed?**

The package explains that transaction.paid fires first and can still be reversed, so settling on it would mark orders paid that Paddle has not finished collecting. transaction.completed is the event that means the money arrived.

**Can I use it without writing Go?**

Yes. The reference binary’s -gateways flag installs every payment module with an empty Config. Switch Paddle on under Settings › Payment methods and fill in the key, the secret and the sandbox flag there.

**Is this a Paddle partnership?**

No. The module calls Paddle’s public API with keys you supply. Paddle does not endorse GoCommerce, and no store is known to run this module in production yet.

### Source

Everything on this page is read from [`ext/payments-paddle`](https://github.com/itswadesh/gocommerce/tree/main/ext/payments-paddle) in the GoCommerce repository, MIT licensed. When this page and the code disagree, the code is right and this page is out of date.Paddle is a trademark of its owner; this module talks to its public API and implies no endorsement. See [trademarks](https://kitcommerce.store/about/#trademarks).

- [ext/payments-paddle on GitHub](https://github.com/itswadesh/gocommerce/tree/main/ext/payments-paddle)
- [Paddle](https://www.paddle.com)
- [All GoCommerce modules](https://kitcommerce.store/integrations/)

### Try it against a store of your own

The one-command stack gives you GoCommerce’s API and admin on your own machine in minutes. Add this module to it and try it on test orders before a real one depends on it.

[Deploy in minutes](https://kitcommerce.store/#one-command) · [Read the module](https://github.com/itswadesh/gocommerce/tree/main/ext/payments-paddle) · [All integrations](https://kitcommerce.store/integrations/)
