Payments
Lemon Squeezy payments for GoCommerce
A Go package that creates a Lemon Squeezy checkout at the order’s price and marks the GoCommerce order paid when a signed order_created webhook says it was paid. It needs one placeholder product in Lemon Squeezy — the one setup step Paddle does not.
- Payments module
- 6 settings
- 7 tests
- ext/payments-lemonsqueezy
What it does
The module registers “lemonsqueezy” as a payment method. At checkout it creates a Lemon Squeezy checkout against a store and a variant you name, replaces the variant’s price with the order’s total in minor units, and answers with a redirect intent carrying the checkout URL.
Lemon Squeezy then calls POST /api/checkout/lemonsqueezy/webhook. The module verifies X-Signature and, when order_created arrives with the status paid, asks the engine to mark the order paid, recording Lemon Squeezy’s own order id — the one a refund is later issued against.
Why the placeholder: Lemon Squeezy sells items from its own catalogue, so a checkout must name a variant that already exists there. You create one product — any name, any price, never shown to a shopper — and every order becomes that variant at the order’s own price. Like Paddle, Lemon Squeezy is the merchant of record and works out the tax itself.
Configuration
Four settings are required: the API key, the webhook’s signing secret, and the store and placeholder variant every checkout is created against. Set them in Config from your own main(), or under Settings › Payment methods.
| Setting | Environment variable | Required | What it does |
|---|---|---|---|
APIKeyAPI key | LEMONSQUEEZY_API_KEY | Yes | Sent as a bearer token. Not sent to the storefront. |
WebhookSecretWebhook signing secret | LEMONSQUEEZY_WEBHOOK_SECRET | Yes | Verifies the X-Signature header. Required because, as the code puts it, without it any caller could mark orders paid. |
StoreIDStore ID | LEMONSQUEEZY_STORE_ID | Yes | The Lemon Squeezy store every checkout is created in. |
VariantIDPlaceholder variant ID | LEMONSQUEEZY_VARIANT_ID | Yes | The catalogue item every checkout is created against; its price is replaced by the order total. |
BaseURLAPI base URL | — | No | Overrides https://api.lemonsqueezy.com, for tests. Empty for production. |
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
- Create the placeholderIn Lemon Squeezy, create one product with any name and price, and note the store id and its variant id. Shoppers never see it.
- Add the webhookPoint a webhook at https://your-api-host/api/checkout/lemonsqueezy/webhook for order_created, and keep its signing secret.
- Install and configurePass lemonsqueezy.New to gocommerce.New as below, or run the reference binary with -gateways and fill in Settings › Payment methods. Leave GoCommerce’s tax rates empty.
- Take a paymentPOST /api/checkout/lemonsqueezy answers with a redirect intent whose url is the hosted checkout. A paid order_created marks the order paid.
main.go
import (
"os"
"github.com/itswadesh/gocommerce/core"
lemonsqueezy "github.com/itswadesh/gocommerce/ext/payments-lemonsqueezy"
)
app, err := gocommerce.New(cfg,
lemonsqueezy.New(lemonsqueezy.Config{
APIKey: os.Getenv("LEMONSQUEEZY_API_KEY"),
WebhookSecret: os.Getenv("LEMONSQUEEZY_WEBHOOK_SECRET"),
StoreID: os.Getenv("LEMONSQUEEZY_STORE_ID"),
VariantID: os.Getenv("LEMONSQUEEZY_VARIANT_ID"),
}),
) The package doc’s own example, with its imports; cfg is your gocommerce.Config. With an empty lemonsqueezy.Config the module installs idle and waits for the panel. Import path github.com/itswadesh/gocommerce/ext/payments-lemonsqueezy.
How it works
-
JSON:API, as required
Requests go as application/vnd.api+json, which Lemon Squeezy insists on — the package notes that plain JSON is refused with a confusing 415.
-
Signed, without a timestamp
X-Signature is a hex HMAC-SHA256 of the raw body, compared in constant time. With no timestamp in it, the idempotency claim is what stops a captured delivery applying twice.
-
Each event once
Lemon Squeezy sends no event id, so claims are keyed on the event name plus the object id. A retry is a duplicate; two different events about one order stay distinct.
-
Only paid settles
order_created with any status other than paid is acknowledged and changes nothing, so a sale still being collected, or refused, is not marked paid.
-
Refunds against the order
A refund posts to Lemon Squeezy’s refund endpoint for its order id, for the amount asked, and keeps the id it answers with on the refund row.
-
A failed update is retried
If marking the order paid fails, the claim is released and the webhook answers 500, so the retry finds work to do.
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.
- A placeholder is requiredWithout StoreID and VariantID the module stays unconfigured, and a wrong variant surfaces as an error from Lemon Squeezy at the first checkout.
- No currency is sentThe checkout carries the total as custom_price in minor units and no currency code. Check that the Lemon Squeezy store charges in the currency your store settles in.
- Tax is Lemon Squeezy’sLeave GoCommerce’s tax rates empty. The module does not check, and configuring both charges tax twice.
- No failure pathThe module never marks a payment failed. A checkout that is abandoned or declined leaves the order unpaid.
- Refunds follow the webhookA refund goes against the Lemon Squeezy order id that only the paid order_created webhook records.
- Dashboard refunds are only loggedorder_refunded is logged, not applied. A refund made in Lemon Squeezy does not change the GoCommerce order.
FAQ
Questions about the Lemon Squeezy module
Why does it need a placeholder product?
Lemon Squeezy sells items from its own catalogue, so a checkout must name a store and a variant that already exist there. The module overrides that variant’s price with the order total, so one placeholder serves every order. The package states it up front because the alternative is discovering it from an error at the first real checkout.
Should I keep GoCommerce’s tax rates?
No. Lemon Squeezy is the merchant of record and works out the tax on the sale itself. Leave GoCommerce’s rates empty so the total it sends is what the shopper pays.
What is the webhook URL?
POST /api/checkout/lemonsqueezy/webhook on the host that serves your GoCommerce API. order_created settles a paid order; order_refunded is logged; anything else is acknowledged.
Is this a Lemon Squeezy partnership?
No. The module calls Lemon Squeezy’s public API with keys you supply. Lemon Squeezy 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-lemonsqueezy in the GoCommerce repository, MIT licensed. When this page and the code disagree, the code is right and this page is out of date.Lemon Squeezy is a trademark of its owner; this module talks to its public API and implies no endorsement. See trademarks.
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.