@dariomvg/create
lemonsqueezypagospago-unicowebhooksintegracion

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

VariableDónde encontrarla
LEMONSQUEEZY_API_KEYDashboard → Settings → API → creá una API key
LEMONSQUEEZY_STORE_IDDashboard → Settings → Stores
LEMONSQUEEZY_WEBHOOK_SECRETDashboard → 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:

  1. Pasar esa referencia como customData al crear el checkout.
  2. Persistir vos mismo el mapeo (reference -> order id) cuando se dispare el webhook order_created: hay un lugar marcado para esto en app/api/webhooks/lemonsqueezy/route.ts.
  3. 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_created
  • order_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.ts lanza Errors simples con el mensaje de error de la API de Lemon Squeezy cuando está disponible.
  • actions.ts los captura y devuelve { success: false, error }: quien llama nunca necesita try/catch, solo verificar result.success.
  • La route del webhook devuelve 401 ante firmas inválidas, 400 ante payloads malformados y 500 si un handler lanza un error, para que la lógica de reintentos de Lemon Squeezy (backoff exponencial, hasta 3 reintentos) se active correctamente.