SIFENDE
Guías

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.

FuenteCuándo apareceCómo se ve
API SifendeInmediato, en el HTTP responseStatus 400/401/403/404/422 + Problem Details JSON
SIFEN (rechazo asíncrono)Después del polling, cuando SIFEN procesa el documentoestado: "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

StatusTipoAcción
400validation-errorRevisar errores por campo, corregir y reenviar
400invalid-enum-valueUsar uno de valoresAceptados
400invalid-formatCorregir el campo indicado en campo — mirá tipoEsperado y valorRecibido
401JSON de autenticaciónAPI key inválida/expirada. Rotá la credencial desde el panel
403document-quota-exceededLa emisión no se ejecutó. No reintentes en un loop; esperá a liberar capacidad, al siguiente período o cambiá de plan
403Sin type de operaciónRevisá los permisos y el ambiente de la clave
404*-not-foundRecurso inexistente. Verificar IDs
405method-not-allowedMétodo HTTP incorrecto para ese endpoint
409idempotency-in-progressEsperá Retry-After y repetí exactamente la misma solicitud con la misma clave
409idempotency-outcome-unknownResultado terminal indeterminado. Detenete: no reenvíes ni cambies la clave
409idempotency-key-expiredVenció el replay de 7 días. La clave sigue reservada y no puede reutilizarse
415unsupported-media-typeMandá Content-Type: application/json
422configuracion-incompletaFalta configuración de la cuenta (campo + accion dicen cuál). Reintentar no ayuda
422documento-electronico-generation-errorEl documento no cumple una regla fiscal
422idempotency-key-reusedLa clave ya pertenece a otro tipo de operación. No la recicles
503idempotency-upstream-unknownEsperá Retry-After y repetí exactamente la misma solicitud con la misma clave

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 SIFENSignificadoCómo corregirlo
1107Fecha de inicio de vigencia del timbrado incorrectaCargá en Sifende la fechaInicio exacta registrada en la SET
1302Falta tipoContribuyente del receptor para B2BCompletar tipoContribuyente: "CONTRIBUYENTE"
1303Se informó tipoContribuyenteReceptor cuando el receptor es NO_CONTRIBUYENTESacá tipoContribuyenteReceptor; tipoContribuyente va siempre
1304Falta numeroDocumento (RUC) para receptor contribuyenteCompletar el RUC del receptor
1305Se informó el RUC del receptor cuando el receptor es NO_CONTRIBUYENTEnumeroDocumento no se elimina: es obligatorio y en B2C viaja como CI/pasaporte. Verificá que no estés mandando datos de RUC/contribuyente para un receptor no contribuyente
1306RUC del receptor inexistente en Marangatu (RUC no registrado en SET)Confirmar el RUC con tu cliente
1309DV del RUC del receptor incorrectoVerificar digitoVerificador
2026CDC asociado no existe o no está aprobadoSolo 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: "[1107] Fecha de inicio de vigencia del timbrado incorrecta | [1302] Falta tipoContribuyente"
  const codigo = resultado.mensajeRechazo?.match(/\[(\d+)\]/)?.[1];
  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 la misma solicitud con la misma Idempotency-Key
409 idempotency-in-progress✅Esperá Retry-After y repetí la misma solicitud con la misma clave
503 idempotency-upstream-unknown✅Esperá Retry-After y repetí la misma solicitud 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
5xx en un POST sin clave⚠️Consultá el estado antes de reenviar — ver abajo
403 document-quota-exceeded❌ inmediatoNo 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 de permisos❌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 reintento, diferencias entre las operaciones y reserva de claves— 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 la primera solicitud 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.
  • estado y mensajeRechazo cuando consultes status.
  • type, title, status y detail del Problem Details ante errores.
  • El cuerpo de la solicitud 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

On this page