Skip to content

Emitir CFEs

  • Cfe describe el comprobante: tipo, receptor, detalle, referencias, etc. Es un record inmutable: se arma con un inicializador y se modifica con with.
  • client.EmitirAsync(cfe) lo valida, le asigna el próximo número del CAE vigente, arma el XML, lo firma y lo registra en el almacenamiento (del que sale el reporte diario). No lo envía.
  • CfeEmitido es el resultado: Numero, Serie, Cae, Totales, Xml firmado, CodigoSeguridad, UrlQr y el estado ante la DGI (EstadoDgi, AceptadoPorDgi).

Todos los tipos se crean igual: new Cfe(TipoCfe.X) { … }. Ver Tipos de CFE.


Para ventas al consumidor final (sin identificar al receptor):

var eticket = await client.EmitirAsync(new Cfe(TipoCfe.ETicket)
{
Detalle =
[
new LineaDetalle { NombreItem = "Servicio de consultoría", Cantidad = 2, PrecioUnitario = 3500m, IndFactIva = TipoIva.Basico }, // 22 %
new LineaDetalle { NombreItem = "Viáticos", Cantidad = 1, PrecioUnitario = 1000m, IndFactIva = TipoIva.Exento },
],
});

FechaEmision es opcional: si se omite, es la fecha de Uruguay al emitir (no la del servidor).


Requiere identificar al receptor con su RUT:

var efactura = await client.EmitirAsync(new Cfe(TipoCfe.EFactura)
{
Receptor = new Receptor
{
Documento = "214844360018",
RazonSocial = "Empresa Compradora S.A.",
Direccion = "Bulevar España 2345",
Ciudad = "Montevideo",
},
Detalle = [new LineaDetalle { NombreItem = "Licencia de software anual", Cantidad = 1, PrecioUnitario = 50000m }],
});

Deben referenciar el CFE original, con su monto y moneda (obligatorios desde el formato CFE v25.01):

var notaCredito = await client.EmitirAsync(new Cfe(TipoCfe.NotaCreditoETicket)
{
Referencias =
[
new RefCfe
{
TipoCfe = TipoCfe.ETicket,
Serie = "A",
NroCfe = 42, // número del CFE original
FechaCfe = new DateOnly(2025, 6, 15),
MontoCfeRef = 1220m, // total del CFE original
MonedaCfeRef = Moneda.PesoUruguayo, // moneda del CFE original
Razon = "Devolución parcial de mercadería",
},
],
Detalle = [new LineaDetalle { NombreItem = "Devolución - Producto X", Cantidad = 2, PrecioUnitario = 500m }],
});

EmitirAsync valida antes de numerar: un CFE inválido lanza CfeValidationException (con la lista en Errores) sin consumir números. La validación aplica las restricciones de los XSD de la DGI (largos, montos, fechas, series, caracteres de control) y las reglas del formato, así que lo que la pasa produce un XML válido. Para revisar sin emitir (por ejemplo, en el validador de un endpoint):

IReadOnlyList<ErrorValidacion> errores = client.Validar(cfe);
foreach (var error in errores)
Console.WriteLine($"{error.Campo}: {error.Mensaje}"); // p. ej. "Detalle[0].PrecioUnitario: No puede ser negativo."

Campo es la ruta de la propiedad dentro del Cfe, para mostrar el error junto al dato en un formulario.


Si el comprobante corresponde a algo de tu sistema (un pedido, un pago), informá su id en NroInterno. Un reintento —por ejemplo, porque el request expiró aunque la emisión se completó— no factura dos veces: EmitirAsync lanza CfeDuplicadoException con el CFE ya emitido.

CfeEmitido emitido;
try
{
emitido = await client.EmitirAsync(cfe with { NroInterno = pedido.Id });
}
catch (CfeDuplicadoException ex)
{
emitido = ex.Existente; // ya estaba emitido
}

La regla es por tipo de comprobante (una nota de crédito puede llevar el mismo NroInterno que su factura) y no cuenta los CFE que la DGI rechazó: para corregir uno rechazado se emite otro.


Valor Tasa Uso común
TipoIva.Basico 22% Servicios y bienes generales (default)
TipoIva.Minimo 10% Alimentos de primera necesidad, medicamentos
TipoIva.Exento 0% Bienes y servicios exonerados
TipoIva.Suspendido — IVA en suspenso (se informa en MntIVaenSusp)

Los precios son sin IVA: el SDK calcula el impuesto y los totales con la misma precisión que el XML.


// 1) Sobre: la DGI responde AS (recibido) o BS/BA (rechazado)
var acuse = await client.EnviarAsync(eticket); // o EnviarAsync([cfe1, cfe2, …]) con hasta 250
if (!acuse.Recibido)
{
Console.WriteLine($"❌ Sobre rechazado [{acuse.Estado}]: {acuse.Mensaje}");
return;
}
// 2) Resultado de cada comprobante: Aceptado (AE), Rechazado (BE), Observado (CE)
var estado = await client.ConsultarEnvioAsync(acuse);
foreach (var cfe in acuse.Comprobantes)
Console.WriteLine($"{cfe}: {cfe.EstadoDgi} {cfe.MensajeDgi}");

Tras un reinicio (cuando ya no tenés el acuse), client.ConsultarCfeAsync(emitido) consulta un comprobante suelto. Las consultas actualizan el estado en el almacenamiento.

Un CFE que la DGI ya recibió o procesó no se reenvía: EnviarAsync lanza InvalidOperationException, porque la DGI lo trataría como duplicado (y el estado aceptado quedaría tapado por el rechazo del duplicado). Si el sobre fue rechazado (BS), sí se reenvía el mismo CfeEmitido.


emitido.Xml es el CFE firmado tal como se envía. La DGI exige conservarlo durante el plazo legal:

await File.WriteAllTextAsync($"cfe/{emitido.Tipo}_{emitido.Serie}{emitido.Numero:D7}.xml", emitido.Xml);

Con UruFactura.EntityFrameworkCore ya queda guardado en la tabla UruFacturaCfes.


try
{
var emitido = await client.EmitirAsync(cfe);
var acuse = await client.EnviarAsync(emitido);
}
catch (CfeValidationException ex) // el CFE no es válido (no se consumió número)
{
Console.Error.WriteLine(string.Join(Environment.NewLine, ex.Errores));
}
catch (CaeException ex) // no hay CAE vigente con números para el tipo
{
Console.Error.WriteLine(ex.Message);
}
catch (DgiCommunicationException ex) // red, timeout, SOAP Fault o HTTP sin acuse
{
Console.Error.WriteLine($"Error de comunicación con la DGI [{ex.CodigoRespuesta}]: {ex.Message}");
}

Ver Códigos de respuesta DGI para la lista completa de estados.