Skip to content

Errores

Todos los errores del SDK heredan de SmartDocError, y se dividen según de dónde vienen.

SmartDocError
├── APIError                  ← la respondió SmartDoc
│   ├── AuthenticationError   401
│   ├── PermissionError       403
│   ├── NotFoundError         404
│   ├── ValidationError       422, 400, o una validación local
│   ├── ConflictError         409
│   ├── RateLimitError        429
│   └── ServerError           5xx
├── ConfigurationError        ← el SDK no pudo resolver la configuración
├── SignatureError            ← un webhook no validó
└── TimeoutError              ← se agotó waitUntilFinal

Todos los APIError traen .code, .apiMessage, .statusCode y .raw. .message es lo que se ve al loguear e incluye el código adelante: [NOT_FOUND] El recurso no existe.

Se distinguen con instanceof:

ts
import { RateLimitError, ValidationError } from '@araitek/smartdocjs';

try {
  await sd.invoices.create({ recipient, items });
} catch (error) {
  if (error instanceof ValidationError) {
    // datos mal armados; no sirve reintentar
  } else if (error instanceof RateLimitError) {
    // transitorio; el SDK ya reintentó
  } else {
    throw error;
  }
}

ValidationError

Puede venir de la API o detectarse localmente, antes de salir a la red.

ts
try {
  await sd.invoices.create({ recipient, items });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.code);          // 'VALIDATION_ERROR', o undefined si es local
    console.log(error.fieldErrors);   // { amount: 'Must be a valid amount.' }
  }
}

Las validaciones locales fallan sin gastar una llamada y dicen qué corregir:

js
await sd.invoices.create({ recipient, items, saleType: 'Contado' });
// ValidationError: "Contado" no es un valor válido de SaleType.
//                  ¿Quisiste decir "cash"? Valores válidos: "cash", "credit".

await sd.invoices.create({ recipient, items, currency: 'USD' });
// ValidationError: Con moneda USD hace falta exchangeRate.

new Item('X', { quantity: 1, unitAmount: 100, iva: Iva.MIXED_TEN });
// ValidationError: El tipo de IVA "mixed_10_percent" es mixto y necesita ivaBase.

En TypeScript, el primero ni siquiera compila

Los catálogos son uniones de literales, así que un valor escrito a mano lo rechaza el compilador. La validación en tiempo de ejecución es la que ataja los valores que vienen de afuera —un formulario, un CSV, otra API— y que el compilador no puede ver.

Un ValidationError no se arregla reintentando: hay que corregir los datos.

PermissionError

ts
try {
  await sd.invoices.list();
} catch (error) {
  if (error instanceof PermissionError) {
    console.log(error.missingPermission);   // 'read_invoices'
  }
}

Los permisos no son jerárquicos

write_invoices no habilita nada que pida read_invoices. Si una clave crea facturas y después las consulta, necesita los dos marcados.

La clave pertenece al contribuyente, no al usuario: los permisos de quien la creó no influyen.

Acciones no permitidas

Una acción que no aplica al estado actual del documento llega como ValidationError con código ACTION_NOT_ALLOWED:

ts
try {
  await sd.invoices.cancel(factura.id, 'Error en el monto');
} catch (error) {
  if (error instanceof ValidationError && error.code === 'ACTION_NOT_ALLOWED') {
    // Por ejemplo, la factura todavía no está aprobada.
  }
}

RateLimitError

El SDK regula las solicitudes por debajo del límite y reintenta con backoff exponencial. Si aun así llega este error, es que se agotaron los reintentos.

ts
new Client({ maxRetries: 5 });              // más reintentos
new Client({ rateLimitPerSecond: 3 });      // más conservador
new Client({ rateLimitPerSecond: 0 });      // sin regulación

ConfigurationError

El SDK no pudo resolver algo que necesita para emitir:

ConfigurationError: El contribuyente tiene 2 establecimientos y no se indicó
cuál usar. Elegilo con establishment: '001' al construir el cliente, o
establishment: '001' en la llamada. Opciones disponibles: 001, 002.

Las opciones también vienen en error.options, por si querés mostrarlas.

Nunca elige por su cuenta: el código elegido queda impreso en el documento y en su CDC, y equivocarlo obliga a anular y reemitir.

TimeoutError

ts
import { TimeoutError } from '@araitek/smartdocjs';

try {
  await sd.invoices.waitUntilFinal(factura.id);
} catch (error) {
  if (error instanceof TimeoutError) {
    console.log(error.lastStatus);    // 'uploaded_to_set'
  }
}

No significa que el documento haya fallado: sigue su curso. Lo que se agotó es la espera.

Documentos que terminan en error

Un documento puede emitirse bien y aun así terminar rechazado. Eso no lanza un error —la llamada fue correcta—, queda en el estado del documento:

ts
if (factura.isError) {
  console.log(factura.errorCode);      // '0160'
  console.log(factura.errorMessage);
}

Ramificá por isError

errorCode puede venir vacío aunque el documento haya fallado, según de dónde venga el rechazo; errorMessage trae el detalle en todos los casos. La pregunta que corresponde es if (factura.isError), que mira el estado.

Cómo se corrigen

Editando el documento. SmartDoc lo vuelve a procesar solo: no hay que reenviarlo ni emitir uno nuevo.

ts
if (factura.isError) {
  await sd.invoices.update(factura.id, { recipient, items });
  const corregida = await sd.invoices.waitUntilFinal(factura.id);
}

update reemplaza el documento entero, así que hay que mandar todos los campos, no solo el que se corrige.

Se puede editar en cualquier estado menos uploaded_to_set y approved_by_set. Ahí la API responde EDIT_NOT_ALLOWED, y lo que corresponde es anular y reemitir.

Idempotencia

La API exige una clave de idempotencia al crear documentos. El SDK la pone: cada create() va con una clave nueva, así que no hay que hacer nada.

ts
const factura = await sd.invoices.create({ recipient, items });

Para controlarla vos, pasala con idempotencyKey:

ts
const factura = await sd.invoices.create({
  recipient,
  items,
  idempotencyKey: `venta-${venta.id}`,
});

Dos llamadas con la misma clave devuelven el mismo documento, sin emitirlo dos veces. La respuesta queda guardada 24 horas, incluidos los errores: si reintentás con la misma clave después de corregir el cuerpo, te devuelve el error viejo.

Reintentos

El SDK reintenta ante 429, 5xx y fallas de red, con backoff exponencial y jitter.

Solicitud¿Reintenta ante 5xx?¿Ante 429?
GET, PUT, DELETE
POST de creaciónSí, lleva clave de idempotencia
POST de acción (cancel, …)No

Una acción no lleva clave de idempotencia, así que ante un 5xx no se puede saber si el servidor alcanzó a ejecutarla y reintentar podría repetirla. Un 429, en cambio, se rechaza antes de ejecutar nada, y por eso siempre se reintenta.

Para ajustar las esperas:

ts
import { Client, RetryPolicy } from '@araitek/smartdocjs';

new Client({
  retry: new RetryPolicy({ maxRetries: 5, backoffBase: 1, backoffMax: 30 }),
});

Ver qué manda el SDK

El transporte acepta un fetch propio, así que se puede envolver el que trae Node para loguear cada solicitud:

ts
const sd = new Client({
  fetch: async (url, init) => {
    console.log(init.method, url, init.body);
    const respuesta = await fetch(url, init);
    console.log('→', respuesta.status);
    return respuesta;
  },
});

Sirve también para salir por un proxy o agregar métricas.