Lemon Squeezy — Módulo de Pagos Únicos
Un módulo autocontenido para pagos únicos con Lemon Squeezy en una app de Next.js. Sin suscripciones ni facturación: solo checkout → orden → reembolso → webhooks.
Dependencia
Este módulo usa el SDK oficial:
@lemonsqueezy/lemonsqueezy.js
(Se resuelve automáticamente cuando instalás las dependencias de tu proyecto; no hay nada más para instalar manualmente.)
Mapa de archivos
lib/lemonsqueezy/
service.ts Internal logic (SDK calls, mapping, signature verification).
Not meant to be imported outside this folder.
actions.ts Server-side functions, ready to import in Server Components
or Server Actions. Every function returns
{ success: true, data } | { success: false, error }.
types.ts Shared types (Order, Checkout, Refund, Webhook payload).
client.ts Client-side helpers ("use client"): redirect/open checkout,
poll a status endpoint.
app/api/webhooks/lemonsqueezy/route.ts
Webhook receiver. Verifies the signature and handles events.
.env.local Environment variables (see below).
Variables de entorno
| Variable | Dónde encontrarla |
|---|---|
LEMONSQUEEZY_API_KEY | Dashboard → Settings → API → creá una API key |
LEMONSQUEEZY_STORE_ID | Dashboard → Settings → Stores |
LEMONSQUEEZY_WEBHOOK_SECRET | Dashboard → Settings → Webhooks → el signing secret de tu webhook |
Usá keys de modo de prueba mientras desarrollás; el modo de prueba de Lemon Squeezy tiene sus propios datos de tienda aislados.
Cómo encajan las piezas
1. Crear un pago (checkout)
Lemon Squeezy no tiene un endpoint separado para "crear pago". Un pago (una Order) se crea automáticamente cuando el cliente completa un Checkout. Por eso "crear checkout" y "crear pago" son la misma llamada:
import { createCheckout } from "@/lib/lemonsqueezy/actions";
const result = await createCheckout({
variantId: 123456,
email: "customer@example.com",
redirectUrl: "https://yourapp.com/payment/result",
customData: { internal_reference: "abc-123" }, // optional, see note below
});
if (result.success) {
// result.data.url -> send the customer here (or use client.ts helpers)
}
En el cliente:
import { redirectToCheckout } from "@/lib/lemonsqueezy/client";
redirectToCheckout(checkoutUrl);
2. Leer un pago
import { getPayment, getPaymentStatus } from "@/lib/lemonsqueezy/actions";
const order = await getPayment(orderId);
const status = await getPaymentStatus(orderId); // "pending" | "paid" | "refunded" | ...
3. Éxito / fallo
Estas no son páginas: son funciones que leen el id de la orden de los query params y verifican su estado:
import { getPaymentSuccess, getPaymentFailure } from "@/lib/lemonsqueezy/actions";
const success = await getPaymentSuccess(searchParams); // succeeds only if status === "paid"
const failure = await getPaymentFailure(searchParams); // succeeds if status !== "paid"
Limitación importante que tenés que conocer antes de conectar esto: Lemon Squeezy
no agrega ningún id a tu redirectUrl automáticamente, y no existe un endpoint
de la API para buscar una Order por el customData que le adjuntaste a un
Checkout. Las funciones de arriba esperan un order_id real de Lemon Squeezy
como query param: ese es el identificador con el que realmente trabaja Lemon Squeezy
(no existe un "checkout_id" del lado de la Order). Si querés correlacionar la
redirección con tu propia referencia, generada antes de crear el checkout,
tenés que:
- Pasar esa referencia como
customDataal crear el checkout. - Persistir vos mismo el mapeo (
reference -> order id) cuando se dispare el webhookorder_created: hay un lugar marcado para esto enapp/api/webhooks/lemonsqueezy/route.ts. - Buscar el id de la orden en tu propio almacenamiento antes de llamar a
getPaymentSuccess/getPaymentFailure.
Este módulo intencionalmente no incluye una base de datos, así que ese paso de búsqueda queda para que lo conectes donde persistas tus datos.
4. Reembolsos (totales y parciales)
Lemon Squeezy tiene un único endpoint de reembolso. Pasar amount (en centavos)
emite un reembolso parcial; omitirlo reembolsa la orden completa:
import { refundPayment } from "@/lib/lemonsqueezy/actions";
await refundPayment({ orderId }); // full refund
await refundPayment({ orderId, amount: 500 }); // partial refund, $5.00
5. Método de pago
Lemon Squeezy no expone una API de métodos de pago para órdenes únicas.
getPaymentMethod solo devuelve la marca de la tarjeta y los últimos 4 dígitos que Lemon Squeezy
registró en la propia Order (puede ser null según el método de pago
usado):
import { getPaymentMethod } from "@/lib/lemonsqueezy/actions";
const method = await getPaymentMethod(orderId);
6. Cancelación — no implementada, a propósito
No existe una API para cancelar una Order pagada. Un Checkout sin terminar
simplemente expira por su cuenta (CheckoutResult.expiresAt): no hay nada que
llamar para cancelarlo manualmente. No se agregó ninguna función para esto, para evitar
simular un comportamiento que Lemon Squeezy no soporta.
7. Webhooks
Registrá https://yourapp.com/api/webhooks/lemonsqueezy en
Dashboard → Settings → Webhooks, suscrito a:
order_createdorder_refunded
El route handler verifica el header X-Signature contra
LEMONSQUEEZY_WEBHOOK_SECRET usando el cuerpo crudo del request, y luego despacha
según event_name. Agregá tu propia persistencia/efectos secundarios dentro del
switch de route.ts.
Manejo de errores
service.tslanzaErrors simples con el mensaje de error de la API de Lemon Squeezy cuando está disponible.actions.tslos captura y devuelve{ success: false, error }: quien llama nunca necesita try/catch, solo verificarresult.success.- La route del webhook devuelve
401ante firmas inválidas,400ante payloads malformados y500si un handler lanza un error, para que la lógica de reintentos de Lemon Squeezy (backoff exponencial, hasta 3 reintentos) se active correctamente.