Manejar Errores
Cómo interpretar y manejar errores de la API de Sifende y rechazos de SIFEN en tu integración.
Esta guía explica cómo distinguir los distintos tipos de error que recibís de Sifende, qué hacer con cada uno y cuándo (no) reintentar.
Dos fuentes de error muy distintas
Mezclarlas en un mismo catch es una de las primeras causas de bugs en integraciones SIFEN.
| Fuente | Cuándo aparece | Cómo se ve |
|---|---|---|
| API Sifende | Inmediato, en el HTTP response | Status 400/401/403/404/422 + Problem Details JSON |
| SIFEN (rechazo asíncrono) | Después del polling, una vez que SIFEN procesa el lote | estado: "RECHAZADO" + mensajeRechazo con código SIFEN |
Errores de la API Sifende (síncronos)
Sigan el formato RFC 9457 Problem Details (excepto autenticación):
{
"type": "https://sifende.com.py/docs/solucion-problemas/validation-error",
"title": "Error de validación",
"status": 400,
"detail": "La solicitud contiene 2 error(es) de validación",
"errores": {
"receptor.numeroDocumento": "Número de documento es obligatorio",
"items[0].precioUnitario": "El precio no puede ser negativo"
}
}Cómo manejarlos en código
const idempotencyKey = await obtenerOCrearClaveIdempotencia(ventaId);
const DOCUMENT_QUOTA_EXCEEDED =
'https://sifende.com.py/docs/solucion-problemas/document-quota-exceeded';
const res = await fetch('https://api.sifende.com.py/api/v1/documento-electronico', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const problem = await res.json();
if (problem.type === DOCUMENT_QUOTA_EXCEEDED) {
console.error('Sin cupo disponible:', {
confirmados: problem.consumoConfirmado,
reservas: problem.reservasActivas,
finPeriodo: problem.finPeriodo,
});
throw new CupoAgotadoError(problem);
}
if (problem.type?.includes('validation-error')) {
for (const [campo, mensaje] of Object.entries(problem.errores ?? {})) {
console.error(`Campo inválido: ${campo} → ${mensaje}`);
}
throw new ValidacionError(problem);
}
if (problem.type?.includes('invalid-enum-value')) {
console.error(`Valor inválido en ${problem.campo}. Aceptados:`, problem.valoresAceptados);
throw new EnumError(problem);
}
throw new ApiError(problem);
}
const { cdc } = await res.json();Tabla rápida de tipos
| Status | Tipo | Acción |
|---|---|---|
| 400 | validation-error | Revisar errores por campo, corregir y reenviar |
| 400 | invalid-enum-value | Usar uno de valoresAceptados |
| 400 | invalid-format | Corregir el campo indicado en campo — mirá tipoEsperado y valorRecibido |
| 401 | (texto plano) | API key inválida/expirada. Rotá la credencial desde el panel |
| 403 | document-quota-exceeded | La emisión no se ejecutó. No reintentes en un loop; esperá a liberar capacidad, al siguiente período o cambiá de plan |
| 403 | access-denied | El usuario/key no tiene acceso a ese contribuyente |
| 404 | *-not-found | Recurso inexistente. Verificar IDs |
| 405 | method-not-allowed | Método HTTP incorrecto para ese endpoint |
| 409 | duplicate-* | Ya existe. Usar el existente o cambiar identificador |
| 409 | idempotency-in-progress | Esperá Retry-After y repetí exactamente el mismo request con la misma clave |
| 409 | idempotency-outcome-unknown | Resultado terminal indeterminado. Detenete: no reenvíes ni cambies la clave |
| 409 | idempotency-key-expired | Venció el replay de 7 días. La clave sigue reservada y no puede reutilizarse |
| 415 | unsupported-media-type | Mandá Content-Type: application/json |
| 422 | configuracion-incompleta | Falta configuración de la cuenta (campo + accion dicen cuál). Reintentar no ayuda |
| 422 | documento-electronico-generation-error | Error de compliance SIFEN al generar el XML |
| 422 | idempotency-key-reused | La clave ya pertenece a otro payload u otra operación. No la recicles |
| 503 | idempotency-upstream-unknown | Esperá Retry-After y repetí exactamente el mismo request con la misma clave |
| 503 | idempotency-unavailable / idempotency-fingerprint-unsupported | La operación no se ejecutó. Conservá la clave y reintentá más tarde |
Catálogo completo en Errores.
Rechazos SIFEN (asíncronos)
Después de emitir, el documento queda en PENDIENTE o EN_LOTE. Una vez procesado, SIFEN devuelve un código con la respuesta. Los más frecuentes en producción:
| Código SIFEN | Significado | Cómo corregirlo |
|---|---|---|
| 1108 | Fecha fin de vigencia del timbrado incorrecta (timbrado vencido o mal configurado) | Renová o corregí la fechaFin del timbrado en SET y actualizalo en Sifende |
| 1302 | Falta tipoContribuyente del receptor para B2B | Completar tipoContribuyente: "CONTRIBUYENTE" |
| 1303 | Se informó tipoContribuyenteReceptor (D207) cuando el receptor es NO_CONTRIBUYENTE | Sacá tipoContribuyenteReceptor; tipoContribuyente va siempre |
| 1304 | Falta numeroDocumento (RUC) para receptor contribuyente | Completar el RUC del receptor |
| 1305 | Se informó el RUC del receptor (D206) cuando el receptor es NO_CONTRIBUYENTE | numeroDocumento no se elimina: es obligatorio y en B2C viaja como CI/pasaporte (D210). Verificá que no estés mandando datos de RUC/contribuyente para un receptor no contribuyente |
| 1306 | RUC del receptor inexistente en Marangatu (RUC no registrado en SET) | Confirmar el RUC con tu cliente |
| 1309 | DV del RUC del receptor incorrecto | Verificar digitoVerificador |
| 2026 | CDC asociado no existe o no está aprobado | Solo emitir NCE/NDE sobre FE en estado APROBADO o APROBADO_OBSERVACION |
Tabla completa: Rechazos SIFEN.
Cómo leer el rechazo
El campo mensajeRechazo del status response contiene el código y la descripción:
const resultado = await esperarResultadoSIFEN(cdc);
if (resultado.estado === 'RECHAZADO') {
// Ej: "1108 - Timbrado vencido"
const codigo = resultado.mensajeRechazo?.split(' ')[0];
log.warn({ cdc, codigo, motivo: resultado.mensajeRechazo }, 'DE rechazado por SIFEN');
}Cuándo reintentar (y cuándo no)
| Situación | ¿Reintentar? | Cómo |
|---|---|---|
5xx en un GET (consulta) | ✅ | Backoff exponencial (1s, 2s, 4s…), máximo 3 intentos |
Timeout, reset o conexión cortada antes de recibir una respuesta de un POST con clave | ✅ | Repetí exactamente el mismo request con la misma Idempotency-Key |
409 idempotency-in-progress | ✅ | Esperá Retry-After y repetí el mismo request con la misma clave |
503 idempotency-upstream-unknown | ✅ | Esperá Retry-After y repetí el mismo request con la misma clave |
409 idempotency-outcome-unknown | ❌ | Detenete. El resultado es indeterminado y terminal; no reenvíes ni cambies de clave |
409 idempotency-key-expired | ❌ | El replay venció y la clave sigue reservada; no la recicles |
503 idempotency-unavailable / idempotency-fingerprint-unsupported | ✅ | No hubo efecto. Conservá la clave y reintentá cuando el contrato esté disponible |
5xx en un POST sin clave | ⚠️ | Consultá el estado antes de reenviar — ver abajo |
403 document-quota-exceeded | ❌ inmediato | No hubo efecto. Reintentá solo cuando un rechazo libere capacidad, comience otro período o cambie el plan; las reservas no son consumo cobrado |
401 / 403 access-denied | ❌ | Arreglá credenciales o permisos, no reintentes con la misma configuración |
400 validation-error | ❌ | Corregí los datos antes de reenviar |
422 configuracion-incompleta | ❌ | Completá lo que indica campo en el panel; reintentar no cambia nada |
404 documento-electronico-not-found | ❌ | El CDC no existe; no aparecerá reintentando |
| Rechazo SIFEN | ⚠️ | Emití un DE nuevo con los datos corregidos. El original queda rechazado para siempre |
Para el contrato completo —creación y persistencia de claves, decisiones de retry, diferencias entre las tres operaciones y reserva permanente— ver Idempotencia y Reintentos Seguros.
Un 5xx en un POST sin Idempotency-Key no significa que la operación no ocurrió. Si reenviás a ciegas y el primer request sí produjo un efecto, podés duplicar una emisión o un evento tributario.
El problem detail indica este riesgo con resultadoIndeterminado:
{
"status": 500,
"type": "https://sifende.com.py/docs/solucion-problemas/internal-error",
"detail": "Ocurrió un error inesperado y el resultado de la operación es indeterminado...",
"resultadoIndeterminado": true,
"accion": "Consultá el estado del recurso antes de reenviar...",
"traceId": "f2f0333f78bfb6a4184cb5a5374685a5"
}Con resultadoIndeterminado: true, investigá el resultado antes de reenviar. Con false en una consulta GET, la operación no tocó datos y el reintento es seguro.
Recomendaciones de logging
En tu integración, logueá siempre:
cdc, para correlacionar con tu sistema.estadoymensajeRechazocuando consultes status.type,title,statusydetaildel Problem Details ante errores.- El request body completo en errores 4xx (excepto la API key); facilita reproducir el problema.
log.error({
cdc,
estado: resultado.estado,
mensajeRechazo: resultado.mensajeRechazo,
}, 'DE rechazado');Próximos pasos
- Para reintentar después de un rechazo, ver Reintentar Rechazados.
- Para ver el catálogo de códigos SIFEN, ver Rechazos SIFEN.
- Para problemas con certificados y firmado, ver Certificado Digital.