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.
Maintainers
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
fetchnativo)
Tabla de contenidos
- Instalación
- Configuración
- Uso rápido
- Casos de uso
- 1. Cotizar un envío
- 2. Solicitar y confirmar un servicio (crear guía)
- 3. Imprimir la guía en PDF
- 4. Seguimiento de una guía
- 5. Cancelar un servicio
- 6. Modificar indicaciones de pickup/entrega
- 7. Listar ciudades disponibles
- 8. Webhook de eventos de seguimiento
- 9. Inventario / existencia de productos
- 10. Reintentos y manejo de errores
- Helpers de bajo nivel
- Constantes
- Sandbox / entornos
- Persistencia / migraciones
- Soporte
Instalación
npm install aex-sdk-typescript
# o
pnpm add aex-sdk-typescript
# o
yarn add aex-sdk-typescriptConfiguració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:
solicitarServicio→ devuelveid_solicitud+ lista de condiciones (servicios disponibles con precios y puntos de entrega).confirmarServicio→ confirma la condición elegida con los datos de pickup/entrega y devuelve elnumero_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_entregaenpickupy/oentrega(obtenido depuntosEntrega). 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 autoservicioSandbox / 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/migrationsVer 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
