@rodmarzavala/recurrente-sdk
v1.2.0
Published
TypeScript SDK para la API de Recurrente — la pasarela de pagos y suscripciones de Guatemala. Zero deps, Edge-first, compatible con Next.js, Astro y Cloudflare Workers.
Maintainers
Readme
@rodmarzavala/recurrente-sdk
SDK no oficial de TypeScript para la API REST de Recurrente 🇬🇹
Hecho en Guatemala para developers guatemaltecos — y para cualquiera que quiera cobrar en quetzales.
🇵🇬 Documentación en español e inglés: rodmarzavala.github.io/recurrente-sdk
¿Por qué este SDK?
Recurrente es la plataforma de pagos y suscripciones líder en Guatemala. Este SDK te da acceso a toda su API desde TypeScript/JavaScript con una experiencia de desarrollo de primer nivel — sin peleas con fetch crudo, sin cobros dobles, sin webhooks inseguros.
| Feature | Detalle |
|---------|---------|
| ⚡️ Edge-first | Usa solo Web APIs estándar — funciona en Cloudflare Workers, Vercel Edge, Deno, Bun y Node.js ≥ 18 sin cambios |
| 📦 Zero dependencias | fetch nativo + Web Crypto API — nada en dependencies |
| 🛡️ Seguro por defecto | Verificación de webhooks con crypto.subtle.verify (tiempo constante) + protección contra replay attacks (ventana 5 min) |
| 💪 Resiliente | Reintentos con exponential backoff para 429 & 5xx · soporte Retry-After · timeout de 30s via AbortController |
| 🎯 100% tipado | TypeScript estricto en todo — noImplicitAny, sin any |
| 🔑 Idempotente | Idempotency-Key generado automáticamente y reutilizado en reintentos — sin cobros dobles |
| 📋 Paginación | Todos los endpoints de lista retornan Page<T> con helpers pageIterator() y autoPagingToArray() |
Instalación
npm install @rodmarzavala/recurrente-sdk
# o
pnpm add @rodmarzavala/recurrente-sdk
# o
yarn add @rodmarzavala/recurrente-sdkRequisito mínimo: Node.js ≥ 18, Deno ≥ 1.38, Bun ≥ 1.0, o cualquier runtime con Fetch API y Web Crypto API.
🪄 Asistente de Configuración (CLI)
El SDK incluye una herramienta de línea de comandos (CLI) interactiva para configurar tu proyecto en segundos. ¡Genera tus variables de entorno y tu ruta de webhooks (ej. para Next.js o Express) automáticamente!
npx @rodmarzavala/recurrente-sdk init🚀 Webhook Forwarder Local (Efecto Stripe CLI)
Desarrollar webhooks en local no debería ser doloroso. No instales ngrok ni pagues por túneles. La CLI del SDK incluye un forwarder que envía los eventos de Recurrente directo a tu localhost, ¡y refirma criptográficamente el payload para que tu código local no falle al verificar las firmas!
npx @rodmarzavala/recurrente-sdk listen --forward-to http://localhost:3000/api/webhooks/recurrenteQuick Start
import { Recurrente } from "@rodmarzavala/recurrente-sdk";
const recurrente = new Recurrente({
publicKey: process.env.RECURRENTE_PUBLIC_KEY!,
secretKey: process.env.RECURRENTE_SECRET_KEY!,
});
// Crea un checkout y redirige al cliente
const checkout = await recurrente.checkouts.create({
items: [
{
name: "Plan Pro",
amount_in_cents: 29900, // Q299.00
currency: "GTQ",
quantity: 1,
},
],
success_url: "https://tudominio.com/gracias",
cancel_url: "https://tudominio.com/cancelar",
});
redirect(checkout.checkout_url);Módulos disponibles
Checkouts
const checkout = await recurrente.checkouts.create({ ... });
const checkout = await recurrente.checkouts.retrieve("ch_abc123");
const page = await recurrente.checkouts.list({ page: 1, items: 20 });Subscriptions
const { subscription, checkout_url } = await recurrente.subscriptions.create({ ... });
const sub = await recurrente.subscriptions.retrieve("su_abc123");
const page = await recurrente.subscriptions.list();
await recurrente.subscriptions.cancel("su_abc123");Refunds
// Reembolso total
const refund = await recurrente.refunds.create({ checkout_id: "ch_abc123" });
// Reembolso parcial
const partial = await recurrente.refunds.create({ checkout_id: "ch_abc123", amount_in_cents: 5000 });
const page = await recurrente.refunds.list({ checkout_id: "ch_abc123" });Products
const page = await recurrente.products.list();
const product = await recurrente.products.retrieve("prod_abc123");
// Puedes pasar `RequestOptions` (idempotencyKey, timeout) como último parámetro en cualquier método
const created = await recurrente.products.create(
{ name: "Plan Pro", ... },
{ idempotencyKey: "req_xyz_123", timeout: 15000 }
);
const updated = await recurrente.products.update("prod_abc123", { name: "Plan Pro v2" });
await recurrente.products.archive("prod_abc123");Customers
const page = await recurrente.customers.list();
const customer = await recurrente.customers.retrieve("cus_abc123");
const created = await recurrente.customers.create({ email: "[email protected]" });Webhook Endpoints
const endpoint = await recurrente.webhookEndpoints.create({
url: "https://myapp.com/webhooks/recurrente",
});
console.log(endpoint.signing_secret); // ¡guárdalo — solo se muestra una vez!
await recurrente.webhookEndpoints.delete(endpoint.id);Paginación
import { pageIterator, autoPagingToArray } from "@rodmarzavala/recurrente-sdk";
// Iterar página por página
for await (const page of pageIterator((p) => recurrente.products.list(p))) {
page.data.forEach((p) => console.log(p.name));
}
// Obtener todos los registros de una sola vez
const all = await autoPagingToArray((p) => recurrente.customers.list(p));Verificación de Webhooks
Verifica que los webhooks entrantes son auténticos y obtén tipado fuerte (Discriminated Union) para el evento.
import { RecurrenteWebhooks } from "@rodmarzavala/recurrente-sdk";
try {
// `constructEvent` verifica la firma y retorna un `RecurrenteEvent` tipado
const event = await RecurrenteWebhooks.constructEvent(
rawBody, // ⚠️ string crudo — NO JSON parseado
{
"svix-id": req.headers["svix-id"],
"svix-timestamp": req.headers["svix-timestamp"],
"svix-signature": req.headers["svix-signature"],
},
process.env.RECURRENTE_WEBHOOK_SECRET! // "whsec_..."
);
switch (event.type) {
case "checkout.succeeded":
console.log(`Pagado: ${event.data.amount_in_cents}`); // event.data es CheckoutResponse
break;
case "subscription.canceled":
console.log(`Cancelada: ${event.data.id}`); // event.data es SubscriptionResponse
break;
}
} catch (err) {
return res.status(401).send("Unauthorized");
}Manejo de errores
import { isRecurrenteError } from "@rodmarzavala/recurrente-sdk";
try {
await recurrente.checkouts.retrieve("ch_nonexistent");
} catch (err) {
if (isRecurrenteError(err)) {
console.error(err.statusCode); // 404
console.error(err.message); // mensaje de error de la API
console.error(err.body); // body completo del error
}
}Reintentos automáticos
El cliente reintenta 429 (rate limit) y 5xx automáticamente con backoff exponencial (max 3 reintentos, cap 30s). Configurable:
const recurrente = new Recurrente({
publicKey: "...",
secretKey: "...",
maxRetries: 5, // o 0 para deshabilitar
});Documentación
📖 rodmarzavala.github.io/recurrente-sdk — Documentación completa en español e inglés.
| Guía | Descripción | |------|-------------| | Inicio Rápido | Instalación, primera request, sandbox vs producción | | API Reference | Todos los métodos, parámetros e interfaces TypeScript | | Webhooks | Verificación, tipos de eventos, ejemplos por framework | | Frameworks | Next.js, Astro, React | | Docs de Recurrente | Documentación oficial de la API de Recurrente |
Compatibilidad
| Runtime | Versión mínima | Estado | |---------|---------------|--------| | Node.js | 18.0.0 | ✅ Soportado | | Cloudflare Workers | Cualquiera | ✅ Soportado | | Vercel Edge Functions | Cualquiera | ✅ Soportado | | Deno | 1.38.0 | ✅ Soportado | | Bun | 1.0.0 | ✅ Soportado | | Browser | Moderno (ES2022+) | ✅ Soportado |
Contribuir
¡Todas las contribuciones son bienvenidas! Ya sea un fix de bug, un módulo nuevo, o una typo en los docs.
👉 Lee CONTRIBUTING.md para empezar.
git clone https://github.com/rodmarzavala/recurrente-sdk.git
cd recurrente-sdk
npm install
npm test # 31 tests, todos deben pasar
npm run typecheck # cero erroresLicense
MIT — ver LICENSE.
Disclaimer: Este es un proyecto open-source independiente y no está oficialmente afiliado ni respaldado por Recurrente.
