npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@retorna-tech/retorna-gateway-sdk

v1.0.4

Published

Retorna Gateway SDK — SDK oficial para integrar Retorna Gateway (B2B API) en aplicaciones Node.js y TypeScript. Autenticación OAuth2 y firma de requests, cotizaciones, órdenes, rutas, billeteras y reportes mediante una interfaz fluida y tipada.

Readme

Retorna Gateway SDK

Versión 2 del SDK para integrar los servicios B2B de Retorna (CPG API) en aplicaciones Node.js y TypeScript. Expone, mediante una interfaz fluida y tipada, las operaciones de billetera, rutas, cotizaciones y órdenes del nuevo b2b-service.

Diferencias con v1:

  • Autenticación: OAuth2 client credentials (Basic auth en /oauth2/token, Bearer en el resto) + firma RSA por request (headers signature y nonce). Todos los endpoints del canal api verifican la firma.
  • Contrato /me/* token-scoped: cada llamada devuelve los datos de la empresa que autentica el token. No hay parámetro ownerId — una credencial = una empresa.
  • snake_case en todo el límite HTTP. Montos como strings decimales ("56.00"), nunca números.

Requisitos

  • Node.js >= 18.18 (usa fetch nativo; sin dependencias en runtime).
  • Se distribuye en ESM y CommonJS con tipos incluidos: import { … } from "@retorna-tech/retorna-gateway-sdk" o const { … } = require("@retorna-tech/retorna-gateway-sdk").

Instalación

npm install @retorna-tech/retorna-gateway-sdk

Requisitos:

  • Node.js >= 18 (native fetch). Sin dependencias de runtime.
  • RETORNA_CLIENT_ID + RETORNA_CLIENT_SECRET (OAuth2) y RETORNA_PRIVATE_KEY (clave privada RSA en PEM, para firmar cada request). Los tres se provisionan al crear la organización.

Quickstart

import {
  RetornaClientBuilder,
  RetornaEnvironment,
  RetornaB2BError,
} from "@retorna-tech/retorna-gateway-sdk";
import { randomUUID } from "node:crypto";

// 1. Construir el cliente
//    scope: dev y sandbox → "sandbox/full_access"; prod → "prod/full_access"
const client = new RetornaClientBuilder()
  .clientId(process.env.RETORNA_CLIENT_ID!)
  .clientSecret(process.env.RETORNA_CLIENT_SECRET!)
  .privateKey(process.env.RETORNA_PRIVATE_KEY!)
  .scope("sandbox/full_access") // prod → "prod/full_access"
  .environment(RetornaEnvironment.DEVELOP)
  .loggingLevel("error")
  .buildClient();

// 2. Mi billetera (el balance es el campo `amount`)
const wallet = await client.getMyWallet();
console.log(wallet.status, wallet.currency, wallet.amount);
// wallet.funding_methods → direcciones de depósito para fondear la billetera:
// "funding_methods": [
//   { "type": "CRYPTO", "asset_code": "USDC", "network": "BASE_TESTNET", "address": "0x703F…", "status": "ACTIVE" }
// ]

// 3. Elegir un corredor (corridor) entre las rutas de mi empresa
const { routes } = await client.getRoutes();
const route = routes[0]; // { name, method, country, limits }

// 4. Cotizar (source.currency SIEMPRE "USDR"; montos como strings)
const quotation = await client.createQuotation({
  source: { currency: "USDR" }, // sólo `currency`: la API rechaza campos extra
  destination: {
    country: route.country,
    currency: "VES",
    // payout_method.type: BANK_TRANSFER | P2P_PHONE_TRANSFER | CRYPTO_TRANSFER
    // mirar en la doc: https://docs.gateway.retorna.app/glossary/payout-type
    payout_method: { type: "P2P_PHONE_TRANSFER" },
  },
  quote: { mode: "SEND_EXACT", amount: "10" },
});
console.log(quotation.id, quotation.exchange_rate.value, quotation.target.amount);

// quotation.destination espeja el destination que mandaste: sirve para
// verificar, ANTES de crear la orden, que payment_instructions coincide con
// el payout_method cotizado (bank_account ↔ BANK_TRANSFER, bank_phone_account
// ↔ P2P_PHONE_TRANSFER). Si no coincide, POST /orders responde 422 B2B_QUOTATION_MISMATCH.
// country y payout_method.type vienen null sólo si CPG no pudo asociar una
// tasa a la cotización (anomalía del proveedor): no crees una orden sobre ella.
console.log(quotation.destination.country, quotation.destination.payout_method.type);

// 5. Crear una orden con esa cotización
//    Pasá un idempotency key explícito para que el reintento sea seguro.
const idempotencyKey = randomUUID();

const order = await client.createOrder(
  {
    quotation_id: quotation.id,
    purpose: "FAMILY_SUPPORT", // obligatorio — catálogo cerrado ORDER_PURPOSES
    sender: {
      names: "Ana María",
      last_names: "Pérez Gómez",
      document: { type: "CC", id: "1020304050", country: "CO" },
      phone: "+573001234567", // opcional
      email: "[email protected]", // opcional
      country: "CO",
    },
    destination: {
      receiver: {
        names: "Carlos Andrés",
        last_names: "Rodríguez López",
        document: { type: "V", id: "12345678", country: "VE" },
        email: "[email protected]", // opcional
        country: "VE",
      },
      // Polimórfico: enviá EXACTAMENTE UNO de bank_account (BANK_TRANSFER)
      // o bank_phone_account (P2P_PHONE_TRANSFER).
      payment_instructions: {
        // bank_name: catálogo cerrado (FINANCIAL_ENTITY_CODES). Un banco fuera
        // del catálogo se rechaza client-side.
        bank_phone_account: { bank_name: "BNC_VE", phone_number: "+584121234567", document_id: "12345678" },
      },
    },
  },
  idempotencyKey
);
console.log(order.id, order.status);

[!NOTE] idempotency_key no es un campo del request — lo asigna el servicio (es la idempotency key enviada en X-Idempotency-Key) y la idempotency key vuelve en el response como order.idempotency_key.

Para un payout P2P por teléfono, usá bank_phone_account en lugar de bank_account. document_id es opcional: si falta, el servicio usa destination.receiver.document.id.

payment_instructions: {
  bank_phone_account: { bank_name: "BANCO_VENEZUELA_VE", phone_number: "+584121234567" },
}

purpose (concepto de la remesa) es obligatorio y debe pertenecer al catálogo ORDER_PURPOSES (FAMILY_SUPPORT, PAYMENT_OF_BILLS, …).

sender.phone (E.164) y sender.email son opcionales — igual que destination.receiver.email — y sólo vuelven en la respuesta si los mandaste al crear la orden.


Modelo de autenticación

Dos capas independientes: token OAuth2 + firma RSA por request. Las dos son obligatorias en el canal api.

1. Firma de request (RSA-SHA256)

Cada endpoint del canal api está anotado con @VerifySignature(...) en b2b-service. El SDK firma automáticamente con la privateKey configurada y agrega dos headers:

| Header | Contenido | |---|---| | signature | RSA-SHA256 en base64 del mensaje canónico | | nonce | Timestamp en milisegundos (Date.now()), válido ±5 minutos |

El mensaje canónico depende del método:

  • Lecturas (GET / DELETE): `${path}?${queryString}${nonce}` — el path es la ruta de la API sin base URL ni stage.
  • Escrituras (POST / PUT / PATCH): `${JSON.stringify(rawBody)}${nonce}` — el body crudo re-encodeado, no el objeto serializado una sola vez.

[!WARNING] El nonce tiene que ser numérico. El guard rechaza cualquier cosa que parseInt no pueda leer (el middleware legacy dejaba pasar un UUID por accidente; éste no).

.disableSignature() existe como escape hatch para stubs locales — contra un deployment real la API responde 401.

2. Token OAuth2

OAuth2 client credentials (RFC 6749), transparente:

  1. En el primer request autenticado, el SDK llama a POST /oauth2/token (Cognito hosted UI) con Authorization: Basic base64(clientId:clientSecret) y el scope configurado.
  2. El token se cachea hasta expires_in - 60s (buffer de seguridad).
  3. Ante un 401/403 con el token cacheado ya expirado, el SDK refresca y reintenta una vez. Si el token aún no había expirado, relanza el error de inmediato (no oculta mal uso de credenciales).

Scopes por entorno

| Entorno | scope | |---|---| | DEVELOP | sandbox/full_access | | SANDBOX | sandbox/full_access | | PRODUCTION | prod/full_access |

El scope es opcional en el builder: si no lo pasás, cae al default del entorno (la tabla de arriba).


Reintentos seguros (idempotency key)

POST /orders requiere el header X-Idempotency-Key (UUID v4). El SDK genera uno solo si no lo pasás.

[!TIP] Para que reintentar createOrder sea seguro (que un corte de red nunca produzca un duplicado), pasá tu propia idempotency key y reusala en el reintento:

import { randomUUID } from "node:crypto";

const key = randomUUID();
await client.createOrder(request, key); // misma key en el reintento → sin duplicado

Listar órdenes

listOrders usa paginación por cursor:

// Primera página (limit por defecto 20, máx 100)
let page = await client.listOrders({ limit: 20 });
for (const order of page.data) {
  console.log(order.id, order.status, order.idempotency_key);
}

// Página siguiente usando el cursor
if (page.pagination.has_more) {
  page = await client.listOrders({
    cursor: page.pagination.next_cursor!,
    limit: 20,
  });
}

// Filtrar por estado o por rango de fechas
const pending = await client.listOrders({ status: "PENDING" });
const recent = await client.listOrders({ created_from: "2026-01-01T00:00:00Z" });

// Filtrar por tipo de transacción CPG (catálogo abierto — pertenece a CPG,
// no a este SDK). Un único valor: "type=A,B" es un 400 desde CPG.
const wallet2pog = await client.listOrders({ type: "WALLET_TO_POG" });

Transacciones de la billetera

getMyWalletTransactions(type, params?) lista los movimientos de tu billetera. Usa paginación por cursor, igual que listOrders — page ya no existe y mandarlo devuelve 400. El type es el tipo de movimiento (hoy: "top_up"):

// Primera página (máx 100)
let txs = await client.getMyWalletTransactions("top_up", { limit: 20 });

for (const tx of txs.data) {
  console.log(tx.id, tx.type, tx.amount, tx.currency, tx.status, tx.created_at);
  // tx.status → "PENDING" | "COMPLETED" (ausente si CPP no reportó estado)
  // tx.crypto_deposit → { code, network, tx_hash } del depósito on-chain
}

// Página siguiente con el cursor
if (txs.pagination.has_more) {
  txs = await client.getMyWalletTransactions("top_up", {
    cursor: txs.pagination.next_cursor!,
    limit: 20,
  });
}

El tercer parámetro es la moneda del path (/me/wallets/:currency), por defecto "USDR". Hoy el servidor sólo valida su forma: la organización tiene una única billetera.


Reportes

Los endpoints reports/* son un passthrough a b2b-reports-service. createReport devuelve 202: el reporte queda pedido, no hecho.

// Tipos disponibles y sus columnas
const { data: types } = await client.getReportTypes();

// Pedir un reporte — fechas ISO-8601 CON offset explícito (una fecha pelada se rechaza)
const { id } = await client.createReport({
  type: "orders",
  date_from: "2026-07-01T00:00:00-04:00",
  date_to: "2026-07-31T23:59:59-04:00",
  format: "CSV",
});

// Estado y descarga (link pre-firmado de S3, no el archivo)
const detail = await client.getReport(id);
if (detail.status === "COMPLETED") {
  const { url, expires_at } = await client.getReportDownloadLink(id, 300);
}

// Listado — este endpoint sí usa paginación por OFFSET
const page = await client.listReports({ page: 1, limit: 50, status: "COMPLETED" });

// Filtrar por origen: true = sólo programados, false = sólo pedidos manualmente,
// ausente = ambos
const scheduled = await client.listReports({ is_scheduled: true });

// Programaciones y entrega por webhook
await client.saveReportGeneration("orders", "DAILY", { format: "CSV", timezone: "America/Caracas" });
await client.saveReportDelivery("orders", "WEBHOOK", {
  is_active: true,
  config: { url: "https://hooks.example.com/reports", secret: "un-secreto-de-16+" },
});

Identificación del SDK

Cada request lleva un header que dice qué SDK y qué versión está llamando. Son automáticos: no hay nada que configurar y un caller no los puede pisar.

| Header | Ejemplo | Para qué | |---|---|---| | X-Retorna-Client | typescript-sdk/1.0.0 | Qué SDK y qué versión, en un solo valor <cliente>/<versión>. Contrato común de la flota (java-sdk/…, typescript-sdk/…) | | User-Agent | typescript-sdk/1.0.0 (node/22.11.0; darwin arm64) | El único que los access logs de ALB/API Gateway ya registran sin cambios en el backend; el paréntesis trae el runtime |

El token typescript-sdk es SDK_CLIENT y la versión es SDK_VERSION (src/core/version.ts). Un test ata la versión a package.json: si bumpeás la versión y te olvidás de la constante, la suite falla en vez de mentir en el header.

Además, cada request lleva Accept: application/json por default (salvo que ya hayas puesto el tuyo): el API Gateway de B2B exige un mínimo de User-Agent + Accept en cada request.

import { SDK_CLIENT, SDK_VERSION, CLIENT_HEADER_VALUE, SDK_IDENTITY_HEADERS } from "@retorna-tech/retorna-gateway-sdk";
// CLIENT_HEADER_VALUE === "typescript-sdk/1.0.0"

[!NOTE] El request del token va a Cognito, no a Retorna: ese no lleva estos headers.


Opciones del builder

| Método | Descripción | Default | |---|---|---| | .clientId(string) | Client ID | — (requerido salvo disableAuth) | | .clientSecret(string) | Client Secret | — (requerido salvo disableAuth) | | .privateKey(string) | Clave privada RSA (PEM) para firmar cada request | — (requerido salvo disableAuth / disableSignature) | | .scope(string) | OAuth2 scope | env default (ver "Scopes por entorno") | | .environment(env) | DEVELOP / SANDBOX / PRODUCTION | DEVELOP | | .baseUrlOverride(url) | URL base personalizada (https:// only) | env default | | .authUrlOverride(url) | URL del token endpoint (https:// only) | Cognito hosted UI | | .disableAuth() | Sin token ni headers de auth (transicional) | — | | .disableSignature() | Sin firma de request (escape hatch: la API real responde 401) | — | | .loggingLevel(level) | none / error / warn / info / debug | error | | .disableLogging() | Desactiva todos los logs | — | | .retries(n) | Reintentos en errores de red y 5xx/429 | 3 | | .backoffMs(ms) | Backoff inicial en ms (exponencial) | 200 |


Endpoints

| Operación | Método | Path | Método SDK | |---|---|---|---| | Token | POST | /oauth2/token | (automático) | | Mi cliente | GET | /me/client | getMyClient() | | Mi billetera | GET | /me/wallets/:currency | getMyWallet(currency?) | | Transacciones de billetera | GET | /me/wallets/:currency/transactions/:type | getMyWalletTransactions(type, params?, currency?) | | Rutas | GET | /me/routes | getRoutes(params?) | | Crear cotización | POST | /quotations | createQuotation(req) | | Cotización por id | GET | /quotations/:id | getQuotation(id) | | Crear orden | POST | /orders | createOrder(req, idempotencyKey?) | | Listar órdenes | GET | /orders | listOrders(params?) | | Orden por id | GET | /orders/:id | getOrder(id) | | Tipos de reporte | GET | /reports/types | getReportTypes() | | Pedir reporte | POST | /reports | createReport(req) | | Listar reportes | GET | /reports | listReports(params?) | | Reporte por id | GET | /reports/:id | getReport(id) | | Link de descarga | GET | /reports/:id/download | getReportDownloadLink(id, ttlSeconds?) | | Preferencias de columnas | GET/PUT/DELETE | /reports/types/:type/preferences | getReportPreferences / saveReportPreferences / deleteReportPreferences | | Programaciones | GET/PUT/PATCH/DELETE | /reports/types/:type/generations[/:frequency] | getReportGenerations / saveReportGeneration / setReportGenerationActive / deleteReportGeneration | | Entregas | GET/PUT/DELETE | /reports/types/:type/delivery[/:method] | getReportDeliveries / getReportDelivery / saveReportDelivery / deleteReportDelivery |

Paginación: órdenes y transacciones de billetera usan cursor (pagination.{next_cursor,has_more,limit,total}); el listado de reportes usa offset ({page,limit,total,total_pages}).


Manejo de errores

import { RetornaB2BError, RetornaSdkError } from "@retorna-tech/retorna-gateway-sdk";

try {
  await client.createOrder(req);
} catch (err) {
  if (err instanceof RetornaB2BError) {
    console.error(err.code);          // "B2B_INSUFFICIENT_BALANCE"
    console.error(err.category);      // "PERMANENT" | "TRANSIENT" | "UNKNOWN"
    console.error(err.httpStatus);    // 422
    console.error(err.correlationId); // string | undefined
    console.error(err.details);       // string[] | undefined — solo en B2B_PROVIDER_REJECTED
  } else if (err instanceof RetornaSdkError) {
    console.error(err.message);       // validación client-side o respuesta no estructurada
  }
}

details aparece únicamente en B2B_PROVIDER_REJECTED: es el veredicto textual de la red de pagos, campo por campo (p. ej. "members.1.receiver.beneficiary documentType must be one of V, E, J, G"). Sirve para saber QUÉ corregir; no es parte estable del contrato, así que loguealo y mostráselo a operaciones, pero no ramifiques sobre él.

Códigos de error B2B

| Código | HTTP | Categoría | |---|---|---| | B2B_UNAUTHORIZED | 401 | PERMANENT | | B2B_QUOTATION_NOT_FOUND | 404 | PERMANENT | | B2B_QUOTATION_EXPIRED | 422 | PERMANENT | | B2B_QUOTATION_MISMATCH | 422 | PERMANENT | | B2B_QUOTATION_ALREADY_USED | 409 | PERMANENT | | B2B_INSUFFICIENT_BALANCE | 422 | PERMANENT | | B2B_BENEFICIARY_INVALID | 422 | PERMANENT | | B2B_RATE_NOT_FOUND | 422 | PERMANENT | | B2B_ORDER_NOT_FOUND | 404 | PERMANENT | | B2B_IDEMPOTENCY_CONFLICT | 409 | PERMANENT | | B2B_COMPANY_NOT_FOUND | 404 | PERMANENT | | B2B_WALLET_NOT_CONFIGURED | 409 | PERMANENT | | B2B_WALLET_NOT_FOUND | 404 | PERMANENT | | B2B_INVALID_PAGINATION_CURSOR | 400 | PERMANENT | | B2B_FUNDING_INVALID_REQUEST | 400 | PERMANENT | | B2B_FUNDING_NOT_ELIGIBLE | 422 | PERMANENT | | B2B_FUNDING_NOT_AUTHORIZED | 403 | PERMANENT | | B2B_RATE_LIMITED | 429 | TRANSIENT | | B2B_PROVIDER_REJECTED | 422 | PERMANENT | | B2B_PROVIDER_UNAVAILABLE | 503 | TRANSIENT | | B2B_UNKNOWN | 500 | UNKNOWN |


Validaciones client-side

El SDK valida antes de hacer el request y lanza errores descriptivos:

  • source.currency → siempre USDR (origen de fondos B2B).
  • quote.amount y montos → string decimal con hasta 6 decimales (^\d+(\.\d{1,6})?$).
  • payout_method.type → BANK_TRANSFER | P2P_PHONE_TRANSFER | CRYPTO_TRANSFER (ver glosario).
  • quote.mode → SEND_EXACT | RECEIVE_EXACT.
  • country (destination/sender/receiver/document) → 2 caracteres ISO alpha-2. source sólo lleva currency.
  • purpose → catálogo cerrado ORDER_PURPOSES (obligatorio en POST /orders).
  • bank_name → catálogo cerrado FINANCIAL_ENTITY_CODES (exportado por el SDK).
  • quotation_id → formato UUID. X-Idempotency-Key → UUID v4 (auto-generado si no se pasa).
  • email del receiver → formato válido (opcional).
  • bank_account.type → CHECKING | SAVINGS | CORRIENTE | AHORRO.
  • payment_instructions → exactamente uno de bank_account o bank_phone_account; phone_number en formato E.164; document_id opcional.
  • currency del path de billetera → 3 a 5 letras MAYÚSCULAS.
  • limit (1..100); page (≥1) sólo en reportes.
  • Fechas de reportes → ISO-8601 con offset explícito; URLs de entrega → https://.
  • Ids de recursos → entre 1 y 100 caracteres.

Entornos

| Entorno | RetornaEnvironment | Base URL | Scope | |---|---|---|---| | Desarrollo | DEVELOP | https://ifomkws6s0.execute-api.us-east-1.amazonaws.com/dev | sandbox/full_access | | Sandbox | SANDBOX | https://api.gateway.sandbox.retorna.app | sandbox/full_access | | Producción | PRODUCTION | https://api.gateway.retorna.app | prod/full_access |

baseUrlOverride sirve para apuntar a cualquier otro host (solo https://):

const client = new RetornaClientBuilder()
  .clientId("id")
  .clientSecret("secret")
  .baseUrlOverride("https://ifomkws6s0.execute-api.us-east-1.amazonaws.com/dev")
  .buildClient();

Entornos del SDK: develop / sandbox / production. En la infra el entorno sandbox se llama stg (stage de API Gateway y dominio Cognito b2b-retorna-stg); el SDK no expone ese nombre.


Publicación (mantenedores)

  1. Actualizá version en package.json y SDK_VERSION en src/core/version.ts (un test los ata), y el CHANGELOG.md.
  2. Mergeá a main y creá el tag: git tag v1.0.0 && git push origin v1.0.0.
  3. El workflow Release verifica que el tag coincida con la versión, corre typecheck, tests y verify:pack, y publica en npm. Necesita el secret NPM_TOKEN (granular access token con permiso de publicación sobre el paquete) en el environment npm del repo. Sin provenance: npm solo la emite desde repos públicos y este es interno.

Licencia

Software propietario de Retorna. Su uso está permitido únicamente para integrar sistemas con Retorna Gateway bajo un acuerdo comercial vigente con Retorna. No se permite redistribuir, modificar ni sublicenciar. Ver LICENSE.