Skip to content

API Reference

Complete reference for all public methods exported by @rodmarzavala/recurrente-sdk.


new Recurrente(options)

Creates the main SDK instance. Share one instance across your application.

typescript
import { Recurrente } from "@rodmarzavala/recurrente-sdk";

const recurrente = new Recurrente(options);

Options

ParameterTypeRequiredDefaultDescription
publicKeystringPublic API key (pk_live_… or pk_test_…)
secretKeystringSecret API key (sk_live_… or sk_test_…)
baseUrlstringhttps://app.recurrente.comOverride the base URL
maxRetriesnumber3Max retry attempts for 429 / 5xx responses
timeoutnumber30000Request timeout in milliseconds. Set 0 to disable.

Pagination

All list() methods return a Page<T> object. Metadata comes from the API response headers (RFC 8288).

typescript
interface Page<T> {
  data: T[];    // items on this page
  meta: {
    currentPage:  number;
    totalPages:   number;
    totalCount:   number;
    itemsPerPage: number;
    hasNextPage:  boolean;
    hasPrevPage:  boolean;
  };
}

Pagination helpers

typescript
import { pageIterator, autoPagingToArray } from "@rodmarzavala/recurrente-sdk";

// Iterate page by page
for await (const page of pageIterator((p) => recurrente.products.list(p))) {
  console.log(`Page ${page.meta.currentPage} of ${page.meta.totalPages}`);
  page.data.forEach((p) => console.log(p.name));
}

// Get all items as a flat array
const allProducts = await autoPagingToArray(
  (p) => recurrente.products.list(p),
  { limit: 100, itemsPerPage: 20 },
);

PaginationParams

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
itemsnumber20Items per page

recurrente.checkouts

.create(data)Promise<CheckoutResponse>

Creates a new one-time payment checkout session.

Auth: Requires only X-PUBLIC-KEY — safe to call from client-side code.

typescript
const checkout = await recurrente.checkouts.create({
  items: [
    {
      name:            "Suscripción Mensual",
      amount_in_cents: 9900,
      currency:        "GTQ",
      quantity:        1,
      description:     "Acceso completo por 30 días",  // optional
    },
  ],
  success_url:    "https://yourdomain.com/gracias",
  cancel_url:     "https://yourdomain.com/cancelar",
  customer_email: "cliente@ejemplo.com",           // optional
  metadata:       { order_id: "ord_123" },         // optional
});

console.log(checkout.checkout_url); // redirect the customer here

CheckoutItem

FieldTypeRequiredDescription
namestringDisplay name
amount_in_centsnumberAmount in centavos
currencystring"GTQ" or "USD"
quantitynumberQuantity
descriptionstringOptional description
metadataRecord<string, string>Forwarded in webhooks

.retrieve(id)Promise<CheckoutResponse>

typescript
const checkout = await recurrente.checkouts.retrieve("ch_abc123");
console.log(checkout.status); // "paid"

.list(params?)Promise<Page<CheckoutResponse>>

typescript
const page = await recurrente.checkouts.list({ page: 1, items: 20 });
console.log(page.meta.totalCount); // total number of checkouts

CheckoutResponse

typescript
interface CheckoutResponse {
  id:              string;
  status:          "pending" | "paid" | "expired" | "cancelled";
  checkout_url:    string;
  live_mode:       boolean;
  amount_in_cents: number;
  currency:        string;
  success_url:     string;
  cancel_url:      string;
  customer_email:  string | null;
  metadata:        Record<string, string>;
  created_at:      string; // ISO 8601
  updated_at:      string; // ISO 8601
}

recurrente.subscriptions

.create(data)Promise<CreateSubscriptionResponse>

typescript
const { subscription, checkout_url } = await recurrente.subscriptions.create({
  product_id:     "prod_xyz",
  price_id:       "price_monthly",
  customer_email: "cliente@ejemplo.com",
  success_url:    "https://yourdomain.com/gracias",
  cancel_url:     "https://yourdomain.com/cancelar",
  metadata:       { referral: "promo_email" },
});
redirect(checkout_url);

.retrieve(id)Promise<SubscriptionResponse>

typescript
const sub = await recurrente.subscriptions.retrieve("su_abc123");
console.log(sub.status); // "active"

.list(params?)Promise<Page<SubscriptionResponse>>

typescript
const page = await recurrente.subscriptions.list({ page: 1, items: 20 });

.cancel(id)Promise<SubscriptionResponse>

typescript
const cancelled = await recurrente.subscriptions.cancel("su_abc123");
console.log(cancelled.status); // "cancelled"

SubscriptionResponse

typescript
interface SubscriptionResponse {
  id:                   string;
  status:               "active" | "past_due" | "paused" | "cancelled" | "trialing";
  customer_email:       string;
  customer_id:          string;
  product_id:           string;
  price:                SubscriptionPrice;
  current_period_start: string;
  current_period_end:   string;
  cancel_at_period_end: boolean;
  live_mode:            boolean;
  metadata:             Record<string, string>;
  created_at:           string;
  updated_at:           string;
}

recurrente.refunds

.create(data)Promise<RefundResponse>

typescript
// Full refund
const refund = await recurrente.refunds.create({
  checkout_id: "ch_abc123",
});

// Partial refund
const partial = await recurrente.refunds.create({
  checkout_id:    "ch_abc123",
  amount_in_cents: 5000, // refund Q50.00 of a larger payment
  reason:          "Customer request",
});

.retrieve(id)Promise<RefundResponse>

typescript
const refund = await recurrente.refunds.retrieve("ref_abc123");
console.log(refund.status); // "succeeded"

.list(params?)Promise<Page<RefundResponse>>

typescript
// All refunds
const all = await recurrente.refunds.list();

// Filtered by checkout
const forCheckout = await recurrente.refunds.list({ checkout_id: "ch_abc123" });

RefundResponse

typescript
interface RefundResponse {
  id:              string;
  checkout_id:     string;
  amount_in_cents: number;
  currency:        string;
  reason:          string | null;
  status:          "pending" | "succeeded" | "failed";
  live_mode:       boolean;
  created_at:      string;
  updated_at:      string;
}

recurrente.products

.list(params?)Promise<Page<ProductResponse>>

typescript
const page = await recurrente.products.list({ page: 1, items: 10 });

.retrieve(id)Promise<ProductResponse>

typescript
const product = await recurrente.products.retrieve("prod_abc123");

.create(data)Promise<ProductResponse>

typescript
const product = await recurrente.products.create({
  name:        "Plan Pro",
  description: "Acceso completo a todas las funciones",
  success_url: "https://yourdomain.com/gracias",
  cancel_url:  "https://yourdomain.com/cancelar",
});

.update(id, data)Promise<ProductResponse>

typescript
const updated = await recurrente.products.update("prod_abc123", {
  name: "Plan Pro — Actualizado",
});

.archive(id)Promise<ProductResponse>

Soft-deletes a product (sets status to "archived").

typescript
await recurrente.products.archive("prod_abc123");

recurrente.customers

.list(params?)Promise<Page<CustomerResponse>>

typescript
const page = await recurrente.customers.list();

.retrieve(id)Promise<CustomerResponse>

typescript
const customer = await recurrente.customers.retrieve("cus_abc123");

.create(data)Promise<CustomerResponse>

typescript
const customer = await recurrente.customers.create({
  email:     "cliente@ejemplo.com",
  full_name: "Juan García",
  phone:     "+502 5555-5555",
});

.update(id, data)Promise<CustomerResponse>

typescript
const updated = await recurrente.customers.update("cus_abc123", {
  full_name: "Juan García López",
});

CustomerResponse

typescript
interface CustomerResponse {
  id:        string;
  email:     string;
  full_name: string | null;
  phone:     string | null;
  live_mode: boolean;
  metadata:  Record<string, string>;
  created_at: string;
  updated_at: string;
}

recurrente.webhookEndpoints

Manage webhook endpoints programmatically.

.list(params?)Promise<Page<WebhookEndpointResponse>>

typescript
const endpoints = await recurrente.webhookEndpoints.list();

.retrieve(id)Promise<WebhookEndpointResponse>

typescript
const endpoint = await recurrente.webhookEndpoints.retrieve("whe_abc123");

.create(data)Promise<WebhookEndpointResponse>

⚠️ Save signing_secret immediately — it is only returned once.

typescript
const endpoint = await recurrente.webhookEndpoints.create({
  url:         "https://yourdomain.com/webhooks/recurrente",
  description: "Production webhook",
});

// Store this securely — it won't be shown again
console.log(endpoint.signing_secret); // "whsec_..."

.delete(id)Promise<void>

typescript
await recurrente.webhookEndpoints.delete("whe_abc123");

WebhookEndpointResponse

typescript
interface WebhookEndpointResponse {
  id:             string;
  url:            string;
  description:    string | null;
  status:         "enabled" | "disabled";
  signing_secret: string;  // only populated on .create()
  live_mode:      boolean;
  created_at:     string;
  updated_at:     string;
}

RecurrenteWebhooks (static)

.verifySignature(rawBody, headers, secret, options?)Promise<boolean>

Verifies the authenticity of an incoming webhook using HMAC-SHA256 (Svix protocol). Uses crypto.subtle.verifyconstant-time comparison, immune to timing attacks. Rejects events older than 5 minutes by default (replay-attack prevention).

typescript
import { RecurrenteWebhooks } from "@rodmarzavala/recurrente-sdk";

const isValid = await RecurrenteWebhooks.verifySignature(
  rawBody,   // string — the unparsed request body
  {
    "svix-id":        headers["svix-id"],
    "svix-timestamp": headers["svix-timestamp"],
    "svix-signature": headers["svix-signature"],
  },
  process.env.RECURRENTE_WEBHOOK_SECRET!, // "whsec_..."
  { maxAgeSeconds: 300 }, // optional — default 300 (5 min), Infinity to disable
);
ParameterTypeDescription
rawBodystringRaw (un-parsed) request body. Do NOT re-serialize.
headersWebhookHeadersObject with the three svix-* headers
secretstringwhsec_<base64> signing secret
options.maxAgeSecondsnumberMax age in seconds (default 300, Infinity to disable)

Returns false (never throws) on invalid input, missing headers, or expired events.


Error Handling

All methods throw RecurrenteError on non-2xx responses.

typescript
import { RecurrenteError } from "@rodmarzavala/recurrente-sdk";

try {
  await recurrente.checkouts.retrieve("ch_nonexistent");
} catch (error) {
  if (error instanceof RecurrenteError) {
    console.error(error.statusCode); // 404
    console.error(error.message);    // "Not found"
    console.error(error.body);       // { message: "Not found" }
    console.error(error.body.errors); // { field: ["error msg"] } on 422
  }
}

RecurrenteError

typescript
class RecurrenteError extends Error {
  statusCode: number;                 // HTTP status (0 = network error / timeout)
  body: {
    message: string;
    errors?: Record<string, string[]>; // per-field validation errors
  };
}

Retry behavior

  • Retries 429 and 5xx with full-jitter exponential backoff
  • Delay: random(0, min(30s, 1000ms × 2ⁿ))
  • Honors Retry-After header when present
  • Max 3 retries (configurable via maxRetries)
  • 4xx errors are never retried
  • Same idempotency key reused across all retry attempts to prevent duplicate charges

Timeout

Requests time out after 30 seconds by default. Override globally or per-request:

typescript
// Global
const recurrente = new Recurrente({ ..., timeout: 10_000 }); // 10 s

// Per-request (not yet exposed on modules — use RecurrenteClient directly)

Released under the MIT License.