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

aex-sdk-typescript

v0.1.0

Published

SDK no oficial de AEX para Node.js / TypeScript. Cliente tipado para envíos, cotizaciones, guías, seguimiento y webhooks.

Readme

aex-sdk-typescript

SDK no oficial de AEX para Node.js / TypeScript.

Cliente tipado para la API de envíos y logística de AEX Paraguay:

  • Autenticación automática — el SDK gestiona el código de autorización (token de 10 min) y lo renueva de forma transparente
  • Cotizar envío (/envios/calcular)
  • Solicitar servicio (/envios/solicitar_servicio) — genera oferta de servicios
  • Confirmar servicio (/envios/confirmar_servicio) — genera la guía de transporte
  • Cancelar servicio (/envios/cancelar)
  • Imprimir guía (/envios/imprimir) — devuelve el PDF
  • Seguimiento / tracking (/envios/tracking)
  • Gestión de novedades (/envios/gestion_novedad)
  • Modificar guía (/actualizar-guia.php?accion=modificar)
  • Listar ciudades (/envios/ciudades) y puntos de entrega (/envios/puntos_entrega)
  • Inventario (/inventario/existencia)
  • Webhooks — parseo, validación y helpers de respuesta
  • ✅ Tipado completo en TypeScript, sin dependencias externas
  • ✅ Funciona en Node 18+ (usa fetch nativo)

Tabla de contenidos


Instalación

npm install aex-sdk-typescript
# o
pnpm add aex-sdk-typescript
# o
yarn add aex-sdk-typescript

Configuración

Necesitás dos claves que te proporciona AEX al habilitar tu cuenta para integración:

| Variable | Descripción | | --- | --- | | AEX_PUBLIC_KEY | clave_publica que identifica a la entidad/usuario. | | AEX_PRIVATE_KEY | clave_privada de autorización. NUNCA debe exponerse al cliente. |

A diferencia de otros servicios, la clave privada no se envía directamente: el SDK calcula md5(clave_privada + codigo_sesion) donde codigo_sesion es un nonce aleatorio generado en cada autenticación. El token resultante (codigo_autorizacion) dura 10 minutos y se renueva de forma automática.


Uso rápido

import { AexClient, TIPO_CARGA } from 'aex-sdk-typescript';

const aex = new AexClient({
  publicKey: process.env.AEX_PUBLIC_KEY!,
  privateKey: process.env.AEX_PRIVATE_KEY!,
  sandbox: true, // usar entorno de prueba
});

// 1. Cotizar
const servicios = await aex.calcular({
  origen: '1',
  destino: '2',
  paquetes: [{ peso: 1, largo: 10, alto: 10, ancho: 10 }],
  codigo_tipo_carga: TIPO_CARGA.PAQUETE,
});

// 2. Solicitar servicio (genera la oferta)
const solicitud = await aex.solicitarServicio({
  origen: '1',
  destino: '2',
  codigo_operacion: 'PED-001',
  paquetes: [{ peso: 1, largo: 10, alto: 10, ancho: 10 }],
});

// 3. Confirmar el servicio elegido (genera la guía)
const condicion = solicitud.condiciones[0]!;
const { numero_guia } = await aex.confirmarServicio({
  id_solicitud: solicitud.id_solicitud,
  id_tipo_servicio: condicion.id_tipo_servicio,
  pickup: {
    calle_principal: 'Av. Mariscal López',
    calle_transversal_1: 'Av. España',
    codigo_ciudad: '1',
  },
  destinatario: {
    codigo: 'CLI-001',
    numero_documento: '1234567',
    nombre: 'Juan Pérez',
    email: '[email protected]',
    telefonos: [{ numero: '0981123456' }],
  },
  entrega: {
    calle_principal: 'Calle Palma',
    calle_transversal_1: 'Calle Estrella',
    codigo_ciudad: '2',
  },
});

// 4. Imprimir la guía
const pdf = await aex.imprimir({ guia: numero_guia, formato: 'guia_A4' });

// 5. Tracking
const eventos = await aex.tracking({ numero_guia });

Casos de uso

1. Cotizar un envío

const servicios = await aex.calcular({
  origen: '1',        // código de ciudad de origen
  destino: '2',       // código de ciudad de destino
  codigo_tipo_carga: TIPO_CARGA.PAQUETE, // o TIPO_CARGA.DOCUMENTO
  paquetes: [
    {
      descripcion: 'Caja de libros',
      cantidad: 2,
      peso: 1.5,       // kg
      largo: 30,       // cm
      alto: 20,        // cm
      ancho: 15,       // cm
      valor: 150000,   // PYG (valor declarado para seguro)
    },
  ],
});

for (const s of servicios) {
  console.log(s.tipo_servicio, s.costo_flete, s.tiempo_entrega);
}

2. Solicitar y confirmar un servicio (crear guía)

El flujo es de dos pasos:

  1. solicitarServicio → devuelve id_solicitud + lista de condiciones (servicios disponibles con precios y puntos de entrega).
  2. confirmarServicio → confirma la condición elegida con los datos de pickup/entrega y devuelve el numero_guia.
import { FORMA_PAGO_AEX, PERSONERIA, TIPO_DOCUMENTO } from 'aex-sdk-typescript';

// Paso 1
const solicitud = await aex.solicitarServicio({
  origen: '1',
  destino: '2',
  codigo_operacion: 'PED-001',
  paquetes: [{ peso: 1, largo: 10, alto: 10, ancho: 10 }],
  importe_cobro: 100000, // contra reembolso (opcional)
});

// Paso 2 — elegir el servicio y enviar los datos
const condicion = solicitud.condiciones[0]!;
const { numero_guia } = await aex.confirmarServicio({
  id_solicitud: solicitud.id_solicitud,
  id_tipo_servicio: condicion.id_tipo_servicio,
  adicionales: condicion.adicionales
    .filter(a => a.preseleccionado)
    .map(a => a.id_adicional),
  codigo_forma_pago: FORMA_PAGO_AEX.CREDITO,
  pickup: {
    calle_principal: 'Av. Mariscal López 1234',
    calle_transversal_1: 'Av. España',
    codigo_ciudad: '1',
    referencias: 'Casa azul con rejas',
    disponible_desde: '2025-06-25 09:00:00',
    disponible_hasta: '2025-06-25 18:00:00',
  },
  destinatario: {
    codigo: 'CLI-001',
    tipo_documento: TIPO_DOCUMENTO.CIP,
    numero_documento: '1234567',
    nombre: 'Juan',
    apellido: 'Pérez',
    email: '[email protected]',
    personeria: PERSONERIA.FISICA,
    telefonos: [{ numero: '0981123456', denominacion: 'Móvil' }],
  },
  entrega: {
    calle_principal: 'Calle Palma 567',
    calle_transversal_1: 'Calle Estrella',
    codigo_ciudad: '2',
  },
});

console.log('Guía creada:', numero_guia);

Con punto de entrega: si querés usar un CAC o ELOCKER en lugar de una dirección, enviá id_punto_entrega en pickup y/o entrega (obtenido de puntosEntrega). Los demás campos de dirección se omiten.

3. Imprimir la guía en PDF

import { FORMATO_IMPRESION } from 'aex-sdk-typescript';

const pdf: Buffer = await aex.imprimir({
  guia: numero_guia,
  formato: FORMATO_IMPRESION.GUIA_A4,
  imprimir_partida: false, // una guía por producto si true
});

// Guardar a disco
import { writeFileSync } from 'node:fs';
writeFileSync(`guia-${numero_guia}.pdf`, pdf);

// O enviar como respuesta HTTP
// res.setHeader('content-type', 'application/pdf');
// res.send(pdf);

Formatos disponibles: etiqueta65x45, etiqueta8x10, etiqueta8x6, guia_A4, guia_A5, guia_A6.

4. Seguimiento de una guía

// Por número de guía
const eventos = await aex.tracking({ numero_guia: 'A002866303' });

// O por código de operación (ID de pedido)
const eventos2 = await aex.tracking({ codigo_operacion: 'PED-001' });

for (const e of eventos) {
  console.log(e.fecha, e.estado, e.tipo_evento, e.observacion);
}

5. Cancelar un servicio

await aex.cancelar({ numero_guia: 'A002866303' });

Solo pueden cancelarse servicios que no hayan tenido gestión operativa.

6. Modificar indicaciones de pickup/entrega

const result = await aex.modificarGuia({
  numero_guia: 'A002866303',
  modificar: {
    indicaciones_pickup: 'Timbre 2, segundo piso',
    indicaciones_entrega: 'Entregar a conserjería',
  },
});

console.log(result.campos_actualizados);
// ['indicaciones_pickup', 'indicaciones_entrega']

7. Listar ciudades disponibles

// Todas las ciudades
const ciudades = await aex.ciudades();

// Filtrar ciudades destino habilitadas para un origen
const destinos = await aex.ciudades('1'); // origen = código de ciudad

// Puntos de entrega para un servicio específico
const puntos = await aex.puntosEntrega({
  id_tipo_servicio: 1,
  origen: '1',
  destino: '2',
});

8. Webhook de eventos de seguimiento

AEX notifica a tu sistema mediante HTTP POST cuando ocurre un evento en una guía. Configurá la URL en AEX y procesá los eventos:

import {
  parseWebhook,
  webhookOk,
  webhookError,
  verifyWebhookHeader,
} from 'aex-sdk-typescript';
import crypto from 'node:crypto';

app.post('/aex/webhook', async (req, res) => {
  // 1. Validar header de autenticación (configurado en AEX)
  const authHeader = req.headers['authorization'];
  if (!verifyWebhookHeader(authHeader, `Bearer ${process.env.AEX_WEBHOOK_TOKEN!}`)) {
    return res.status(401).json(webhookError('No autorizado'));
  }

  // 2. Parsear el evento
  let evento;
  try {
    evento = parseWebhook(req.body);
  } catch {
    return res.status(400).json(webhookError('Payload inválido'));
  }

  // 3. Idempotencia: evitar procesar duplicados (AEX reintenta hasta 4 veces)
  const idempotencyKey = crypto
    .createHash('sha1')
    .update(`${evento.guia}|${evento.fecha}|${evento.codigo_tipo_evento ?? ''}`)
    .digest('hex');

  const ya = await db.webhooks.findByKey(idempotencyKey);
  if (ya) return res.status(200).json(webhookOk());

  // 4. Procesar el evento
  await db.transaction(async (tx) => {
    await tx.guias.updateEstado(evento.guia, {
      estado: evento.estado,
      codigoEstado: evento.codigo_estado,
      fechaUltimoEvento: evento.fecha,
    });
    await tx.webhooks.save({ ...evento, idempotencyKey });
  });

  // 5. Responder a AEX
  return res.status(200).json(webhookOk());
});

9. Inventario / existencia de productos

// Todos los productos
const productos = await aex.inventarioExistencia();

// Productos específicos
const algunos = await aex.inventarioExistencia({
  codigos_producto: ['SKU-001', 'SKU-002'],
});

for (const p of productos) {
  console.log(p.codigo_producto, p.existencia, p.denominacion);
}

10. Reintentos y manejo de errores

import {
  AexApiError,
  AexAuthError,
  AexNetworkError,
} from 'aex-sdk-typescript';

async function conReintento<T>(fn: () => Promise<T>, intentos = 3): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < intentos; i++) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      // Sóolo reintentar errores de red, no errores lógicos
      if (err instanceof AexNetworkError) {
        await new Promise((r) => setTimeout(r, 500 * 2 ** i));
        continue;
      }
      throw err;
    }
  }
  throw lastErr;
}

try {
  const servicios = await conReintento(() =>
    aex.calcular({ origen: '1', destino: '2', paquetes: [...] }),
  );
} catch (err) {
  if (err instanceof AexAuthError) {
    // Credenciales inválidas o token expirado irrecuperable
    logger.error({ err }, 'AEX: error de autenticación');
  } else if (err instanceof AexApiError) {
    // err.codigo    → código de error de AEX (ej: "2")
    // err.mensaje   → mensaje descriptivo
    // err.endpoint  → path invocado
    // err.raw       → cuerpo crudo de la respuesta
    logger.error({ err }, 'AEX: error lógico');
  } else if (err instanceof AexNetworkError) {
    logger.error({ err }, 'AEX: inalcanzable, reintentar luego');
  } else {
    throw err;
  }
}

Helpers de bajo nivel

Si necesitás manejar la autenticación manualmente (tests, integraciones legacy):

import {
  md5,
  hashClavePrivada,
  generarCodigoSesion,
} from 'aex-sdk-typescript';

// md5(value) → hash MD5 hexadecimal
md5('hola'); // '4d186321c1a7f0f354b297e8914ab240'

// hashClavePrivada(clavePrivada, codigoSesion) → md5(clave + sesion)
const sesion = generarCodigoSesion(); // UUID aleatorio
const hash = hashClavePrivada(process.env.AEX_PRIVATE_KEY!, sesion);

// Llamada manual al endpoint de autorización:
// POST /autorizacion-acceso/generar
// { clave_publica, clave_privada: hash, codigo_sesion: sesion }

Constantes

import {
  FORMATO_IMPRESION,
  TIPO_CARGA,
  FORMA_PAGO_AEX,
  TIPO_DOCUMENTO,
  PERSONERIA,
  TIPO_PUNTO_ENTREGA,
} from 'aex-sdk-typescript';

// Formatos de impresión de guía
FORMATO_IMPRESION.ETIQUETA_65X45 // 'etiqueta65x45' — 6,5 x 4,5 cm
FORMATO_IMPRESION.ETIQUETA_8X10  // 'etiqueta8x10'  — 8 x 10 cm
FORMATO_IMPRESION.ETIQUETA_8X6   // 'etiqueta8x6'   — 8 x 6 cm
FORMATO_IMPRESION.GUIA_A4        // 'guia_A4'       — A4 (pickup + entrega)
FORMATO_IMPRESION.GUIA_A5        // 'guia_A5'       — A5 (solo entrega)
FORMATO_IMPRESION.GUIA_A6        // 'guia_A6'       — A6 (solo entrega)

// Tipo de carga
TIPO_CARGA.PAQUETE   // 'P' — paquete (default)
TIPO_CARGA.DOCUMENTO // 'D' — documento / sobre

// Método de pago del flete
FORMA_PAGO_AEX.CREDITO     // 'C' — crédito (default)
FORMA_PAGO_AEX.PAGO_ORIGEN // 'O' — pago en origen
FORMA_PAGO_AEX.PAGO_DESTINO // 'D' — pago en destino

// Tipo de documento
TIPO_DOCUMENTO.RUC // 'RUC'
TIPO_DOCUMENTO.CIP // 'CIP'
TIPO_DOCUMENTO.PAS // 'PAS'

// Personería
PERSONERIA.FISICA   // 'F'
PERSONERIA.JURIDICA // 'J'

// Tipo de punto de entrega
TIPO_PUNTO_ENTREGA.CAC     // 'CAC'     — centro de atención
TIPO_PUNTO_ENTREGA.ELOCKER // 'ELOCKER' — terminal de autoservicio

Sandbox / entornos

AEX provee un entorno de sandbox distinto al de producción:

// Sandbox
const aex = new AexClient({
  publicKey: '...',
  privateKey: '...',
  sandbox: true,  // → https://sandbox.aex.com.py/api/v1
});

// Producción (default)
const aexProd = new AexClient({
  publicKey: '...',
  privateKey: '...',
  // sandbox: false (default) → https://aex.com.py/api/v1
});

// URL personalizada
const aexCustom = new AexClient({
  publicKey: '...',
  privateKey: '...',
  baseUrl: 'https://www.aex.com.py/api/v1',
  timeoutMs: 30_000,
});

Persistencia / migraciones

El paquete incluye en migrations/ un set de migraciones Knex (compatibles con PostgreSQL, MySQL, MariaDB, SQLite y MSSQL) que crean las siguientes tablas:

| Tabla | Propósito | | --- | --- | | aex_guias | Guías de transporte generadas + estado de seguimiento actual | | aex_webhooks | Eventos de webhook recibidos (con idempotencia para reintentos) | | aex_api_logs | Bitácora de cada request/response HTTP con AEX |

npm install --save-dev knex pg
npx knex migrate:latest --migrations-directory ./node_modules/aex-sdk-typescript/migrations

Ver migrations/README.md para el snippet completo de cómo loggear request/response y manejar idempotencia de webhooks.


Soporte

Datos de contacto de AEX para integraciones:

  • Correo: [email protected]
  • WhatsApp: +595 974 621 965
  • Horario: Lunes a viernes, 08:00 a 17:00 horas

Licencia

MIT