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
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() |
| Developer Experience | Incluye un Webhook Forwarder nativo (CLI) para recibir y re-firmar eventos localmente sin usar ngrok. Ahorra horas de setup y depuración en localhost. |
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.
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 initDesarrollar 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/recurrenteimport { 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);const checkout = await recurrente.checkouts.create({ ... });
const checkout = await recurrente.checkouts.retrieve("ch_abc123");
const page = await recurrente.checkouts.list({ page: 1, items: 20 });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");// 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" });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");const page = await recurrente.customers.list();
const customer = await recurrente.customers.retrieve("cus_abc123");
const created = await recurrente.customers.create({ email: "user@example.com" });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);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));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");
}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
}
}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
});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 |
| 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 |
¡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 erroresMIT — ver LICENSE.
Disclaimer: Este es un proyecto open-source independiente y no está oficialmente afiliado ni respaldado por Recurrente.