@quo-digital/multipac-sdk
v0.2.0
Published
SDK oficial de Node/TypeScript para consumir la API CFDI de Multipac.
Maintainers
Readme
@quo-digital/multipac-sdk
SDK oficial de Node/TypeScript para consumir la API CFDI de Multipac (/api/v1/cfdi/*).
Maneja autenticación (obtención y renovación de token de forma transparente), reintentos ante fallos transitorios del PAC o del rate limit, y validación/tipado en runtime de cada respuesta — para que no tengas que hablar HTTP a mano.
Instalación
npm install @quo-digital/multipac-sdkRequiere Node.js 18 o superior (usa fetch nativo, sin dependencias de cliente HTTP).
Uso rápido
client.timbrar() recibe el comprobante como JSON estructurado — Multipac construye,
sella y timbra el XML por ti; nunca le mandas un XML ya armado.
import { MultipacClient } from '@quo-digital/multipac-sdk';
const client = new MultipacClient({
baseUrl: 'https://sandbox-multipac.quo.solutions',
clientKey: process.env.MULTIPAC_CLIENT_KEY!,
clientSecret: process.env.MULTIPAC_CLIENT_SECRET!,
});
const { uuidCfdi, xmlTimbrado } = await client.timbrar({
comprobante: {
tipoDeComprobante: 'I',
subTotal: 100,
moneda: 'MXN',
total: 100,
lugarExpedicion: '64000',
emisor: { rfc: 'EKU9003173C9' },
receptor: {
rfc: 'URE180429TM6',
nombre: 'UNIVERSIDAD ROBOTICA ESPANOLA',
domicilioFiscalReceptor: '65000',
regimenFiscalReceptor: '601',
usoCFDI: 'G03',
},
conceptos: [
{
claveProdServ: '01010101',
cantidad: 1,
claveUnidad: 'H87',
descripcion: 'Producto de prueba',
valorUnitario: 100,
importe: 100,
objetoImp: '01',
},
],
},
});El cliente obtiene y renueva el token internamente en la primera llamada — no necesitas manejarlo.
Multipac valida la aritmética (importe = cantidad × valorUnitario, subTotal/total,
impuestos agregados, totales de complementos, …) pero nunca la calcula para insertarla: si
algo no cuadra, rechaza la petición con ConceptoInconsistenteError (codigo:
CONCEPTO_INCONSISTENTE) en vez de adivinar un valor. El SDK valida localmente la forma
(campos requeridos, tipos, formato de RFC/CP) antes de mandar la petición — ver Manejo de
errores — pero esa aritmética es responsabilidad de Multipac, no del SDK.
Complementos
comprobante.complementos agrega un complemento soportado — hoy, solo Nómina (nomina12):
const { uuidCfdi } = await client.timbrar({
comprobante: {
tipoDeComprobante: 'N', // Nómina exige "N", y "N" exige un complemento nomina12
subTotal: 5000,
descuento: 250,
moneda: 'MXN',
metodoPago: 'PUE', // requerido para N — formaPago, en cambio, está prohibido
total: 4750,
lugarExpedicion: '72530',
emisor: { rfc: 'EWE1709045U0' },
receptor: {
rfc: 'AAA010101AAA',
nombre: 'Receptor de nómina de prueba',
domicilioFiscalReceptor: '72530',
regimenFiscalReceptor: '605',
usoCFDI: 'CN01',
},
conceptos: [
{
claveProdServ: '84111505',
cantidad: 1,
claveUnidad: 'ACT',
descripcion: 'Pago de nómina',
valorUnitario: 5000,
importe: 5000,
descuento: 250,
objetoImp: '01',
},
],
},
complementos: [
{
tipo: 'nomina12',
data: {
tipoNomina: 'O',
fechaPago: '2026-08-31',
fechaInicialPago: '2026-08-16',
fechaFinalPago: '2026-08-31',
numDiasPagados: 15,
emisor: { registroPatronal: 'Y4646861106' },
receptor: {
curp: 'XEXX010101HNEXXXA4',
tipoRegimen: '02',
numEmpleado: '001',
tipoContrato: '01',
periodicidadPago: '04',
claveEntFed: 'CMX',
},
percepciones: [
{
tipoPercepcion: '001',
clave: '001',
concepto: 'Sueldo',
importeGravado: 5000,
importeExento: 0,
},
],
totalSueldos: 5000,
totalGravado: 5000,
totalExento: 0,
},
},
],
});El tipo Complemento (exportado por el SDK) es un discriminated union sobre tipo — cuando
Multipac soporte un complemento nuevo, el SDK le agrega su propio miembro sin tocar el resto de
los tipos de timbrar().
API
| Método | Descripción |
| ---------------------------------------------- | -------------------------------------------------------------------------- |
| client.timbrar(input) | Timbra un CFDI 4.0 |
| client.cancelar(uuidCfdi, input) | Cancela un CFDI timbrado |
| client.consultarEstado(uuidCfdi, params) | Consulta el estado de un CFDI ante el PAC |
| client.recuperarXml(uuidCfdi, params?) | Recupera el XML timbrado |
| client.consultarOperacion(operacionId) | Consulta el estado de una operación (útil en modo asíncrono) |
| esperarOperacion(client, operacionId, opts?) | Hace polling de una operación asíncrona hasta que llegue a un estado final |
Manejo de errores
Cada operación puede lanzar una subclase de MultipacError. Los errores de negocio del catálogo
de Multipac (ConceptoInconsistenteError, CsdNoVigenteError, CreditosAgotadosError, etc.)
exponen un codigo estable — ConceptoInconsistenteError (CONCEPTO_INCONSISTENTE) es el más
frecuente al timbrar: sale cuando algo en comprobante/complementos no cuadra aritméticamente:
import {
ConceptoInconsistenteError,
CreditosAgotadosError,
MultipacError,
} from '@quo-digital/multipac-sdk';
try {
await client.timbrar({ comprobante });
} catch (err) {
if (err instanceof ConceptoInconsistenteError) {
// un total/importe declarado no cuadra con el detalle — revisa err.message
} else if (err instanceof CreditosAgotadosError) {
// manejar saldo agotado
} else if (err instanceof MultipacError) {
// cualquier otro error del SDK
}
}Un input que no cumple la forma esperada (falta comprobante, un campo requerido, un RFC mal
formado, …) nunca llega a la red: client.timbrar() lanza MultipacValidationError de inmediato.
Reintentos
429/502 se reintentan automáticamente (con backoff) tanto en GET como en POST, porque son
respuestas del servidor que confirman que la operación no se completó. Un fallo de red puro
(timeout, conexión perdida) solo se reintenta en operaciones de lectura (consultarEstado,
recuperarXml, consultarOperacion). timbrar/cancelar nunca reintentan un fallo de red:
si la respuesta se pierde, no hay forma de saber si el servidor ya procesó la operación, y
reintentar podría timbrar el mismo CFDI dos veces o duplicar una cancelación. Ante un
MultipacNetworkError de timbrar/cancelar, verifica el estado real con
client.consultarOperacion() antes de reintentar manualmente.
Desarrollo
Requiere Node.js 18+ y npm.
git clone [email protected]:excel/multipac/sdk.git
cd sdk
npm installnpm install corre prepare (Husky) y deja activados los hooks de pre-commit (lint-staged) y
pre-push.
Scripts
| Comando | Qué hace |
| ---------------------- | ------------------------------------------------------------------------ |
| npm run dev | Build en modo watch (tsup --watch) |
| npm run build | Build dual ESM/CJS + .d.ts en dist/ |
| npm run typecheck | Chequeo de tipos sin emitir (tsc --noEmit) |
| npm run lint | ESLint con --fix |
| npm run lint:check | ESLint sin modificar archivos (el que corre en CI) |
| npm run format | Prettier con --write |
| npm run format:check | Prettier en modo check (el que corre en CI) |
| npm test | Corre la suite de tests (Vitest) |
| npm run test:watch | Vitest en modo watch |
| npm run test:cov | Suite + cobertura (gate de CI: 80% líneas/branches/funciones/statements) |
Tests
La suite usa msw para mockear la API de Multipac — no necesitas credenciales reales, sandbox, ni
red para correrla. Los mocks viven en src/__mocks__/.
Releases
El versionado y el publish a npm son automáticos vía CI, gatillados por changeset:
- En tu PR, si el cambio debe reflejarse en una nueva versión, corre
npx changeset— te pregunta el tipo de bump (patch/minor/major) y una descripción; esto crea un archivo en.changeset/. Cambios internos que no afectan al consumidor (CI, tests, docs) no necesitan uno. - Al mergear a
main, el jobversionconsume los changesets pendientes, bumpeapackage.json, actualizaCHANGELOG.md, y commitea ese cambio de vuelta amain. - Ese commit dispara un nuevo pipeline: el job
publishdetecta la versión nueva (aún no está en el registro) y la publica. Un push sin changesets pendientes no genera versión nueva — el pipeline queda verde sin publicar nada.
Licencia
MIT
