Node.js TypeScript
Typed integration for Node.js on top of axios. Includes the HTTP client, the request/response types, and a webhook receiver with HMAC verification over the raw body.
Setup
npm install axios uuid
npm install -D @types/uuid typescript ts-nodeComplete client
The LxsClient class exposes the methods for issuing, querying, downloading PDF/XML, resending email, voiding, and the next consecutive number. Each issuance receives a unique Idempotency-Key (or the one provided to retry).
// 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;
/** Id of a currently valid customer exemption (GET /clientes/{identificacion} → exoneraciones).
Do NOT use montoExoneracion: that field exists in the contract but the server ignores it. */
idExoneracion?: number;
}
export interface FacturaRequest {
tipo: TipoComprobante;
idUsuario: number;
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.pos.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;
}
}
// Usage:
const lxs = new LxsClient(process.env.LXS_API_KEY!);
const factura = await lxs.emitir({
tipo: 'FacturaElectronica',
idUsuario: 7,
cliente: { idCliente: 42 },
condicionVenta: '01',
medioPago: '01',
moneda: 'CRC',
lineas: [{ idProducto: 100, cantidad: 2 }]
});
console.log('Key:', factura.clave);Webhook receiver (Express)
You must use express.raw({ type: 'application/json' }) to preserve the raw body: if Express is allowed to parse it, bytes are lost (spacing, key order) and the HMAC signature will not match. After validation, processing must be queued with setImmediate or whatever queue the integrator uses, so that the response is issued in under 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('Event received:', evento.evento, evento.clave, evento.ind_estado);
// Process asynchronously
setImmediate(() => procesarEvento(evento));
res.status(200).end();
}
);
async function procesarEvento(evento: any) {
// Dedup by nonce
// Update local DB
// Notify other systems
}
app.listen(3000, () => console.log('Webhook server en :3000'));