Emitir CFEs
El modelo: Cfe → CfeEmitido
Section titled “El modelo: Cfe → CfeEmitido”Cfedescribe el comprobante: tipo, receptor, detalle, referencias, etc. Es un record inmutable: se arma con un inicializador y se modifica conwith.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.CfeEmitidoes el resultado:Numero,Serie,Cae,Totales,Xmlfirmado,CodigoSeguridad,UrlQry el estado ante la DGI (EstadoDgi,AceptadoPorDgi).
Todos los tipos se crean igual: new Cfe(TipoCfe.X) { … }. Ver Tipos de CFE.
e-Ticket (101)
Section titled “e-Ticket (101)”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).
e-Factura (111)
Section titled “e-Factura (111)”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 }],});Notas de Crédito y Débito
Section titled “Notas de Crédito y Débito”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 }],});Validar antes de emitir
Section titled “Validar antes de emitir”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.
Emitir una sola vez
Section titled “Emitir una sola vez”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.
Tipos de IVA
Section titled “Tipos de IVA”| 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.
Enviar a la DGI
Section titled “Enviar a la DGI”// 1) Sobre: la DGI responde AS (recibido) o BS/BA (rechazado)var acuse = await client.EnviarAsync(eticket); // o EnviarAsync([cfe1, cfe2, …]) con hasta 250if (!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.
El XML firmado
Section titled “El XML firmado”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.
Manejo de errores
Section titled “Manejo de errores”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.