Node.js TypeScript
Integración tipada para Node.js sobre axios. Incluye el cliente HTTP, los tipos de request/response y un receptor de webhook con verificación HMAC sobre el body crudo.
Setup
npm install axios uuid
npm install -D @types/uuid typescript ts-nodeCliente completo
La clase LxsClient expone los métodos de emisión, consulta, descarga de PDF/XML, reenvío de correo, anulación y siguiente consecutivo. Cada emisión recibe un Idempotency-Key único (o el que vos le pases para reintentar).
// src/lxs-client.ts
import axios, { AxiosInstance } from 'axios';
import { randomUUID } from 'crypto';
export type TipoComprobante = 'FacturaElectronica' | 'TiqueteElectronico' | 'NotaCredito' | 'NotaDebito';
export interface Linea {
idProducto: number;
cantidad: number;
precioUnitario?: number;
montoDescuento?: number;
naturalezaDescuento?: string;
montoExoneracion?: number;
}
export interface FacturaRequest {
tipo: TipoComprobante;
cliente?: { idCliente: number } | null;
condicionVenta: string;
plazoCredito?: number;
medioPago: string;
moneda: string;
tipoCambio?: number;
codigoActividad?: string;
comentario?: string;
lineas: Linea[];
referencias?: any[];
}
export interface FacturaResponse {
clave: string;
numeroConsecutivo: string;
tipoDocumento: '01' | '02' | '03' | '04';
estadoHacienda: string;
totalComprobante: number;
moneda: string;
fechaEmision: string;
}
export class LxsClient {
private http: AxiosInstance;
constructor(apiKey: string, baseURL = 'https://api.lxsyscr.com') {
this.http = axios.create({
baseURL,
headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' },
timeout: 30000
});
}
async emitir(data: FacturaRequest, idempotencyKey?: string): Promise<FacturaResponse> {
const key = idempotencyKey || randomUUID();
const { data: response } = await this.http.post<FacturaResponse>(
'/api/v1/facturas', data,
{ headers: { 'Idempotency-Key': key } }
);
return response;
}
async consultar(clave: string): Promise<FacturaResponse> {
const { data } = await this.http.get<FacturaResponse>(`/api/v1/facturas/${clave}`);
return data;
}
async descargarPdf(clave: string): Promise<Buffer> {
const { data } = await this.http.get(`/api/v1/facturas/${clave}/pdf`, { responseType: 'arraybuffer' });
return Buffer.from(data);
}
async descargarXml(clave: string): Promise<string> {
const { data } = await this.http.get(`/api/v1/facturas/${clave}/xml`);
return data;
}
async reenviarEmail(clave: string): Promise<{ mensaje: string }> {
const { data } = await this.http.post(`/api/v1/facturas/${clave}/email`);
return data;
}
async anular(clave: string): Promise<void> {
await this.http.delete(`/api/v1/facturas/${clave}`);
}
async proximoConsecutivo(tipo: '01' | '02' | '03' | '04'): Promise<number> {
const { data } = await this.http.get(`/api/v1/consecutivos/proximo`, { params: { tipo } });
return data.proximo;
}
}
// Uso:
const lxs = new LxsClient(process.env.LXS_API_KEY!);
const factura = await lxs.emitir({
tipo: 'FacturaElectronica',
cliente: { idCliente: 42 },
condicionVenta: '01',
medioPago: '01',
moneda: 'CRC',
lineas: [{ idProducto: 100, cantidad: 2 }]
});
console.log('Clave:', factura.clave);Receptor de webhook (Express)
Usá express.raw({ type: 'application/json' }) para conservar el body crudo: si dejás que Express lo parsee, perdés bytes (espacios, orden de claves) y la firma HMAC no va a coincidir. Después de validar, encolá el procesamiento con setImmediate o tu cola favorita para responder en menos de 10 s.
// src/webhook-server.ts
import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';
const app = express();
const WEBHOOK_SECRET = process.env.LXS_WEBHOOK_SECRET!;
app.post('/webhooks/lxs',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = (req.headers['x-lxs-signature'] || '').toString().replace(/^sha256=/, '');
const expected = createHmac('sha256', WEBHOOK_SECRET).update(req.body).digest('hex');
if (sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
const evento = JSON.parse(req.body.toString('utf8'));
console.log('Evento recibido:', evento.evento, evento.clave, evento.ind_estado);
// Procesa de forma asíncrona
setImmediate(() => procesarEvento(evento));
res.status(200).end();
}
);
async function procesarEvento(evento: any) {
// Dedup por nonce
// Actualizar BD local
// Notificar a otros sistemas
}
app.listen(3000, () => console.log('Webhook server en :3000'));