Generación de facturas en C# .NET para VeriFactu, TicketBAI y Facturae

This article was translated from English: Does it need improvement?
Translated
View the article in English

La generación de facturas en C# .NET para el régimen fiscal español obliga al desarrollador a manejar simultáneamente cuatro marcos diferentes: VeriFactu (anti-fraude nacional, sanciones de hasta 150 000 €/año para fabricantes de software de facturación bajo el RDL 15/2025), TicketBAI (vigente en Bizkaia, Gipuzkoa y Araba con firma XAdES encadenada), Facturae (formato nacional B2G enviado a través de FACe) y Crea y Crece (mandato B2B de recepción que despliega entre 2027 y 2028). IronPDF cubre la capa de generación visual y la incrustación de QR de la AEAT, la firma PAdES sobre el PDF visualizado, y el embebido de XML Facturae dentro de PDF/A-3 para el patrón híbrido. La firma XAdES propiamente dicha sobre el ticket TicketBAI queda fuera del alcance de IronPDF; el componente intermedio se utiliza dentro de software de facturación certificado, no como sistema certificado en sí mismo.

TL;DR: Guía rápida para el cumplimiento español

Este tutorial se ocupa exclusivamente del régimen ibérico (es-ES). Para casos LATAM equivalentes (CFDI México, AFIP Argentina, DIAN Colombia, DTE Chile) existe documentación paralela.

  • A quién va dirigido: Desarrolladores senior de ISV de software de facturación, equipos internos de empresas obligadas por VeriFactu, integradores que conectan ERP a FACe o a las plataformas TicketBAI forales, y arquitectos que evalúan IronPDF como alternativa a iText (AGPL) bajo la exposición de 150 000 €/año del régimen sancionador VeriFactu.
  • Qué construirá: Un PDF de factura conforme con el QR obligatorio de la AEAT y la leyenda VERI*FACTU, un PDF/A-3 con Facturae XML embebido para envío B2G a FACe, un patrón de firma XAdES integrado en el flujo TicketBAI provincial (Bizkaia, Gipuzkoa, Araba), y una canalización de recepción Crea y Crece que extrae datos del XML embebido sin necesidad de OCR.
  • Dónde se ejecuta: .NET 10, .NET 8 LTS, .NET Framework 4.6.2+ y .NET Standard 2.0. Diseño on-premise compatible con los requisitos LOPDGDD de la AEPD para tratamiento local de documentos electrónicos.
  • Cuándo aplicar este enfoque: Cuando deba emitir facturas conformes con VeriFactu (postergado a 1 ene 2027 para Impuesto de Sociedades / 1 jul 2027 resto), TicketBAI (ya vigente en territorio foral vasco), o recibir Facturae bajo el mandato Crea y Crece. Cuando deba sustituir un componente PDF con licencia AGPL incompatible con la distribución comercial bajo VeriFactu.
  • Por qué importa técnicamente: IronPDF renderiza HTML a PDF con precisión de píxel, admite la incrustación PDF/A-3 que exige el patrón híbrido Facturae+PDF, ofrece firma PAdES con certificados emitidos por la FNMT-RCM, y permite extracción de texto y de adjuntos embebidos para procesar facturas entrantes Facturae sin OCR.

Generar una primera factura PDF con la leyenda obligatoria y el QR de la AEAT:

  1. Instala IronPDF con el Administrador de Paquetes NuGet

    PM > Install-Package IronPdf
  2. Copie y ejecute este fragmento de código.

    var renderer = new IronPdf.ChromePdfRenderer();
    var pdf = renderer.RenderHtmlAsPdf("<h1>Invoice #1001</h1><p>Total: $500.00</p>");
    pdf.SaveAs("invoice.pdf");
  3. Despliegue para probar en su entorno real

    Comienza a usar IronPDF en tu proyecto hoy mismo con una prueba gratuita

    arrow pointer

Una vez adquirida la licencia de IronPDF o iniciada la prueba de 30 días, registre la clave al arrancar el proceso. Esta línea es indispensable: bajo VeriFactu, el software de facturación debe documentar la procedencia y versión de cada componente embebido.

IronPdf.License.LicenseKey = "KEY";
IronPdf.License.LicenseKey = "KEY";
Imports IronPdf

IronPdf.License.LicenseKey = "KEY"
$vbLabelText   $csharpLabel

Comience a usar IronPDF en su proyecto hoy con una prueba gratuita.

Primer Paso:
green arrow pointer
NuGet Instalar con NuGet

PM >  Install-Package IronPdf

Echa un vistazo a IronPDF en NuGet para una instalación rápida. Con más de 10 millones de descargas, está transformando el desarrollo de PDF con C#. También puede descargar el DLL o el instalador de Windows.

Tabla de contenido

NuGet Instalar con NuGet

PM >  Install-Package IronPdf

Echa un vistazo a IronPDF en NuGet para una instalación rápida. Con más de 10 millones de descargas, está transformando el desarrollo de PDF con C#. También puede descargar el DLL o el instalador de Windows.

Marco regulatorio español de la facturación electrónica

España no aplica un único régimen de facturación electrónica, sino cuatro marcos paralelos con calendarios y obligaciones distintas. Antes de escribir una sola línea de código de generación de facturas, conviene situar cada componente: VeriFactu (sistema anti-fraude nacional), TicketBAI (régimen foral vasco), Crea y Crece (mandato B2B nacional) y Facturae sobre FACe (formato B2G de larga trayectoria). Cada marco impone obligaciones diferentes al software de facturación, no solo al obligado tributario. Esta es la peculiaridad ibérica que conviene tener clara desde el principio: el régimen sancionador VeriFactu apunta directamente al fabricante del software, no únicamente al contribuyente.

VeriFactu y la huella encadenada bajo el RDL 15/2025

VeriFactu es la respuesta de la AEAT al fraude en el sector del software de facturación. La obligación de generar registros de facturación encadenados criptográficamente y de remitirlos —de forma voluntaria— a la sede electrónica de la AEAT se inscribe en el Real Decreto 1007/2023, modificado y postergado por el Real Decreto-Ley 15/2025 de 2 de diciembre de 2025. El calendario vigente es:

  • 1 de enero de 2027: obligados al Impuesto de Sociedades.
  • 1 de julio de 2027: resto de obligados tributarios.
  • 29 de julio de 2025 (fecha ya cumplida): los proveedores de software de facturación debían comercializar productos con capacidades VeriFactu, con independencia de cuándo los activen sus clientes.

La obligación material que impacta al desarrollador es triple. Primero, cada registro de facturación generado por el sistema debe contener una huella — un hash encadenado con el registro inmediatamente anterior, replicando la lógica de una cadena de bloques aplicada al log local del software. Cualquier ruptura en la cadena (intento de borrar o reescribir un registro intermedio) es detectable y constituye irregularidad sancionable. Segundo, toda visualización de una factura cuyo sistema esté operando en modo VeriFactu debe incluir un código QR conforme a la especificación AEAT que enlaza con el verificador en sede.agenciatributaria.gob.es, y la leyenda textual mandatoria VERI*FACTU (preservando el asterisco intermedio) o Factura verificable en la sede electrónica de la AEAT. Tercero, el sistema debe mantener un canal de remisión hacia la AEAT cuando opera en modo VeriFactu propiamente dicho (en contraposición al modo no VeriFactu con registro local).

Lo que diferencia a España del resto de regímenes europeos es el régimen sancionador dirigido al fabricante: hasta 150 000 € por año para el productor de software de facturación que comercialice un sistema incapaz de operar conforme a VeriFactu. Esta cifra es excepcional en el panorama mundial; los regímenes equivalentes (CFDI mexicano, AFIP argentino, FatturaPA italiano, ZUGFeRD/E-Rechnung alemán) sancionan al obligado tributario, no a la cadena de suministro del software. Para el equipo de procurement de un ISV ibérico, la lectura es directa: los componentes embebidos en el producto de facturación deben acreditar trazabilidad, licenciamiento comercial inequívoco y compatibilidad con la operación VeriFactu. La distribución bajo AGPL de bibliotecas de PDF de terceros entra en colisión frontal con esta exigencia, como veremos en la sección de positioning.

TicketBAI en Bizkaia, Gipuzkoa y Araba (vigente)

A diferencia de VeriFactu — todavía en periodo de prórroga — TicketBAI ya es obligatorio desde hace varios ejercicios en los tres territorios forales de la Comunidad Autónoma del País Vasco. El sistema no es monolítico: cada Diputación Foral (Bizkaia, Gipuzkoa, Araba) gestiona su propia variante con reglas técnicas específicas, plataforma de recepción independiente y fuentes normativas autónomas:

  • Bizkaia (bizkaia.eus) — opera además el programa de cumplimiento ampliado BATUZ, que integra el flujo TicketBAI con el suministro inmediato de información tributaria.
  • Gipuzkoa (gipuzkoa.eus) — gestiona su propia plataforma y su esquema XSD.
  • Araba (araba.eus) — variante minoritaria en volumen, pero con su propia foral.

Adicionalmente, la Comunidad Foral de Navarra está desplegando NaTicket (navarra.es/hacienda), un sistema emergente que comparte filosofía anti-fraude con TicketBAI sin ser técnicamente compatible. Cualquier solución de facturación con cobertura nacional debe estar diseñada para coexistir con esta fragmentación regional sin colapsar la lógica en un único modelo monolítico.

El elemento técnico distintivo de TicketBAI es la firma XAdES encadenada: cada ticket TicketBAI se firma criptográficamente y la firma incluye, dentro del propio XML, una referencia al hash del ticket inmediatamente anterior. La cadena es por tanto auditable de forma análoga a la huella VeriFactu, pero con la diferencia de que TicketBAI usa firma XML estándar XAdES (XML Advanced Electronic Signatures) en lugar de un hash simple. El certificado utilizado en la firma debe ser cualificado en el sentido eIDAS — habitualmente emitido por la FNMT-RCM (Fábrica Nacional de Moneda y Timbre — Real Casa de la Moneda) o por una autoridad reconocida (IZENPE en territorio vasco, Camerfirma, etc.). Cuando el patrón requiere proteger la clave privada en un dispositivo seguro, se recurre a HSM (Hardware Security Module) certificado.

La firma XAdES sobre el XML TicketBAI propiamente dicha queda fuera del alcance directo de IronPDF: la biblioteca firma documentos PDF (PAdES), no documentos XML (XAdES). Pero el flujo completo TicketBAI requiere generar también un PDF visualización del ticket (la boleta que recibe el cliente final), y ese PDF sí entra dentro del alcance de IronPDF. El patrón habitual es: el ERP genera el XML, lo firma con XAdES usando la cadena foral correspondiente, IronPDF renderiza el PDF visualizable a partir de los mismos datos y opcionalmente firma ese PDF con PAdES como capa adicional de no repudio.

Crea y Crece: mandato B2B de recepción 2027/2028

La Ley 18/2022 de Creación y Crecimiento de Empresas (conocida como Crea y Crece) establece la obligación de emisión y recepción de factura electrónica en las operaciones B2B españolas. El calendario, fijado tras varias actualizaciones reglamentarias, se consolida en:

  • 2027: empresas con facturación anual superior a 8 M €.
  • 2028: resto de empresas y autónomos.

El régimen Crea y Crece es independiente de VeriFactu. Mientras VeriFactu se centra en la integridad del registro interno del software de facturación (anti-fraude desde la perspectiva de la AEAT), Crea y Crece se ocupa del intercambio B2B de la factura entre emisor y receptor. Una factura puede ser conforme con VeriFactu pero no con Crea y Crece (si no se emite en el formato estructurado exigido) y viceversa.

El formato estructurado de referencia es Facturae (el formato nacional español, en su versión 3.2.2 o posteriores), interpretado dentro del marco europeo de la norma EN 16931 mediante el perfil de especificación nacional CIUS-ES (Core Invoice Usage Specification — España). El intercambio puede realizarse a través de plataformas privadas autorizadas (las grandes — EDICOM, Sovos, Seres, Voxel — concentran la cuota de mercado del ISV/proveedor de soluciones de cumplimiento), o bien a través de la plataforma pública de la AEAT cuando ésta complete su desarrollo.

Para el desarrollador, las implicaciones técnicas de Crea y Crece son dos. Primera: la recepción de facturas entrantes (un flujo que muchos ISP de software de facturación habían tratado como secundario) pasa a ser obligatoria. Esto significa procesar XML Facturae embebido en PDF/A-3 (el patrón híbrido), extraer los datos del XML sin recurrir a OCR, validar la firma electrónica, y conciliar contra el sistema interno. Segunda: la emisión debe alinear el contenido del Facturae XML con el contenido del PDF visualizable — cualquier discordancia entre el documento humano y el documento de máquina compromete la validez.

Facturae, FACe y SII: el conjunto nacional

Tres siglas adicionales completan el panorama. Facturae es el formato XML nacional, gestionado de forma conjunta por la AEAT y el Ministerio de Asuntos Económicos. FACe (face.gob.es) es la plataforma pública B2G en la que las administraciones públicas españolas reciben las facturas que les emiten sus proveedores. Cualquier empresa que facture a la Administración General del Estado, autonomías o entidades locales debe ya, por defecto, hacerlo en formato Facturae a través de FACe. SII (Suministro Inmediato de Información, en su acepción española — no confundir con el SII chileno, Servicio de Impuestos Internos) es el régimen de envío casi en tiempo real de los libros registro de IVA, aplicable a contribuyentes de cierto tamaño.

El conjunto VeriFactu + TicketBAI + Crea y Crece + Facturae/FACe + SII define el universo de cumplimiento sobre el que se construye cualquier software de facturación ibérico, y la coexistencia entre estos regímenes es lo que multiplica la complejidad: una factura emitida por un sujeto pasivo del SII a un cliente B2B bajo Crea y Crece, archivada localmente con huella VeriFactu, y enviada en formato Facturae a través de FACe (si el cliente es entidad pública) o por canal privado (si es B2B), atraviesa cinco capas regulatorias simultáneamente. La arquitectura del software debe permitir activar y desactivar capas de forma modular, no escribir cada flujo como un caso especial monolítico.


IronPDF como componente dentro de software de facturación certificado

Antes de entrar en código, una clarificación de positioning que es necesaria precisamente por la naturaleza del régimen sancionador VeriFactu y de la atención de la AEAT al fabricante de software.

Positioning: componente, no sistema certificado

IronPDF es una biblioteca componente que el desarrollador embebe dentro de su propio producto. No es un sistema de facturación certificado en el sentido del RD 1007/2023 ni un software de facturación VeriFactu en el sentido del régimen sancionador. La distinción es importante en dos niveles. En el nivel técnico, las obligaciones VeriFactu (generación de huella, encadenamiento, remisión a la AEAT, gestión del libro de eventos) recaen sobre el sistema integrador en su conjunto, no sobre la biblioteca de renderizado PDF que ese sistema utiliza. En el nivel de procurement y de comunicación, conviene evitar cualquier afirmación que sugiera que IronPDF certifica facturas o que su mera inclusión satisface por sí sola las obligaciones VeriFactu del ISV.

El reparto natural es el siguiente. El ERP o el software de facturación construido por el ISV implementa la lógica VeriFactu (generación del registro encadenado, cálculo de huella, gestión de la remisión, persistencia del libro de eventos). IronPDF se utiliza dentro de ese producto para renderizar la visualización del documento — el PDF que se entrega al cliente o se archiva. La visualización incluye el QR de la AEAT (generado por IronQR a partir del hash del registro y de la URL de la sede de la AEAT), la leyenda textual obligatoria VERI*FACTU, los datos identificativos del emisor y del receptor (incluido el NIF, dirección fiscal, etc.) y, opcionalmente, el XML Facturae embebido en PDF/A-3 si se pretende un envío híbrido bajo Crea y Crece. Sobre ese PDF, IronPDF puede aplicar una firma PAdES con el certificado cualificado del emisor — capa de no repudio paralela a la firma XAdES que el sistema haya aplicado sobre el XML TicketBAI o Facturae correspondiente.

La cuestión iText AGPL bajo la sanción de 150 000 €/año

La librería iText es históricamente la opción más extendida en el sector español del software de facturación .NET, y se distribuye bajo doble licencia: comercial de pago, o AGPLv3 gratuita. La distribución bajo AGPL implica que cualquier software que la incluya — incluso accedido como servicio en red — queda obligado a poner el código fuente derivado a disposición del usuario. En la práctica del ISV español de software de facturación, esta obligación entra en conflicto directo con la propuesta de valor del producto: la lógica fiscal, los conectores con FACe, el motor TicketBAI, el flujo VeriFactu son precisamente la propiedad intelectual diferencial que el ISV vende. Distribuir el producto bajo AGPL para satisfacer la obligación de iText no es viable comercialmente.

A esto se añade la lectura agravada bajo VeriFactu. El régimen sancionador apunta al fabricante del software de facturación. Un componente PDF con licenciamiento incompatible con la distribución comercial del producto introduce una zona gris en la trazabilidad de la cadena de software, precisamente cuando la AEAT está incrementando su atención sobre la composición de los productos certificados. La salida convencional ha sido adquirir la licencia comercial de iText, con la consiguiente carga económica y la fricción operativa derivada del modelo de negocio basado en consumo o asientos.

IronPDF se posiciona en este contexto como biblioteca con licenciamiento comercial royalty-free y previsible, sin componente AGPL en su distribución, sin obligaciones de redistribución de fuentes, y con un modelo de licenciamiento perpetuo que se ajusta al modelo de negocio del software de facturación tradicional. La sustitución de iText por IronPDF en un producto de facturación ibérico no se justifica únicamente por preferencias técnicas; se justifica también por la reducción de exposición regulatoria bajo VeriFactu y por la simplificación del due diligence de licenciamiento que las grandes compras de Administración Pública y de cliente corporativo español requieren.

Nunca se debe utilizar la palabra libre para describir el coste cero de la prueba o de la versión gratuita de IronPDF: en el contexto ibérico, software libre connota open source / FOSS / AGPL — exactamente la confusión competitiva que conviene evitar. Use gratuito, gratuita, prueba gratuita, versión gratuita o gratis para los registros pertinentes.


Generación de facturas conformes con VeriFactu en C#

Con el marco regulatorio interiorizado, pasamos al código. La generación de la factura es la fase donde IronPDF aporta el grueso del valor. El patrón general es: construir una plantilla HTML con CSS donde se inyectan los datos identificativos (emisor, receptor, NIF español, dirección fiscal), las partidas con su base imponible y cuota de IVA al tipo correspondiente, el desglose de totales con la información del régimen (en su caso) y, en visualizaciones VeriFactu, el bloque del QR de la AEAT y la leyenda VERI*FACTU. El motor Chrome embebido en IronPDF convierte el HTML en un PDF con precisión de píxel, lo que permite igualar el resultado al PDF de referencia del cliente final.

Antes de continuar, puede descargar el proyecto de muestra completo desde aquí.

Plantilla HTML de factura ibérica con NIF e IVA al 21 %

Una plantilla HTML base que recoge los campos identificativos ibéricos (NIF emisor, NIF receptor, IVA al tipo correspondiente, número de factura con la serie del ejercicio):

:path=/static-assets/pdf/content-code-examples/tutorials/csharp-invoice-processing/basic-invoice-template.cs
using IronPdf;

// Define the HTML template for a basic invoice
// Uses inline CSS for styling headers, tables, and totals
string invoiceHtml = @"
E html>


le>
body { font-family: Arial, sans-serif; padding: 40px; }
.header { text-align: right; margin-bottom: 40px; }
.company-name { font-size: 24px; font-weight: bold; color: #333; }
.invoice-title { font-size: 32px; margin: 20px 0; }
.bill-to { margin: 20px 0; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th { background-color: #2A95D5; color: white; padding: 10px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #ddd; }
.total { text-align: right; font-size: 20px; font-weight: bold; margin-top: 20px; }
yle>


 class='header'>
<div class='company-name'>Your Company Name</div>
<div>123 Business Street</div>
<div>City, State 12345</div>
v>

 class='invoice-title'>INVOICE</div>

 class='bill-to'>
<strong>Bill To:</strong><br>
Customer Name<br>
456 Customer Avenue<br>
City, State 67890
v>

le>
<tr>
    <th>Description</th>
    <th>Quantity</th>
    <th>Price</th>
    <th>Total</th>
</tr>
<tr>
    <td>Web Development Services</td>
    <td>10 hours</td>
    <td>$100.00</td>
    <td>$1,000.00</td>
</tr>
<tr>
    <td>Consulting</td>
    <td>5 hours</td>
    <td>$150.00</td>
    <td>$750.00</td>
</tr>
ble>

 class='total'>Total: $1,750.00</div>

;

// Initialize the Chrome-based PDF renderer
var renderer = new ChromePdfRenderer();

// Convert the HTML string to a PDF document
var pdf = renderer.RenderHtmlAsPdf(invoiceHtml);

// Save the generated PDF to disk
pdf.SaveAs("basic-invoice.pdf");
Imports IronPdf

' Define the HTML template for a basic invoice
' Uses inline CSS for styling headers, tables, and totals
Dim invoiceHtml As String = "
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 40px; }
.header { text-align: right; margin-bottom: 40px; }
.company-name { font-size: 24px; font-weight: bold; color: #333; }
.invoice-title { font-size: 32px; margin: 20px 0; }
.bill-to { margin: 20px 0; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th { background-color: #2A95D5; color: white; padding: 10px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #ddd; }
.total { text-align: right; font-size: 20px; font-weight: bold; margin-top: 20px; }
</style>
</head>
<body>
<div class='header'>
    <div class='company-name'>Your Company Name</div>
    <div>123 Business Street</div>
    <div>City, State 12345</div>
</div>
<div class='invoice-title'>INVOICE</div>
<div class='bill-to'>
    <strong>Bill To:</strong><br>
    Customer Name<br>
    456 Customer Avenue<br>
    City, State 67890
</div>
<table>
    <tr>
        <th>Description</th>
        <th>Quantity</th>
        <th>Price</th>
        <th>Total</th>
    </tr>
    <tr>
        <td>Web Development Services</td>
        <td>10 hours</td>
        <td>$100.00</td>
        <td>$1,000.00</td>
    </tr>
    <tr>
        <td>Consulting</td>
        <td>5 hours</td>
        <td>$150.00</td>
        <td>$750.00</td>
    </tr>
</table>
<div class='total'>Total: $1,750.00</div>
</body>
</html>
"

' Initialize the Chrome-based PDF renderer
Dim renderer As New ChromePdfRenderer()

' Convert the HTML string to a PDF document
Dim pdf = renderer.RenderHtmlAsPdf(invoiceHtml)

' Save the generated PDF to disk
pdf.SaveAs("basic-invoice.pdf")
$vbLabelText   $csharpLabel

Muestra de resultado

Cualquier CSS que funcione en Chrome funciona también en IronPDF, incluidos flexbox, grid, fuentes personalizadas y propiedades específicas para la impresión. Las hojas de estilo externas y las imágenes pueden referenciarse por URL o por ruta local del sistema de archivos, lo que permite mantener la lógica de plantilla separada del código C# y editarla con herramientas de diseño habituales.

Partidas dinámicas, base imponible y cuota de IVA

Las facturas reales rara vez tienen partidas estáticas. El patrón ibérico productivo añade exigencias específicas que es necesario modelar en el dominio antes de tocar la plantilla HTML. La clase Factura debe llevar serie y número conforme a la convención ibérica habitual ({serie}/{ejercicio}/{secuencial}, por ejemplo A/2026/00237), NIF del emisor y del receptor con validación del dígito de control, dirección fiscal con código postal de cinco dígitos, y partidas que distinguen base imponible por tipo de IVA — general 21 %, reducido 10 %, superreducido 4 %, exento o no sujeto. En servicios profesionales, hay que prever la retención del IRPF (15 % general, 7 % en alta inicial autónomos). En regímenes específicos (recargo de equivalencia para minoristas, inversión del sujeto pasivo en operaciones intracomunitarias o de construcción), las partidas llevan marcas que afectan al cálculo del IVA y al texto legal que debe aparecer en la factura.

El cálculo del total ibérico es: total = sum(base_imponible_por_tipo) + sum(cuota_IVA_por_tipo) - retenciones_IRPF + recargo_equivalencia_si_aplica. La cuota de IVA se calcula independientemente por cada tipo (no se mezclan las bases de 21 % con las de 10 % o 4 %), y cada tipo aparece como línea separada en el desglose. El siguiente patrón muestra una clase mínima FacturaIberica que captura este modelo, junto con la generación del HTML con los desgloses correctos:

using IronPdf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;

// Línea de factura ibérica con tipo de IVA explícito.
public class LineaFactura
{
    public string Descripcion { get; set; }
    public decimal Cantidad { get; set; }
    public decimal PrecioUnitario { get; set; }
    public decimal TipoIva { get; set; } = 0.21m;  // 21 % general por defecto
    public decimal BaseImponible => Cantidad * PrecioUnitario;
    public decimal CuotaIva => BaseImponible * TipoIva;
}

// Factura ibérica con desglose multi-IVA y retención IRPF opcional.
public class FacturaIberica
{
    public string Serie { get; set; }            // p. ej. "A"
    public int Ejercicio { get; set; }            // p. ej. 2026
    public int Secuencial { get; set; }           // p. ej. 237
    public DateTime FechaEmision { get; set; }
    public string NifEmisor { get; set; }
    public string NombreEmisor { get; set; }
    public string NifReceptor { get; set; }
    public string DireccionFiscalReceptor { get; set; }
    public List<LineaFactura> Lineas { get; set; }
    public decimal TipoRetencionIrpf { get; set; }  // 0,15m, 0,07m o 0m

    public string NumeroFactura =>
        $"{Serie}/{Ejercicio:D4}/{Secuencial:D5}";

    public decimal BaseImponibleTotal => Lineas.Sum(l => l.BaseImponible);
    public decimal CuotaIvaTotal => Lineas.Sum(l => l.CuotaIva);
    public decimal RetencionIrpf => BaseImponibleTotal * TipoRetencionIrpf;
    public decimal Total => BaseImponibleTotal + CuotaIvaTotal - RetencionIrpf;

    // Desglose por tipo de IVA para mostrar en el cuerpo de la factura.
    public IEnumerable<(decimal Tipo, decimal Base, decimal Cuota)> DesgloseIva() =>
        Lineas.GroupBy(l => l.TipoIva)
              .Select(g => (g.Key,
                            g.Sum(l => l.BaseImponible),
                            g.Sum(l => l.CuotaIva)));
}
using IronPdf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;

// Línea de factura ibérica con tipo de IVA explícito.
public class LineaFactura
{
    public string Descripcion { get; set; }
    public decimal Cantidad { get; set; }
    public decimal PrecioUnitario { get; set; }
    public decimal TipoIva { get; set; } = 0.21m;  // 21 % general por defecto
    public decimal BaseImponible => Cantidad * PrecioUnitario;
    public decimal CuotaIva => BaseImponible * TipoIva;
}

// Factura ibérica con desglose multi-IVA y retención IRPF opcional.
public class FacturaIberica
{
    public string Serie { get; set; }            // p. ej. "A"
    public int Ejercicio { get; set; }            // p. ej. 2026
    public int Secuencial { get; set; }           // p. ej. 237
    public DateTime FechaEmision { get; set; }
    public string NifEmisor { get; set; }
    public string NombreEmisor { get; set; }
    public string NifReceptor { get; set; }
    public string DireccionFiscalReceptor { get; set; }
    public List<LineaFactura> Lineas { get; set; }
    public decimal TipoRetencionIrpf { get; set; }  // 0,15m, 0,07m o 0m

    public string NumeroFactura =>
        $"{Serie}/{Ejercicio:D4}/{Secuencial:D5}";

    public decimal BaseImponibleTotal => Lineas.Sum(l => l.BaseImponible);
    public decimal CuotaIvaTotal => Lineas.Sum(l => l.CuotaIva);
    public decimal RetencionIrpf => BaseImponibleTotal * TipoRetencionIrpf;
    public decimal Total => BaseImponibleTotal + CuotaIvaTotal - RetencionIrpf;

    // Desglose por tipo de IVA para mostrar en el cuerpo de la factura.
    public IEnumerable<(decimal Tipo, decimal Base, decimal Cuota)> DesgloseIva() =>
        Lineas.GroupBy(l => l.TipoIva)
              .Select(g => (g.Key,
                            g.Sum(l => l.BaseImponible),
                            g.Sum(l => l.CuotaIva)));
}
Imports IronPdf
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Globalization

' Línea de factura ibérica con tipo de IVA explícito.
Public Class LineaFactura
    Public Property Descripcion As String
    Public Property Cantidad As Decimal
    Public Property PrecioUnitario As Decimal
    Public Property TipoIva As Decimal = 0.21D ' 21 % general por defecto
    Public ReadOnly Property BaseImponible As Decimal
        Get
            Return Cantidad * PrecioUnitario
        End Get
    End Property
    Public ReadOnly Property CuotaIva As Decimal
        Get
            Return BaseImponible * TipoIva
        End Get
    End Property
End Class

' Factura ibérica con desglose multi-IVA y retención IRPF opcional.
Public Class FacturaIberica
    Public Property Serie As String ' p. ej. "A"
    Public Property Ejercicio As Integer ' p. ej. 2026
    Public Property Secuencial As Integer ' p. ej. 237
    Public Property FechaEmision As DateTime
    Public Property NifEmisor As String
    Public Property NombreEmisor As String
    Public Property NifReceptor As String
    Public Property DireccionFiscalReceptor As String
    Public Property Lineas As List(Of LineaFactura)
    Public Property TipoRetencionIrpf As Decimal ' 0.15D, 0.07D o 0D

    Public ReadOnly Property NumeroFactura As String
        Get
            Return $"{Serie}/{Ejercicio:D4}/{Secuencial:D5}"
        End Get
    End Property

    Public ReadOnly Property BaseImponibleTotal As Decimal
        Get
            Return Lineas.Sum(Function(l) l.BaseImponible)
        End Get
    End Property

    Public ReadOnly Property CuotaIvaTotal As Decimal
        Get
            Return Lineas.Sum(Function(l) l.CuotaIva)
        End Get
    End Property

    Public ReadOnly Property RetencionIrpf As Decimal
        Get
            Return BaseImponibleTotal * TipoRetencionIrpf
        End Get
    End Property

    Public ReadOnly Property Total As Decimal
        Get
            Return BaseImponibleTotal + CuotaIvaTotal - RetencionIrpf
        End Get
    End Property

    ' Desglose por tipo de IVA para mostrar en el cuerpo de la factura.
    Public Function DesgloseIva() As IEnumerable(Of (Tipo As Decimal, Base As Decimal, Cuota As Decimal))
        Return Lineas.GroupBy(Function(l) l.TipoIva) _
                     .Select(Function(g) (g.Key, g.Sum(Function(l) l.BaseImponible), g.Sum(Function(l) l.CuotaIva)))
    End Function
End Class
$vbLabelText   $csharpLabel

La invocación del renderizador IronPDF parte de la instancia de FacturaIberica, formatea los importes con CultureInfo("es-ES") para garantizar la convención de coma decimal y punto miles (1.234,56 €), e inyecta el desglose de IVA como filas separadas en la tabla de totales. El patrón [{importe:N2} €] con cultura es-ES produce automáticamente 1.234,56 €; el formato [{importe:C}] con la misma cultura añade el símbolo de euro postfijado con espacio fino. En contextos técnicos o de API, el formato EUR 1234.56 con cultura invariante es preferible — la decisión depende de si la salida es destinada a humanos o a sistemas downstream.

Muestra de resultado

Para entornos B2C (consumidor final), conviene además validar el NIF receptor cuando es persona física: el dígito de control del NIF se calcula como la letra correspondiente al resto módulo 23 del número, según la tabla oficial publicada por la AEAT. Una factura emitida con NIF inválido se rechaza por la AEAT al ser reportada en SII o en VeriFactu — conviene atrapar este error en validación cliente antes de generar el PDF.

Marca de empresa y marcas de agua (BORRADOR, PAGADA)

Las facturas profesionales necesitan marca corporativa (logotipo, colores, tipografía) y, en algunos flujos, marcas de agua que indiquen el estado del documento. En el contexto ibérico es habitual marcar facturas en borrador como BORRADOR antes de su contabilización definitiva, y como PAGADA cuando se ha conciliado el cobro:

:path=/static-assets/pdf/content-code-examples/tutorials/csharp-invoice-processing/branding-watermarks.cs
using IronPdf;
using IronPdf;

var renderer = new ChromePdfRenderer();

// Invoice HTML template with company logo embedded via URL
// Logo can also be Base64-encoded or a local file path
string htmlWithLogo = @"
E html>


le>
body { font-family: Arial, sans-serif; padding: 40px; }
.logo { width: 200px; margin-bottom: 20px; }
yle>


 style='text-align: center;'>
<img src='https://yourcompany.com/logo.png' alt='Company Logo' class='logo' />
v>
INVOICE</h1>
strong>Invoice Number:</strong> INV-2024-001</p>
strong>Total:</strong> $1,250.00</p>

;

// Render the HTML to PDF
var pdf = renderer.RenderHtmlAsPdf(htmlWithLogo);

// Apply a diagonal "UNPAID" watermark to mark invoice status
// 30% opacity keeps the content readable while the watermark is visible
pdf.ApplyWatermark("<h1 style='color: red;'>UNPAID</h1>",
    opacity: 30,
    rotation: 45,
    verticalAlignment: IronPdf.Editing.VerticalAlignment.Middle);

pdf.SaveAs("invoice-with-watermark.pdf");
using IronPdf;
Imports IronPdf

Dim renderer As New ChromePdfRenderer()

' Invoice HTML template with company logo embedded via URL
' Logo can also be Base64-encoded or a local file path
Dim htmlWithLogo As String = "
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; padding: 40px; }
        .logo { width: 200px; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div style='text-align: center;'>
        <img src='https://yourcompany.com/logo.png' alt='Company Logo' class='logo' />
        <h1>INVOICE</h1>
        <p><strong>Invoice Number:</strong> INV-2024-001</p>
        <p><strong>Total:</strong> $1,250.00</p>
    </div>
</body>
</html>
"

' Render the HTML to PDF
Dim pdf = renderer.RenderHtmlAsPdf(htmlWithLogo)

' Apply a diagonal "UNPAID" watermark to mark invoice status
' 30% opacity keeps the content readable while the watermark is visible
pdf.ApplyWatermark("<h1 style='color: red;'>UNPAID</h1>",
                   opacity:=30,
                   rotation:=45,
                   verticalAlignment:=IronPdf.Editing.VerticalAlignment.Middle)

pdf.SaveAs("invoice-with-watermark.pdf")
$vbLabelText   $csharpLabel

Muestra de resultado

El método ApplyWatermark acepta contenido HTML, con lo que dispone de control total sobre el aspecto: opacidad, rotación, posición. Esto resulta útil para marcar facturas como BORRADOR, PAGADA, RECTIFICATIVA, ANULADA o para añadir el sello de la huella VeriFactu de forma visible sin necesidad de regenerar todo el documento.

Incrustar el QR obligatorio de la AEAT y la leyenda VERI*FACTU

Esta es la sección con mayor especificidad ibérica del tutorial. Toda visualización de factura conforme con VeriFactu — tanto factura completa como factura simplificada (tique) — debe incluir un código QR generado conforme a la especificación de la AEAT. El payload del QR contiene la URL del verificador de la sede electrónica, los datos identificativos mínimos del documento (NIF emisor, número y serie, fecha, importe total) y un identificador del registro de facturación. El cliente final escanea el QR con su móvil y la sede electrónica de la AEAT le devuelve, en tiempo real, una confirmación de que el documento ha sido remitido al sistema (en modo VeriFactu propiamente dicho) o una indicación de que el sistema opera en modo no VeriFactu con registro local únicamente.

La leyenda textual obligatoria es VERI*FACTU — con asterisco en el medio, exactamente así, sin modificaciones. La especificación admite alternativamente el texto largo Factura verificable en la sede electrónica de la AEAT. Ambos elementos pueden coexistir en el documento. Cualquier paráfrasis, traducción o reordenación de estas cadenas invalida el cumplimiento formal.

IronPDF se combina con IronQR para incrustar el código QR en el HTML antes del renderizado, manteniendo la coherencia entre el documento PDF entregado al cliente y los datos enviados a la AEAT:

:path=/static-assets/pdf/content-code-examples/tutorials/csharp-invoice-processing/qr-code-payment.cs
using IronPdf;
using IronQr;
using IronSoftware.Drawing;

string invoiceNumber = "INV-2026-002";
decimal amount = 1500.00m;

// Create a payment URL with invoice details as query parameters
string paymentUrl = $"https://yourcompany.com/pay?invoice={invoiceNumber}&amount={amount}";

// Generate QR code from the payment URL using IronQR
QrCode qrCode = QrWriter.Write(paymentUrl);
AnyBitmap qrImage = qrCode.Save();
qrImage.SaveAs("payment-qr.png", AnyBitmap.ImageFormat.Png);

// Build invoice HTML with the QR code image embedded
// Customers can scan the QR to pay directly from their phone
string invoiceHtml = $@"
E html>


le>
body {{ font-family: Arial, sans-serif; padding: 40px; }}
.payment-section {{ margin-top: 40px; text-align: center;
                   border-top: 2px solid #eee; padding-top: 20px; }}
.qr-code {{ width: 150px; height: 150px; }}
yle>


INVOICE {invoiceNumber}</h1>
strong>Amount Due:</strong> ${amount:F2}</p>

 class='payment-section'>
<p><strong>Scan to Pay Instantly:</strong></p>
<img src='payment-qr.png' alt='Payment QR Code' class='qr-code' />
<p style='font-size: 12px; color: #666;'>
    Or visit: {paymentUrl}
</p>
v>

;

// Convert HTML to PDF and save
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf(invoiceHtml);
pdf.SaveAs($"invoice-{invoiceNumber}.pdf");
Imports IronPdf
Imports IronQr
Imports IronSoftware.Drawing

Dim invoiceNumber As String = "INV-2026-002"
Dim amount As Decimal = 1500.00D

' Create a payment URL with invoice details as query parameters
Dim paymentUrl As String = $"https://yourcompany.com/pay?invoice={invoiceNumber}&amount={amount}"

' Generate QR code from the payment URL using IronQR
Dim qrCode As QrCode = QrWriter.Write(paymentUrl)
Dim qrImage As AnyBitmap = qrCode.Save()
qrImage.SaveAs("payment-qr.png", AnyBitmap.ImageFormat.Png)

' Build invoice HTML with the QR code image embedded
' Customers can scan the QR to pay directly from their phone
Dim invoiceHtml As String = $"
<!DOCTYPE html>
<html>
<head>
    <title>Invoice</title>
    <style>
        body {{ font-family: Arial, sans-serif; padding: 40px; }}
        .payment-section {{ margin-top: 40px; text-align: center; border-top: 2px solid #eee; padding-top: 20px; }}
        .qr-code {{ width: 150px; height: 150px; }}
    </style>
</head>
<body>
    <h1>INVOICE {invoiceNumber}</h1>
    <p><strong>Amount Due:</strong> ${amount:F2}</p>
    <div class='payment-section'>
        <p><strong>Scan to Pay Instantly:</strong></p>
        <img src='payment-qr.png' alt='Payment QR Code' class='qr-code' />
        <p style='font-size: 12px; color: #666;'>
            Or visit: {paymentUrl}
        </p>
    </div>
</body>
</html>"

' Convert HTML to PDF and save
Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf(invoiceHtml)
pdf.SaveAs($"invoice-{invoiceNumber}.pdf")
$vbLabelText   $csharpLabel

Muestra de resultado

En el HTML se reserva un bloque con la imagen del QR (generada por IronQR como PNG o SVG embebido en data: URI) y, contiguo a ella, la leyenda VERI*FACTU. El QR no debe ocultarse ni reducirse a un tamaño que impida su lectura — la especificación AEAT fija un tamaño mínimo de visualización y un nivel de corrección de errores mínimo (típicamente Q o H). Ese mismo bloque puede acoger un segundo QR si el flujo TicketBAI lo requiere — la Diputación Foral correspondiente publica su propia especificación de QR de ticket, distinta del QR AEAT.


Firma XAdES para TicketBAI (Bizkaia / Gipuzkoa / Araba)

El flujo TicketBAI exige una pieza adicional sobre la generación PDF: la firma XAdES sobre el XML TicketBAI, encadenada al ticket anterior. Esta firma propiamente dicha no la genera IronPDF — la genera la biblioteca XAdES del ISV o un componente especializado. Pero el resultado del flujo — el ticket-factura entregable al cliente — sigue siendo un PDF, y ese PDF es responsabilidad de IronPDF.

Cadena de bloques de tickets y huella encadenada

Cada ticket TicketBAI lleva, dentro del XML firmado con XAdES, una referencia al hash del XML del ticket inmediatamente anterior emitido por el mismo sujeto pasivo en el mismo dispositivo. La estructura resultante es análoga a una cadena de bloques restringida al log local: cualquier intento de modificar, borrar o reordenar tickets anteriores rompe la cadena en el punto del cambio y se detecta en la siguiente verificación. La huella encadenada VeriFactu sigue la misma lógica conceptual aplicada al log VeriFactu, pero usando un hash simple en lugar de una firma XAdES completa.

using System;
using System.Security.Cryptography;
using System.Text;

// Patrón análogo a la cadena de tickets TicketBAI: cada registro
// referencia el hash del registro anterior. La cadena resultante
// es auditable de extremo a extremo y replica la lógica del libro
// de eventos VeriFactu en el lado del software de facturación.
public class RegistroFacturacion
{
    public string NumeroSerie { get; set; }
    public DateTime FechaEmision { get; set; }
    public string NifEmisor { get; set; }
    public decimal ImporteTotal { get; set; }
    public string HuellaAnterior { get; set; }       // huella del registro previo
    public string HuellaActual { get; private set; } // huella propia, calculada

    public void CalcularHuella()
    {
        // Canonicaliza los campos del registro y los encadena con la huella
        // del registro inmediatamente anterior en el log VeriFactu local.
        string payload = $"{NumeroSerie}|{FechaEmision:yyyyMMddHHmmss}|{NifEmisor}|{ImporteTotal:F2}|{HuellaAnterior}";
        using var sha256 = SHA256.Create();
        byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(payload));
        HuellaActual = Convert.ToHexString(hash);
    }
}
using System;
using System.Security.Cryptography;
using System.Text;

// Patrón análogo a la cadena de tickets TicketBAI: cada registro
// referencia el hash del registro anterior. La cadena resultante
// es auditable de extremo a extremo y replica la lógica del libro
// de eventos VeriFactu en el lado del software de facturación.
public class RegistroFacturacion
{
    public string NumeroSerie { get; set; }
    public DateTime FechaEmision { get; set; }
    public string NifEmisor { get; set; }
    public decimal ImporteTotal { get; set; }
    public string HuellaAnterior { get; set; }       // huella del registro previo
    public string HuellaActual { get; private set; } // huella propia, calculada

    public void CalcularHuella()
    {
        // Canonicaliza los campos del registro y los encadena con la huella
        // del registro inmediatamente anterior en el log VeriFactu local.
        string payload = $"{NumeroSerie}|{FechaEmision:yyyyMMddHHmmss}|{NifEmisor}|{ImporteTotal:F2}|{HuellaAnterior}";
        using var sha256 = SHA256.Create();
        byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(payload));
        HuellaActual = Convert.ToHexString(hash);
    }
}
Imports System
Imports System.Security.Cryptography
Imports System.Text

' Patrón análogo a la cadena de tickets TicketBAI: cada registro
' referencia el hash del registro anterior. La cadena resultante
' es auditable de extremo a extremo y replica la lógica del libro
' de eventos VeriFactu en el lado del software de facturación.
Public Class RegistroFacturacion
    Public Property NumeroSerie As String
    Public Property FechaEmision As DateTime
    Public Property NifEmisor As String
    Public Property ImporteTotal As Decimal
    Public Property HuellaAnterior As String       ' huella del registro previo
    Public Property HuellaActual As String         ' huella propia, calculada

    Public Sub CalcularHuella()
        ' Canonicaliza los campos del registro y los encadena con la huella
        ' del registro inmediatamente anterior en el log VeriFactu local.
        Dim payload As String = $"{NumeroSerie}|{FechaEmision:yyyyMMddHHmmss}|{NifEmisor}|{ImporteTotal:F2}|{HuellaAnterior}"
        Using sha256 As SHA256 = SHA256.Create()
            Dim hash As Byte() = sha256.ComputeHash(Encoding.UTF8.GetBytes(payload))
            HuellaActual = Convert.ToHexString(hash)
        End Using
    End Sub
End Class
$vbLabelText   $csharpLabel

El patrón aplica tanto al log VeriFactu interno como, en el modo TicketBAI, al campo EncadenamientoFactura del XML. La diferencia está en quién firma y dónde se persiste — el log VeriFactu vive en el almacén local del software de facturación y se remite opcionalmente a la AEAT; el ticket TicketBAI se firma con XAdES y se remite obligatoriamente a la Diputación Foral correspondiente, con plazos de envío diferentes en Bizkaia, Gipuzkoa y Araba.

Patrón de firma XAdES con certificado FNMT-RCM

La firma XAdES sobre el XML TicketBAI se realiza con un certificado cualificado en el sentido eIDAS. En el entorno ibérico predomina el certificado FNMT-RCM (Fábrica Nacional de Moneda y Timbre — Real Casa de la Moneda) emitido sobre tarjeta criptográfica o sobre soporte software. En la Comunidad Foral del País Vasco coexiste el certificado emitido por IZENPE, autoridad de certificación pública vasca, y otros emisores privados (AC Camerfirma, FirmaProfesional) son habituales en el sector empresarial. Para clientes con volumen significativo o requisitos de protección de clave avanzados, la clave privada del certificado se mantiene dentro de un HSM (Hardware Security Module) accesible vía interfaz PKCS#11.

La biblioteca XAdES propiamente dicha queda fuera del alcance de IronPDF. La pieza que sí cubre IronPDF es la firma PAdES sobre el PDF visualizable del ticket — una capa adicional de no repudio que algunas configuraciones de cumplimiento aplican incluso sobre el PDF de visualización, no solo sobre el XML TicketBAI. PAdES (PDF Advanced Electronic Signatures, ETSI EN 319 142) es a PDF lo que XAdES es a XML, y comparte el mismo modelo de certificados cualificados. El certificado de firmante es el mismo en ambos casos.

Para los flujos donde la organización tiene su parque de certificados FNMT-RCM en un HSM corporativo, IronPDF admite firma PAdES a través del proveedor criptográfico del sistema operativo o vía PKCS#11 directo. El resultado es un PDF firmado con la marca temporal del momento de la firma (típicamente desde un servicio TSA acreditado, como los publicados en la lista de prestadores cualificados de servicios de confianza de la Secretaría de Estado de Digitalización), conservando la trazabilidad legal exigida por el conjunto eIDAS + LOPDGDD + régimen sectorial específico (sanidad, alimentación, etc.).

Una nota operativa: el patrón habitual de implementación en ISV ibéricos no es generar el PDF y firmar después con un proceso separado, sino integrar la firma PAdES dentro del propio flujo de generación, de modo que el documento de salida ya es el documento firmado. Esto reduce ventanas de tiempo en las que el PDF no firmado existe transitoriamente — relevante para la cadena de custodia LOPDGDD del documento que contiene datos personales del receptor.


Generación de Facturae XML para FACe (B2G) y Crea y Crece (B2B)

Pasamos al tercer marco regulatorio: la generación de XML estructurado Facturae. Aunque Facturae como formato lleva años en producción en el flujo B2G hacia FACe, su importancia crece bajo Crea y Crece porque pasa a ser también el formato B2B de referencia. Para el desarrollador, el patrón es similar en ambos casos: serializar los datos de la factura en XML Facturae 3.2.2 (o versión vigente), firmar el XML con XAdES, y opcionalmente embeber el XML dentro de un PDF/A-3 como factura híbrida.

Facturae 3.2.2 y CIUS-ES sobre EN 16931

Facturae es un esquema XML mantenido en sede.agenciatributaria.gob.es. La versión vigente al momento de este tutorial es 3.2.2, con variantes 3.2.1 y 3.2 aún en uso en algunas plataformas legacy. La norma europea EN 16931 define un modelo semántico de factura común para toda la UE, y los Estados miembros definen sus Core Invoice Usage Specifications (CIUS) — perfiles nacionales que restringen y extienden el modelo común. CIUS-ES es la concreción española de EN 16931 para Facturae. La consecuencia para el desarrollador es que el contenido obligatorio del XML Facturae está condicionado por el cruce entre Facturae 3.2.2 (esquema técnico nacional) y CIUS-ES (perfil semántico nacional sobre EN 16931).

Los elementos clave del XML Facturae son: identificación del emisor con NIF, dirección fiscal y opcionalmente datos registrales (registro mercantil, datos del Colegio profesional en su caso); identificación del receptor con NIF y dirección; periodo de facturación; partidas con la información detallada por línea (descripción, cantidad, precio unitario, importe sin impuestos, tipo de IVA aplicable); desglose de impuestos por tipo (la separación entre IVA, IGIC canario, IPSI ceutí/melillense según jurisdicción del receptor); retenciones del IRPF si aplican; y resumen de totales. El esquema admite también extensiones específicas para regímenes especiales (recargo de equivalencia, criterio de caja, etc.).

Patrón híbrido: PDF/A-3 con Facturae XML embebido

El patrón técnico más interesante de cara al receptor de factura entrante es el híbrido: un único archivo PDF/A-3 que contiene la visualización humana de la factura (el PDF renderizado tradicionalmente) y, embebido como anexo, el XML Facturae con los mismos datos estructurados. PDF/A-3 (ISO 19005-3) es el primer perfil PDF/A que admite explícitamente la incrustación de archivos arbitrarios — su predecesor PDF/A-2 ya admitía anexos, pero PDF/A-3 los integró de forma normativa para casos de uso como éste.

Para el emisor, el patrón híbrido permite distribuir un único archivo que satisface dos audiencias: el humano (departamento de cuentas a pagar del cliente) que abre el PDF y revisa la factura visualmente, y la máquina (el ERP del receptor) que extrae el XML embebido y lo procesa sin intervención manual. Para el receptor, el patrón híbrido elimina la necesidad de OCR — la extracción se reduce a leer el archivo Facturae.xml embebido en el PDF/A-3. En el contexto Crea y Crece, el patrón híbrido es la forma técnicamente más limpia de cumplir simultáneamente con el envío del documento estructurado y con la entrega de un documento visualizable al receptor.

IronPDF cubre el lado emisor del patrón: genera el PDF/A-3 desde la plantilla HTML y embebe el archivo Facturae.xml como anexo, con el nombre de archivo, tipo MIME y relación apropiados:

using IronPdf;
using System;
using System.IO;
using System.Xml.Linq;

// Genera factura híbrida PDF/A-3 + Facturae para los flujos Crea y Crece
// y FACe. El PDF visual es lo que ve el humano; el XML Facturae embebido
// como anexo es lo que el ERP receptor procesa sin necesidad de OCR.
public class GeneradorFacturaHibrida
{
    public void GenerarFacturaHibrida(FacturaIberica factura, string rutaFacturaeXml, string rutaSalida)
    {
        // Renderiza el lado humano del documento siguiendo el mismo patrón
        // de plantilla HTML que la factura PDF independiente.
        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.PdfPaperSize = IronPdf.Rendering.PdfPaperSize.A4;

        string facturaHtml = ConstruirHtmlFactura(factura);
        var pdf = renderer.RenderHtmlAsPdf(facturaHtml);

        // Convierte a PDF/A-3b. PDF/A-3 es el primer perfil PDF/A que admite
        // normativamente anexos arbitrarios — por eso Facturae lo eligió como
        // contenedor del patrón híbrido.
        pdf = pdf.ConvertToPdfA(IronPdf.PdfAVersions.PdfA3b);

        // Embebe el XML Facturae como anexo con el nombre convencional.
        // El ERP receptor busca exactamente este nombre de archivo.
        byte[] facturaeXml = File.ReadAllBytes(rutaFacturaeXml);
        pdf.Attachments.AddAttachment("Facturae.xml", facturaeXml);

        pdf.SaveAs(rutaSalida);
    }

    private string ConstruirHtmlFactura(FacturaIberica factura)
    {
        // El HTML visible refleja los datos del XML Facturae para que
        // la verificación humana del PDF y la ingestión automatizada del
        // XML estructurado concuerden — discordancia compromete la validez.
        return $@"
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8' />
    <style>
        body {{ font-family: Arial, sans-serif; padding: 30px; }}
        .header {{ border-bottom: 2px solid #333; padding-bottom: 10px; }}
        .totals {{ margin-top: 20px; text-align: right; }}
        .legal {{ margin-top: 30px; font-size: 11px; color: #555; }}
    </style>
</head>
<body>
    <div class='header'>
        <h1>FACTURA {factura.NumeroFactura}</h1>
        <p>Fecha de emisión: {factura.FechaEmision:dd/MM/yyyy}</p>
    </div>
    <p><strong>Emisor:</strong> {factura.NombreEmisor} — NIF {factura.NifEmisor}</p>
    <p><strong>Receptor:</strong> NIF {factura.NifReceptor} — {factura.DireccionFiscalReceptor}</p>
    <div class='totals'>
        <p>Base imponible: {factura.BaseImponibleTotal:N2} €</p>
        <p>Cuota IVA (21 %): {factura.CuotaIvaTotal:N2} €</p>
        <p><strong>Total: {factura.Total:N2} €</strong></p>
    </div>
    <div class='legal'>
        Documento conforme con el esquema Facturae 3.2.2 (CIUS-ES sobre EN 16931).
        XML estructurado embebido como anexo bajo el patrón híbrido PDF/A-3.
    </div>
</body>
</html>";
    }
}
using IronPdf;
using System;
using System.IO;
using System.Xml.Linq;

// Genera factura híbrida PDF/A-3 + Facturae para los flujos Crea y Crece
// y FACe. El PDF visual es lo que ve el humano; el XML Facturae embebido
// como anexo es lo que el ERP receptor procesa sin necesidad de OCR.
public class GeneradorFacturaHibrida
{
    public void GenerarFacturaHibrida(FacturaIberica factura, string rutaFacturaeXml, string rutaSalida)
    {
        // Renderiza el lado humano del documento siguiendo el mismo patrón
        // de plantilla HTML que la factura PDF independiente.
        var renderer = new ChromePdfRenderer();
        renderer.RenderingOptions.PdfPaperSize = IronPdf.Rendering.PdfPaperSize.A4;

        string facturaHtml = ConstruirHtmlFactura(factura);
        var pdf = renderer.RenderHtmlAsPdf(facturaHtml);

        // Convierte a PDF/A-3b. PDF/A-3 es el primer perfil PDF/A que admite
        // normativamente anexos arbitrarios — por eso Facturae lo eligió como
        // contenedor del patrón híbrido.
        pdf = pdf.ConvertToPdfA(IronPdf.PdfAVersions.PdfA3b);

        // Embebe el XML Facturae como anexo con el nombre convencional.
        // El ERP receptor busca exactamente este nombre de archivo.
        byte[] facturaeXml = File.ReadAllBytes(rutaFacturaeXml);
        pdf.Attachments.AddAttachment("Facturae.xml", facturaeXml);

        pdf.SaveAs(rutaSalida);
    }

    private string ConstruirHtmlFactura(FacturaIberica factura)
    {
        // El HTML visible refleja los datos del XML Facturae para que
        // la verificación humana del PDF y la ingestión automatizada del
        // XML estructurado concuerden — discordancia compromete la validez.
        return $@"
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8' />
    <style>
        body {{ font-family: Arial, sans-serif; padding: 30px; }}
        .header {{ border-bottom: 2px solid #333; padding-bottom: 10px; }}
        .totals {{ margin-top: 20px; text-align: right; }}
        .legal {{ margin-top: 30px; font-size: 11px; color: #555; }}
    </style>
</head>
<body>
    <div class='header'>
        <h1>FACTURA {factura.NumeroFactura}</h1>
        <p>Fecha de emisión: {factura.FechaEmision:dd/MM/yyyy}</p>
    </div>
    <p><strong>Emisor:</strong> {factura.NombreEmisor} — NIF {factura.NifEmisor}</p>
    <p><strong>Receptor:</strong> NIF {factura.NifReceptor} — {factura.DireccionFiscalReceptor}</p>
    <div class='totals'>
        <p>Base imponible: {factura.BaseImponibleTotal:N2} €</p>
        <p>Cuota IVA (21 %): {factura.CuotaIvaTotal:N2} €</p>
        <p><strong>Total: {factura.Total:N2} €</strong></p>
    </div>
    <div class='legal'>
        Documento conforme con el esquema Facturae 3.2.2 (CIUS-ES sobre EN 16931).
        XML estructurado embebido como anexo bajo el patrón híbrido PDF/A-3.
    </div>
</body>
</html>";
    }
}
Imports IronPdf
Imports System
Imports System.IO
Imports System.Xml.Linq

' Genera factura híbrida PDF/A-3 + Facturae para los flujos Crea y Crece
' y FACe. El PDF visual es lo que ve el humano; el XML Facturae embebido
' como anexo es lo que el ERP receptor procesa sin necesidad de OCR.
Public Class GeneradorFacturaHibrida
    Public Sub GenerarFacturaHibrida(factura As FacturaIberica, rutaFacturaeXml As String, rutaSalida As String)
        ' Renderiza el lado humano del documento siguiendo el mismo patrón
        ' de plantilla HTML que la factura PDF independiente.
        Dim renderer = New ChromePdfRenderer()
        renderer.RenderingOptions.PdfPaperSize = IronPdf.Rendering.PdfPaperSize.A4

        Dim facturaHtml As String = ConstruirHtmlFactura(factura)
        Dim pdf = renderer.RenderHtmlAsPdf(facturaHtml)

        ' Convierte a PDF/A-3b. PDF/A-3 es el primer perfil PDF/A que admite
        ' normativamente anexos arbitrarios — por eso Facturae lo eligió como
        ' contenedor del patrón híbrido.
        pdf = pdf.ConvertToPdfA(IronPdf.PdfAVersions.PdfA3b)

        ' Embebe el XML Facturae como anexo con el nombre convencional.
        ' El ERP receptor busca exactamente este nombre de archivo.
        Dim facturaeXml As Byte() = File.ReadAllBytes(rutaFacturaeXml)
        pdf.Attachments.AddAttachment("Facturae.xml", facturaeXml)

        pdf.SaveAs(rutaSalida)
    End Sub

    Private Function ConstruirHtmlFactura(factura As FacturaIberica) As String
        ' El HTML visible refleja los datos del XML Facturae para que
        ' la verificación humana del PDF y la ingestión automatizada del
        ' XML estructurado concuerden — discordancia compromete la validez.
        Return $"
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8' />
    <style>
        body {{ font-family: Arial, sans-serif; padding: 30px; }}
        .header {{ border-bottom: 2px solid #333; padding-bottom: 10px; }}
        .totals {{ margin-top: 20px; text-align: right; }}
        .legal {{ margin-top: 30px; font-size: 11px; color: #555; }}
    </style>
</head>
<body>
    <div class='header'>
        <h1>FACTURA {factura.NumeroFactura}</h1>
        <p>Fecha de emisión: {factura.FechaEmision:dd/MM/yyyy}</p>
    </div>
    <p><strong>Emisor:</strong> {factura.NombreEmisor} — NIF {factura.NifEmisor}</p>
    <p><strong>Receptor:</strong> NIF {factura.NifReceptor} — {factura.DireccionFiscalReceptor}</p>
    <div class='totals'>
        <p>Base imponible: {factura.BaseImponibleTotal:N2} €</p>
        <p>Cuota IVA (21 %): {factura.CuotaIvaTotal:N2} €</p>
        <p><strong>Total: {factura.Total:N2} €</strong></p>
    </div>
    <div class='legal'>
        Documento conforme con el esquema Facturae 3.2.2 (CIUS-ES sobre EN 16931).
        XML estructurado embebido como anexo bajo el patrón híbrido PDF/A-3.
    </div>
</body>
</html>"
    End Function
End Class
$vbLabelText   $csharpLabel

El archivo Facturae.xml que se embebe debe haber sido generado previamente conforme al esquema 3.2.2 y, en escenarios B2G, firmado con XAdES por el certificado del emisor antes de la incrustación. La firma del XML es independiente de cualquier firma PAdES que pueda aplicarse posteriormente sobre el PDF/A-3 contenedor — son dos firmas conviven en el mismo documento, una sobre el contenido estructurado y otra sobre el documento visual completo.

Para envíos B2G, el PDF/A-3 híbrido se sube a través de FACe (face.gob.es) — bien a través de la interfaz web directamente, bien vía la API SOAP/REST que FACe expone para integraciones automáticas con ERP. Para envíos B2B Crea y Crece, el PDF/A-3 viaja a través de plataformas privadas autorizadas o por canal directo emisor-receptor según el acuerdo bilateral, hasta que la plataforma pública AEAT alcance su despliegue completo.


Procesamiento de facturas entrantes bajo Crea y Crece

La obligación de Crea y Crece sobre la recepción de facturas electrónicas cambia el centro de gravedad del software de facturación: lo que antes era un caso secundario (cuentas a pagar manual con escaneo de PDF) pasa a ser un flujo automático crítico. El receptor de factura tiene la obligación de procesar el documento entrante en formato estructurado, conciliarlo contra los pedidos y albaranes propios, y registrar el resultado en el libro registro de IVA con los plazos que marca el SII si el receptor es sujeto pasivo del régimen.

Extracción del Facturae embebido sin OCR

Cuando la factura entrante llega como PDF/A-3 híbrido con Facturae embebido, la extracción es trivial: se lee el anexo Facturae.xml directamente, se valida la firma XAdES si está presente, y se parsea el XML contra el esquema 3.2.2. No es necesario OCR ni heurística textual:

using IronPdf;
using System;
using System.Linq;
using System.Xml.Linq;

// Extrae el XML Facturae de una factura entrante en formato PDF/A-3 híbrido.
// Diseñado para el flujo de recepción bajo Crea y Crece, donde la carga útil
// estructurada llega como anexo embebido y no como texto visible del PDF.
public class ExtractorFacturae
{
    public XDocument ExtraerFacturae(string rutaPdf)
    {
        var pdf = PdfDocument.FromFile(rutaPdf);

        // Localiza el anexo Facturae por el nombre de archivo convencional.
        // Algunos emisores usan 'Factura-e.xml' o 'factura.xml' como variantes,
        // por compatibilidad con plataformas legacy o configuraciones locales.
        var anexo = pdf.Attachments.FirstOrDefault(a =>
            a.Name.Equals("Facturae.xml", StringComparison.OrdinalIgnoreCase) ||
            a.Name.Equals("Factura-e.xml", StringComparison.OrdinalIgnoreCase) ||
            a.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase));

        if (anexo == null)
        {
            Console.WriteLine("No se encontró anexo Facturae — recurrir a extracción textual o IA");
            return null;
        }

        // Los datos del anexo son la carga útil estructurada canónica
        // de la factura — todo el resto del PDF es presentación humana.
        string xmlString = System.Text.Encoding.UTF8.GetString(anexo.Data);
        return XDocument.Parse(xmlString);
    }

    // Lee el texto visible del PDF únicamente cuando el anexo híbrido no está
    // presente — por ejemplo en emisores legacy anteriores a Crea y Crece.
    public string ExtraerTextoVisible(string rutaPdf)
    {
        var pdf = PdfDocument.FromFile(rutaPdf);
        return pdf.ExtractAllText();
    }
}
using IronPdf;
using System;
using System.Linq;
using System.Xml.Linq;

// Extrae el XML Facturae de una factura entrante en formato PDF/A-3 híbrido.
// Diseñado para el flujo de recepción bajo Crea y Crece, donde la carga útil
// estructurada llega como anexo embebido y no como texto visible del PDF.
public class ExtractorFacturae
{
    public XDocument ExtraerFacturae(string rutaPdf)
    {
        var pdf = PdfDocument.FromFile(rutaPdf);

        // Localiza el anexo Facturae por el nombre de archivo convencional.
        // Algunos emisores usan 'Factura-e.xml' o 'factura.xml' como variantes,
        // por compatibilidad con plataformas legacy o configuraciones locales.
        var anexo = pdf.Attachments.FirstOrDefault(a =>
            a.Name.Equals("Facturae.xml", StringComparison.OrdinalIgnoreCase) ||
            a.Name.Equals("Factura-e.xml", StringComparison.OrdinalIgnoreCase) ||
            a.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase));

        if (anexo == null)
        {
            Console.WriteLine("No se encontró anexo Facturae — recurrir a extracción textual o IA");
            return null;
        }

        // Los datos del anexo son la carga útil estructurada canónica
        // de la factura — todo el resto del PDF es presentación humana.
        string xmlString = System.Text.Encoding.UTF8.GetString(anexo.Data);
        return XDocument.Parse(xmlString);
    }

    // Lee el texto visible del PDF únicamente cuando el anexo híbrido no está
    // presente — por ejemplo en emisores legacy anteriores a Crea y Crece.
    public string ExtraerTextoVisible(string rutaPdf)
    {
        var pdf = PdfDocument.FromFile(rutaPdf);
        return pdf.ExtractAllText();
    }
}
Imports IronPdf
Imports System
Imports System.Linq
Imports System.Xml.Linq

' Extrae el XML Facturae de una factura entrante en formato PDF/A-3 híbrido.
' Diseñado para el flujo de recepción bajo Crea y Crece, donde la carga útil
' estructurada llega como anexo embebido y no como texto visible del PDF.
Public Class ExtractorFacturae
    Public Function ExtraerFacturae(rutaPdf As String) As XDocument
        Dim pdf = PdfDocument.FromFile(rutaPdf)

        ' Localiza el anexo Facturae por el nombre de archivo convencional.
        ' Algunos emisores usan 'Factura-e.xml' o 'factura.xml' como variantes,
        ' por compatibilidad con plataformas legacy o configuraciones locales.
        Dim anexo = pdf.Attachments.FirstOrDefault(Function(a) _
            a.Name.Equals("Facturae.xml", StringComparison.OrdinalIgnoreCase) OrElse _
            a.Name.Equals("Factura-e.xml", StringComparison.OrdinalIgnoreCase) OrElse _
            a.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))

        If anexo Is Nothing Then
            Console.WriteLine("No se encontró anexo Facturae — recurrir a extracción textual o IA")
            Return Nothing
        End If

        ' Los datos del anexo son la carga útil estructurada canónica
        ' de la factura — todo el resto del PDF es presentación humana.
        Dim xmlString As String = System.Text.Encoding.UTF8.GetString(anexo.Data)
        Return XDocument.Parse(xmlString)
    End Function

    ' Lee el texto visible del PDF únicamente cuando el anexo híbrido no está
    ' presente — por ejemplo en emisores legacy anteriores a Crea y Crece.
    Public Function ExtraerTextoVisible(rutaPdf As String) As String
        Dim pdf = PdfDocument.FromFile(rutaPdf)
        Return pdf.ExtractAllText()
    End Function
End Class
$vbLabelText   $csharpLabel

El acceso a los datos estructurados se reduce a navegar el XML extraído contra el esquema Facturae. Los elementos clave para el flujo de cuentas a pagar son: identificador del emisor (SellerParty/TaxIdentification/TaxIdentificationNumber), fecha de emisión (InvoiceIssueData/IssueDate), número y serie (InvoiceNumber, InvoiceSeriesCode), importe total (InvoiceTotals/InvoiceTotal), desglose IVA (InvoiceTotals/TaxOutputs/Tax), y el detalle de partidas (Items/InvoiceLine).

Reconciliación contra ERP con expresiones regulares ibéricas

Cuando la factura entrante no llega en formato Facturae estructurado — situación todavía frecuente con proveedores fuera del ámbito Crea y Crece, o con clientes B2C que generan facturas en PDF tradicional — es necesario recurrir a extracción textual con patrones. El conjunto de expresiones útiles en el contexto ibérico es específico:

using IronPdf;
using System;
using System.Text.RegularExpressions;

// Extractor de patrones para facturas ibéricas no Facturae. Fallback útil
// cuando el documento es anterior a Crea y Crece o proviene de una contraparte
// fuera del perímetro de facturación estructurada.
public class ExtractorPatronesIberico
{
    public DatosFactura Procesar(string rutaPdf)
    {
        var pdf = PdfDocument.FromFile(rutaPdf);
        string texto = pdf.ExtractAllText();
        var datos = new DatosFactura();

        // NIF español: tres variantes formales — 8 dígitos + letra (persona),
        // letra + 7 dígitos + control (CIF histórico de entidades), o letra
        // X/Y/Z + 7 dígitos + letra (NIE de residentes extranjeros).
        var coincidenciaNif = Regex.Match(texto,
            @"\b([0-9]{8}[A-HJ-NP-TV-Z]|[A-HJ-NP-SUVW][0-9]{7}[0-9A-J]|[XYZ][0-9]{7}[A-HJ-NP-TV-Z])\b");
        if (coincidenciaNif.Success) datos.NifEmisor = coincidenciaNif.Value;

        // Número de factura — convención ibérica habitual: serie/ejercicio/secuencial.
        // Ejemplos válidos: A/2026/00237, FA-2026-12345, FRA0001/26.
        var patronesNumero = new[]
        {
            @"Factura\s*(?:N[uú]mero|N[uú]m\.?|#)?\s*:?\s*([A-Z0-9][A-Z0-9\-/]{2,})",
            @"N[uú]mero\s+de\s+factura\s*:?\s*([A-Z0-9][A-Z0-9\-/]{2,})",
            @"\b([A-Z]{1,3}\d{4,}[-/]\d{2,4})\b"
        };
        foreach (var patron in patronesNumero)
        {
            var coincidencia = Regex.Match(texto, patron, RegexOptions.IgnoreCase);
            if (coincidencia.Success) { datos.NumeroFactura = coincidencia.Groups[1].Value; break; }
        }

        // Fecha — convención ibérica DD/MM/YYYY, también admite punto y guión.
        var coincidenciaFecha = Regex.Match(texto, @"\b(\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4})\b");
        if (coincidenciaFecha.Success) datos.FechaEmision = coincidenciaFecha.Value;

        // Importe total — formato ibérico con coma decimal y punto miles.
        // Etiquetas frecuentes: Total, Importe total, Total factura, A pagar.
        var coincidenciaTotal = Regex.Match(texto,
            @"(?:Total(?:\s+factura)?|Importe\s+total|A\s+pagar)\s*:?\s*([\d.]+,\d{2})\s*€?",
            RegexOptions.IgnoreCase);
        if (coincidenciaTotal.Success)
        {
            // Convierte el formato ibérico "1.234,56" al invariante "1234.56"
            // antes de parsear — los miles se eliminan, la coma pasa a punto.
            string normalizado = coincidenciaTotal.Groups[1].Value
                .Replace(".", "")
                .Replace(",", ".");
            if (decimal.TryParse(normalizado,
                System.Globalization.NumberStyles.Any,
                System.Globalization.CultureInfo.InvariantCulture,
                out decimal total))
            {
                datos.ImporteTotal = total;
            }
        }

        return datos;
    }
}

public class DatosFactura
{
    public string NifEmisor { get; set; }
    public string NumeroFactura { get; set; }
    public string FechaEmision { get; set; }
    public decimal ImporteTotal { get; set; }
}
using IronPdf;
using System;
using System.Text.RegularExpressions;

// Extractor de patrones para facturas ibéricas no Facturae. Fallback útil
// cuando el documento es anterior a Crea y Crece o proviene de una contraparte
// fuera del perímetro de facturación estructurada.
public class ExtractorPatronesIberico
{
    public DatosFactura Procesar(string rutaPdf)
    {
        var pdf = PdfDocument.FromFile(rutaPdf);
        string texto = pdf.ExtractAllText();
        var datos = new DatosFactura();

        // NIF español: tres variantes formales — 8 dígitos + letra (persona),
        // letra + 7 dígitos + control (CIF histórico de entidades), o letra
        // X/Y/Z + 7 dígitos + letra (NIE de residentes extranjeros).
        var coincidenciaNif = Regex.Match(texto,
            @"\b([0-9]{8}[A-HJ-NP-TV-Z]|[A-HJ-NP-SUVW][0-9]{7}[0-9A-J]|[XYZ][0-9]{7}[A-HJ-NP-TV-Z])\b");
        if (coincidenciaNif.Success) datos.NifEmisor = coincidenciaNif.Value;

        // Número de factura — convención ibérica habitual: serie/ejercicio/secuencial.
        // Ejemplos válidos: A/2026/00237, FA-2026-12345, FRA0001/26.
        var patronesNumero = new[]
        {
            @"Factura\s*(?:N[uú]mero|N[uú]m\.?|#)?\s*:?\s*([A-Z0-9][A-Z0-9\-/]{2,})",
            @"N[uú]mero\s+de\s+factura\s*:?\s*([A-Z0-9][A-Z0-9\-/]{2,})",
            @"\b([A-Z]{1,3}\d{4,}[-/]\d{2,4})\b"
        };
        foreach (var patron in patronesNumero)
        {
            var coincidencia = Regex.Match(texto, patron, RegexOptions.IgnoreCase);
            if (coincidencia.Success) { datos.NumeroFactura = coincidencia.Groups[1].Value; break; }
        }

        // Fecha — convención ibérica DD/MM/YYYY, también admite punto y guión.
        var coincidenciaFecha = Regex.Match(texto, @"\b(\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4})\b");
        if (coincidenciaFecha.Success) datos.FechaEmision = coincidenciaFecha.Value;

        // Importe total — formato ibérico con coma decimal y punto miles.
        // Etiquetas frecuentes: Total, Importe total, Total factura, A pagar.
        var coincidenciaTotal = Regex.Match(texto,
            @"(?:Total(?:\s+factura)?|Importe\s+total|A\s+pagar)\s*:?\s*([\d.]+,\d{2})\s*€?",
            RegexOptions.IgnoreCase);
        if (coincidenciaTotal.Success)
        {
            // Convierte el formato ibérico "1.234,56" al invariante "1234.56"
            // antes de parsear — los miles se eliminan, la coma pasa a punto.
            string normalizado = coincidenciaTotal.Groups[1].Value
                .Replace(".", "")
                .Replace(",", ".");
            if (decimal.TryParse(normalizado,
                System.Globalization.NumberStyles.Any,
                System.Globalization.CultureInfo.InvariantCulture,
                out decimal total))
            {
                datos.ImporteTotal = total;
            }
        }

        return datos;
    }
}

public class DatosFactura
{
    public string NifEmisor { get; set; }
    public string NumeroFactura { get; set; }
    public string FechaEmision { get; set; }
    public decimal ImporteTotal { get; set; }
}
Imports IronPdf
Imports System
Imports System.Text.RegularExpressions

' Extractor de patrones para facturas ibéricas no Facturae. Fallback útil
' cuando el documento es anterior a Crea y Crece o proviene de una contraparte
' fuera del perímetro de facturación estructurada.
Public Class ExtractorPatronesIberico
    Public Function Procesar(rutaPdf As String) As DatosFactura
        Dim pdf = PdfDocument.FromFile(rutaPdf)
        Dim texto As String = pdf.ExtractAllText()
        Dim datos As New DatosFactura()

        ' NIF español: tres variantes formales — 8 dígitos + letra (persona),
        ' letra + 7 dígitos + control (CIF histórico de entidades), o letra
        ' X/Y/Z + 7 dígitos + letra (NIE de residentes extranjeros).
        Dim coincidenciaNif = Regex.Match(texto, "\b([0-9]{8}[A-HJ-NP-TV-Z]|[A-HJ-NP-SUVW][0-9]{7}[0-9A-J]|[XYZ][0-9]{7}[A-HJ-NP-TV-Z])\b")
        If coincidenciaNif.Success Then datos.NifEmisor = coincidenciaNif.Value

        ' Número de factura — convención ibérica habitual: serie/ejercicio/secuencial.
        ' Ejemplos válidos: A/2026/00237, FA-2026-12345, FRA0001/26.
        Dim patronesNumero = {
            "Factura\s*(?:N[uú]mero|N[uú]m\.?|#)?\s*:?\s*([A-Z0-9][A-Z0-9\-/]{2,})",
            "N[uú]mero\s+de\s+factura\s*:?\s*([A-Z0-9][A-Z0-9\-/]{2,})",
            "\b([A-Z]{1,3}\d{4,}[-/]\d{2,4})\b"
        }
        For Each patron In patronesNumero
            Dim coincidencia = Regex.Match(texto, patron, RegexOptions.IgnoreCase)
            If coincidencia.Success Then
                datos.NumeroFactura = coincidencia.Groups(1).Value
                Exit For
            End If
        Next

        ' Fecha — convención ibérica DD/MM/YYYY, también admite punto y guión.
        Dim coincidenciaFecha = Regex.Match(texto, "\b(\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4})\b")
        If coincidenciaFecha.Success Then datos.FechaEmision = coincidenciaFecha.Value

        ' Importe total — formato ibérico con coma decimal y punto miles.
        ' Etiquetas frecuentes: Total, Importe total, Total factura, A pagar.
        Dim coincidenciaTotal = Regex.Match(texto, "(?:Total(?:\s+factura)?|Importe\s+total|A\s+pagar)\s*:?\s*([\d.]+,\d{2})\s*€?", RegexOptions.IgnoreCase)
        If coincidenciaTotal.Success Then
            ' Convierte el formato ibérico "1.234,56" al invariante "1234.56"
            ' antes de parsear — los miles se eliminan, la coma pasa a punto.
            Dim normalizado As String = coincidenciaTotal.Groups(1).Value.Replace(".", "").Replace(",", ".")
            Dim total As Decimal
            If Decimal.TryParse(normalizado, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, total) Then
                datos.ImporteTotal = total
            End If
        End If

        Return datos
    End Function
End Class

Public Class DatosFactura
    Public Property NifEmisor As String
    Public Property NumeroFactura As String
    Public Property FechaEmision As String
    Public Property ImporteTotal As Decimal
End Class
$vbLabelText   $csharpLabel

Las particularidades ibéricas a tener en cuenta son: el NIF español tiene tres variantes formales (NIF personal, NIE para residentes extranjeros, antiguo CIF para entidades — reformado pero aún visible en facturas legacy), con un dígito de control calculable; el formato numérico español usa coma como separador decimal y punto como separador de miles, exactamente al revés que la convención angloamericana — el código debe normalizar antes de parsear; el formato de fecha ibérico es DD/MM/YYYY, no MM/DD/YYYY; y la etiqueta de total varía entre Total, Total factura, Importe total, A pagar según el emisor y la versión de su sistema.

Análisis sintáctico con IA para facturas no Facturae

Para facturas entrantes que no llegan en formato Facturae embebido — situación frecuente con proveedores fuera del perímetro Crea y Crece, PDFs escaneados de pequeños suministradores, o plantillas exóticas de proveedores internacionales — el patrón habitual es combinar la extracción textual de IronPDF (pdf.ExtractAllText()) con una llamada a un modelo de lenguaje grande. La biblioteca IronPDF aporta el texto bruto; el LLM aporta la comprensión semántica del contenido y la generación de JSON estructurado conforme a un esquema dado.

Para el contexto ibérico, el prompt debe instruir al modelo de forma explícita sobre las convenciones del documento. Cuatro precisiones marcan la diferencia entre extracciones correctas y extracciones que requieren intervención manual: pedir que el importe total se devuelva con punto como separador decimal (notación canónica del JSON, opuesta a la coma decimal del documento ibérico) para evitar errores de parseo; pedir que la fecha se devuelva en formato ISO YYYY-MM-DD independientemente del formato DD/MM/YYYY que aparezca en el documento; pedir que el NIF se devuelva verbatim sin normalización (cualquier cambio en el dígito de control invalida el identificador); y pedir que el campo proveedor capture el NIF + razón social juntos (la confusión entre nombre comercial y razón social es habitual en proveedores con marca diferenciada del CIF).

// Prompt-shaping para extracción de facturas ibéricas con LLM.
// Combina el texto extraído por IronPDF.ExtractAllText() con instrucciones
// de formato específicas del contexto ibérico antes de llamar al modelo.
public string ConstruirPromptIberico(string textoExtraidoPdf) => $@"
Extrae los datos de la factura española siguiente. Devuelve ÚNICAMENTE JSON
válido con la estructura exacta indicada — sin markdown, sin texto explicativo.

Convenciones del documento (importante):
- El formato numérico ibérico usa coma decimal y punto como separador de miles.
  Convierte ""1.234,56"" a 1234.56 en el JSON resultante.
- La fecha en el documento aparece como DD/MM/YYYY. Conviértela a YYYY-MM-DD.
- El NIF del emisor (formato 8 dígitos + letra, o letra + 7 dígitos + control)
  debe extraerse VERBATIM sin modificación — el dígito de control es sensible.
- Si el proveedor tiene nombre comercial distinto de la razón social, devuelve
  la razón social asociada al NIF (la que aparece en datos de cabecera).

Estructura JSON esperada:
{{
  ""numero"": ""serie/ejercicio/secuencial"",
  ""fecha"": ""YYYY-MM-DD"",
  ""nif_emisor"": ""verbatim del documento"",
  ""razon_social_emisor"": ""razón social completa"",
  ""base_imponible"": 0.00,
  ""cuota_iva"": 0.00,
  ""tipo_iva"": 0.21,
  ""total"": 0.00
}}

Texto extraído de la factura:
{textoExtraidoPdf}";
// Prompt-shaping para extracción de facturas ibéricas con LLM.
// Combina el texto extraído por IronPDF.ExtractAllText() con instrucciones
// de formato específicas del contexto ibérico antes de llamar al modelo.
public string ConstruirPromptIberico(string textoExtraidoPdf) => $@"
Extrae los datos de la factura española siguiente. Devuelve ÚNICAMENTE JSON
válido con la estructura exacta indicada — sin markdown, sin texto explicativo.

Convenciones del documento (importante):
- El formato numérico ibérico usa coma decimal y punto como separador de miles.
  Convierte ""1.234,56"" a 1234.56 en el JSON resultante.
- La fecha en el documento aparece como DD/MM/YYYY. Conviértela a YYYY-MM-DD.
- El NIF del emisor (formato 8 dígitos + letra, o letra + 7 dígitos + control)
  debe extraerse VERBATIM sin modificación — el dígito de control es sensible.
- Si el proveedor tiene nombre comercial distinto de la razón social, devuelve
  la razón social asociada al NIF (la que aparece en datos de cabecera).

Estructura JSON esperada:
{{
  ""numero"": ""serie/ejercicio/secuencial"",
  ""fecha"": ""YYYY-MM-DD"",
  ""nif_emisor"": ""verbatim del documento"",
  ""razon_social_emisor"": ""razón social completa"",
  ""base_imponible"": 0.00,
  ""cuota_iva"": 0.00,
  ""tipo_iva"": 0.21,
  ""total"": 0.00
}}

Texto extraído de la factura:
{textoExtraidoPdf}";
' Prompt-shaping para extracción de facturas ibéricas con LLM.
' Combina el texto extraído por IronPDF.ExtractAllText() con instrucciones
' de formato específicas del contexto ibérico antes de llamar al modelo.
Public Function ConstruirPromptIberico(textoExtraidoPdf As String) As String
    Return $"
Extrae los datos de la factura española siguiente. Devuelve ÚNICAMENTE JSON
válido con la estructura exacta indicada — sin markdown, sin texto explicativo.

Convenciones del documento (importante):
- El formato numérico ibérico usa coma decimal y punto como separador de miles.
  Convierte ""1.234,56"" a 1234.56 en el JSON resultante.
- La fecha en el documento aparece como DD/MM/YYYY. Conviértela a YYYY-MM-DD.
- El NIF del emisor (formato 8 dígitos + letra, o letra + 7 dígitos + control)
  debe extraerse VERBATIM sin modificación — el dígito de control es sensible.
- Si el proveedor tiene nombre comercial distinto de la razón social, devuelve
  la razón social asociada al NIF (la que aparece en datos de cabecera).

Estructura JSON esperada:
{{
  ""numero"": ""serie/ejercicio/secuencial"",
  ""fecha"": ""YYYY-MM-DD"",
  ""nif_emisor"": ""verbatim del documento"",
  ""razon_social_emisor"": ""razón social completa"",
  ""base_imponible"": 0.00,
  ""cuota_iva"": 0.00,
  ""tipo_iva"": 0.21,
  ""total"": 0.00
}}

Texto extraído de la factura:
{textoExtraidoPdf}"
End Function
$vbLabelText   $csharpLabel

El patrón de invocación del LLM en sí (autenticación, modelo, temperatura baja para resultados deterministas) sigue las convenciones habituales y no es específico del régimen ibérico — lo único que cambia es el prompt y el esquema JSON destino. Bajo LOPDGDD conviene preferir modelos de procesamiento local (despliegues on-premise de Llama 3, Mistral, Phi) sobre APIs externas, particularmente cuando la factura contiene datos personales del receptor (B2C) y la organización quiere minimizar la superficie de transferencia internacional del dato.

Bajo LOPDGDD, el procesamiento de facturas con datos personales del receptor (cuando el receptor es persona física) entra dentro de los tratamientos sometidos al reglamento. La elección entre API externa (OpenAI u otra) y modelo on-premise (Llama, modelos de Meta, Mistral en despliegue local) tiene implicaciones de protección de datos: la API externa implica transferencia internacional del dato en el momento del prompt; el modelo on-premise se ejecuta dentro del perímetro de la organización sin esa transferencia. La AEPD ha publicado guías sobre tratamiento con sistemas de IA y, dado su historial sancionador agresivo, conviene documentar formalmente la base jurídica y la valoración de impacto antes de desplegar el procesamiento con IA sobre facturas que contengan datos personales.


LOPDGDD y procesamiento on-premise de documentos

La Ley Orgánica 3/2018 de Protección de Datos Personales y Garantía de los Derechos Digitales (LOPDGDD) es la transposición ibérica del Reglamento General de Protección de Datos europeo. Su autoridad de control es la AEPD (Agencia Española de Protección de Datos), con sede en aepd.es. El detalle relevante para el desarrollador de software de facturación es que la AEPD mantiene, de forma consistente en los últimos años, un historial sancionador notoriamente más agresivo que las autoridades de control análogas de otros Estados miembros — Italia (Garante) y Francia (CNIL) sancionan con frecuencia, pero el ritmo y la cuantía media de las sanciones AEPD son superiores en relación con el tamaño de la economía española.

El historial sancionador de la AEPD como motor de procurement

Para el responsable de tratamiento ibérico, esta agresividad sancionadora se traduce en una preferencia muy clara hacia arquitecturas on-premise para el tratamiento documental sensible. Una factura emitida o recibida contiene datos identificativos del emisor y del receptor; cuando alguna de las partes es persona física (consumidor final, profesional autónomo, persona física en operaciones B2C), esos datos son personales en el sentido del RGPD/LOPDGDD. Su tratamiento por servicios externos en la nube — particularmente en jurisdicciones fuera del Espacio Económico Europeo — abre una superficie de exposición regulatoria que muchos sujetos pasivos ibéricos prefieren cerrar manteniendo el procesamiento dentro del perímetro de la organización.

IronPDF se ejecuta como biblioteca embebida en el proceso del sistema integrador. No envía documentos a servicios externos, no requiere conectividad a la red más allá del registro de licencia inicial, y no comparte datos con la cadena de suministro del software. Para arquitecturas en contenedor (Docker, Kubernetes, despliegues en eu-south-2 región Madrid o Spain Central de AWS y Azure respectivamente), la biblioteca opera dentro del contenedor sin requerir conexiones salientes. Esta característica, que para algunos casos es accesoria, es para el procurement ibérico una propiedad operativa fundamental.

El contraste con plataformas SaaS de facturación que procesan los documentos en la nube del proveedor es directo. Los flujos SaaS son perfectamente legítimos bajo LOPDGDD si se cumple la cadena de garantías de tratamiento (encargado del tratamiento, cláusulas contractuales, transferencias internacionales documentadas), pero la complejidad documental que la AEPD exige para validar esa cadena es no trivial. La opción componente embebido on-premise simplifica el ejercicio de cumplimiento: el dato no abandona el perímetro de la organización, y la responsabilidad del tratamiento se concentra en el sujeto pasivo.

Para usos avanzados, IronPDF se combina con IronOCR (otra biblioteca de la misma suite) cuando la factura entrante es un escaneo o imagen, manteniendo todo el procesamiento — incluido el reconocimiento óptico — dentro del entorno controlado del sujeto pasivo. Esto es relevante para receptores en sectores con sensibilidad LOPDGDD reforzada (sanidad bajo el régimen sectorial sanitario, banca y seguros bajo el marco prudencial financiero, sector público bajo las obligaciones de los esquemas nacionales de seguridad).

Por favor notaPara procesar facturas escaneadas o imágenes sin extracción textual directa, IronOCR — incluida en Iron Suite — se integra con IronPDF en el mismo proceso. Visite https://ironsoftware.com/csharp/ocr/ para detalles sobre extracción OCR con soporte para español, catalán, euskera y gallego, manteniendo el tratamiento en local bajo el régimen LOPDGDD.


Integración con ERPs en mercado ibérico

Cierre operativo: una vez generadas las facturas (emisión), o procesadas las entrantes (recepción), los datos deben fluir hacia el sistema contable y financiero correspondiente. El mercado ERP ibérico tiene particularidades — predominio significativo de SAP en gran empresa, Dynamics 365 con cuota creciente, software vertical local (Sage 200/Despachos, A3, Wolters Kluwer, Holded para pyme), plataformas de cumplimiento que actúan como capa intermedia (EDICOM, Sovos Group, Seres, Voxel) — que conviene reconocer al diseñar la capa de integración.

SAP Iberia, Dynamics 365 y las plataformas EDICOM/Sovos/Seres/Voxel

SAP es el ERP dominante en gran empresa española. Las grandes corporaciones (banca, energía, retail, manufactura) operan SAP S/4HANA o variantes ECC con localización ibérica (paquete país que cubre IVA español, retenciones IRPF, Modelo 303/390, etc.). La integración con SAP se realiza típicamente vía IDoc para flujo asíncrono o vía SAP RFC/BAPI/OData para acceso síncrono. La pieza Facturae se ubica en el extremo de salida (emisión) o entrada (recepción) y se conecta al módulo FI-AP (cuentas a pagar) o FI-AR (cuentas a cobrar) del SAP correspondiente. Microsoft Dynamics 365 (Business Central o Finance) cubre la franja media-alta del mercado, con conectores nativos hacia Office 365 y Power Platform que facilitan la lógica de transformación documental.

Las plataformas de cumplimiento ocupan una capa adicional que es específica del mercado ibérico de cumplimiento normativo. EDICOM (edicom.com/es), Sovos, Seres y Voxel ofrecen plataformas SaaS o instalables que actúan como hub entre el ERP del cliente y las múltiples administraciones (AEAT para VeriFactu y SII, Diputaciones Forales para TicketBAI, FACe para B2G, plataformas privadas autorizadas para Crea y Crece). El positioning del componente IronPDF dentro de este ecosistema es: el componente vive dentro del producto del cliente (sea su ERP, su software de facturación propio, o un adapter hacia la plataforma de cumplimiento), no dentro de la plataforma EDICOM o Sovos. Estas últimas son competidores en el espacio del sistema completo, no en el espacio del componente PDF.

using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

// Capa de integración genérica para enviar datos de factura a sistemas
// contables y de cumplimiento. Adapta las llamadas a la API en función de
// la plataforma concreta — SAP IDoc/BAPI, Dynamics 365, A3/Sage, o las
// plataformas EDICOM/Sovos/Seres/Voxel que actúan como hub frente a FACe.
public class IntegracionContable
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly string _baseUrl;

    public IntegracionContable(string apiKey, string baseUrl)
    {
        _apiKey = apiKey;
        _baseUrl = baseUrl;
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
    }

    // Envío hacia el módulo FI-AP de SAP a través de una pasarela REST.
    // En entornos productivos esta pasarela puede ser un middleware propio
    // o un conector como SAP Document and Reporting Compliance.
    public async Task EnviarASap(DatosFactura factura)
    {
        var carga = new
        {
            Proveedor = new { Nif = factura.NifEmisor },
            FechaContable = factura.FechaEmision,
            NumeroDocumento = factura.NumeroFactura,
            ImporteTotal = factura.ImporteTotal,
            CuentaContable = "400000"   // Proveedores nacionales en el plan PGC
        };
        await PostearApi("/sap/fi-ap/invoice", carga);
    }

    // Reenvío hacia una plataforma de cumplimiento (EDICOM/Sovos/Seres/Voxel)
    // que actúa como hub frente a AEAT (SII), FACe (B2G) y las Diputaciones
    // Forales (TicketBAI Bizkaia, Gipuzkoa, Araba) según corresponda al receptor.
    public async Task EnviarAPlataformaCumplimiento(DatosFactura factura)
    {
        var sobre = new
        {
            Tipo = "FACTURA_ENTRANTE",
            EmisorNif = factura.NifEmisor,
            Numero = factura.NumeroFactura,
            Fecha = factura.FechaEmision,
            Total = factura.ImporteTotal,
            Origen = "OCR_IRONPDF"      // trazabilidad de la fuente del dato
        };
        await PostearApi("/api/v2/recepciones", sobre);
    }

    // Helper genérico de POST con manejo de errores.
    private async Task PostearApi(string endpoint, object carga)
    {
        string json = JsonSerializer.Serialize(carga);
        var contenido = new StringContent(json, Encoding.UTF8, "application/json");

        var respuesta = await _httpClient.PostAsync($"{_baseUrl}{endpoint}", contenido);

        if (!respuesta.IsSuccessStatusCode)
        {
            string error = await respuesta.Content.ReadAsStringAsync();
            throw new Exception($"Error API: {respuesta.StatusCode} - {error}");
        }

        Console.WriteLine($"Publicado correctamente en {endpoint}");
    }
}
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

// Capa de integración genérica para enviar datos de factura a sistemas
// contables y de cumplimiento. Adapta las llamadas a la API en función de
// la plataforma concreta — SAP IDoc/BAPI, Dynamics 365, A3/Sage, o las
// plataformas EDICOM/Sovos/Seres/Voxel que actúan como hub frente a FACe.
public class IntegracionContable
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly string _baseUrl;

    public IntegracionContable(string apiKey, string baseUrl)
    {
        _apiKey = apiKey;
        _baseUrl = baseUrl;
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
    }

    // Envío hacia el módulo FI-AP de SAP a través de una pasarela REST.
    // En entornos productivos esta pasarela puede ser un middleware propio
    // o un conector como SAP Document and Reporting Compliance.
    public async Task EnviarASap(DatosFactura factura)
    {
        var carga = new
        {
            Proveedor = new { Nif = factura.NifEmisor },
            FechaContable = factura.FechaEmision,
            NumeroDocumento = factura.NumeroFactura,
            ImporteTotal = factura.ImporteTotal,
            CuentaContable = "400000"   // Proveedores nacionales en el plan PGC
        };
        await PostearApi("/sap/fi-ap/invoice", carga);
    }

    // Reenvío hacia una plataforma de cumplimiento (EDICOM/Sovos/Seres/Voxel)
    // que actúa como hub frente a AEAT (SII), FACe (B2G) y las Diputaciones
    // Forales (TicketBAI Bizkaia, Gipuzkoa, Araba) según corresponda al receptor.
    public async Task EnviarAPlataformaCumplimiento(DatosFactura factura)
    {
        var sobre = new
        {
            Tipo = "FACTURA_ENTRANTE",
            EmisorNif = factura.NifEmisor,
            Numero = factura.NumeroFactura,
            Fecha = factura.FechaEmision,
            Total = factura.ImporteTotal,
            Origen = "OCR_IRONPDF"      // trazabilidad de la fuente del dato
        };
        await PostearApi("/api/v2/recepciones", sobre);
    }

    // Helper genérico de POST con manejo de errores.
    private async Task PostearApi(string endpoint, object carga)
    {
        string json = JsonSerializer.Serialize(carga);
        var contenido = new StringContent(json, Encoding.UTF8, "application/json");

        var respuesta = await _httpClient.PostAsync($"{_baseUrl}{endpoint}", contenido);

        if (!respuesta.IsSuccessStatusCode)
        {
            string error = await respuesta.Content.ReadAsStringAsync();
            throw new Exception($"Error API: {respuesta.StatusCode} - {error}");
        }

        Console.WriteLine($"Publicado correctamente en {endpoint}");
    }
}
Imports System
Imports System.Net.Http
Imports System.Text
Imports System.Text.Json
Imports System.Threading.Tasks

' Capa de integración genérica para enviar datos de factura a sistemas
' contables y de cumplimiento. Adapta las llamadas a la API en función de
' la plataforma concreta — SAP IDoc/BAPI, Dynamics 365, A3/Sage, o las
' plataformas EDICOM/Sovos/Seres/Voxel que actúan como hub frente a FACe.
Public Class IntegracionContable
    Private ReadOnly _httpClient As HttpClient
    Private ReadOnly _apiKey As String
    Private ReadOnly _baseUrl As String

    Public Sub New(apiKey As String, baseUrl As String)
        _apiKey = apiKey
        _baseUrl = baseUrl
        _httpClient = New HttpClient()
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}")
    End Sub

    ' Envío hacia el módulo FI-AP de SAP a través de una pasarela REST.
    ' En entornos productivos esta pasarela puede ser un middleware propio
    ' o un conector como SAP Document and Reporting Compliance.
    Public Async Function EnviarASap(factura As DatosFactura) As Task
        Dim carga = New With {
            .Proveedor = New With {.Nif = factura.NifEmisor},
            .FechaContable = factura.FechaEmision,
            .NumeroDocumento = factura.NumeroFactura,
            .ImporteTotal = factura.ImporteTotal,
            .CuentaContable = "400000"   ' Proveedores nacionales en el plan PGC
        }
        Await PostearApi("/sap/fi-ap/invoice", carga)
    End Function

    ' Reenvío hacia una plataforma de cumplimiento (EDICOM/Sovos/Seres/Voxel)
    ' que actúa como hub frente a AEAT (SII), FACe (B2G) y las Diputaciones
    ' Forales (TicketBAI Bizkaia, Gipuzkoa, Araba) según corresponda al receptor.
    Public Async Function EnviarAPlataformaCumplimiento(factura As DatosFactura) As Task
        Dim sobre = New With {
            .Tipo = "FACTURA_ENTRANTE",
            .EmisorNif = factura.NifEmisor,
            .Numero = factura.NumeroFactura,
            .Fecha = factura.FechaEmision,
            .Total = factura.ImporteTotal,
            .Origen = "OCR_IRONPDF"      ' trazabilidad de la fuente del dato
        }
        Await PostearApi("/api/v2/recepciones", sobre)
    End Function

    ' Helper genérico de POST con manejo de errores.
    Private Async Function PostearApi(endpoint As String, carga As Object) As Task
        Dim json As String = JsonSerializer.Serialize(carga)
        Dim contenido = New StringContent(json, Encoding.UTF8, "application/json")

        Dim respuesta = Await _httpClient.PostAsync($"{_baseUrl}{endpoint}", contenido)

        If Not respuesta.IsSuccessStatusCode Then
            Dim error As String = Await respuesta.Content.ReadAsStringAsync()
            Throw New Exception($"Error API: {respuesta.StatusCode} - {error}")
        End If

        Console.WriteLine($"Publicado correctamente en {endpoint}")
    End Function
End Class
$vbLabelText   $csharpLabel

QuickBooks y Xero son referencias internacionales útiles para ilustrar el patrón. En el mercado ibérico real, los integradores adaptan este patrón hacia SAP (via IDoc INVOIC, BAPI BAPI_INCOMINGINVOICE_CREATE para AP, o el módulo SAP Document and Reporting Compliance para flujos Facturae), Dynamics 365 (con conectores Power Platform o directamente vía API), y Sage/A3/Holded mediante sus APIs específicas. La capa intermedia EDICOM/Sovos/Seres/Voxel se integra típicamente como un servicio adicional que el ERP llama tras la emisión local — el patrón es: ERP genera factura → IronPDF renderiza PDF/A-3 híbrido → conector envía PDF y XML estructurado a la plataforma de cumplimiento → plataforma de cumplimiento remite a FACe / AEAT / Diputación Foral según corresponda.

Procesamiento por lotes con concurrencia controlada

El cierre mensual o trimestral en una organización mediana implica el procesamiento simultáneo de cientos o miles de facturas — tanto emitidas como recibidas. La concurrencia debe controlarse para no saturar los servicios externos (la API de FACe tiene límites de tarifa; las APIs de las plataformas EDICOM/Sovos también; SAP tiene umbrales de carga del módulo FI) y para no agotar los recursos del propio proceso de cliente. El patrón habitual usa SemaphoreSlim para limitar la concurrencia a un valor configurable:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

// Resultado del procesamiento de una factura individual dentro de un lote.
public class ResultadoLote
{
    public string Ruta { get; set; }
    public bool Exito { get; set; }
    public string NumeroFactura { get; set; }
    public string Error { get; set; }
}

// Procesador de facturas para cierre mensual o trimestral ibérico. Controla
// la concurrencia para no saturar las plataformas externas (FACe, AEAT,
// EDICOM/Sovos/Seres) y respeta los plazos SII de cuatro días naturales.
public class ProcesadorLoteFacturas
{
    private readonly ExtractorFacturae _extractor;
    private readonly IntegracionContable _integracion;
    private readonly int _concurrenciaMaxima;

    public ProcesadorLoteFacturas(string clavePlataforma, string urlPlataforma,
        int concurrenciaMaxima = 5)
    {
        _extractor = new ExtractorFacturae();
        _integracion = new IntegracionContable(clavePlataforma, urlPlataforma);
        _concurrenciaMaxima = concurrenciaMaxima;   // ajustar según límites del proveedor
    }

    // Procesa múltiples facturas en paralelo con concurrencia controlada.
    // Aplica back-pressure mediante semáforo para no saturar el hub destino.
    public async Task<List<ResultadoLote>> ProcesarLote(List<string> rutas)
    {
        // Bolsa concurrente para acumular resultados desde tareas paralelas.
        var resultados = new ConcurrentBag<ResultadoLote>();

        // El semáforo limita cuántas facturas se procesan a la vez —
        // típicamente 3 a 8 según el plan contratado con el hub.
        var semaforo = new SemaphoreSlim(_concurrenciaMaxima);

        var tareas = rutas.Select(async ruta =>
        {
            await semaforo.WaitAsync();
            try
            {
                var resultado = await ProcesarFacturaIndividual(ruta);
                resultados.Add(resultado);
            }
            finally
            {
                // Libera el hueco para que la siguiente factura entre al pool.
                semaforo.Release();
            }
        });

        await Task.WhenAll(tareas);

        var lista = resultados.ToList();
        int correctos = lista.Count(r => r.Exito);
        int fallidos = lista.Count(r => !r.Exito);

        Console.WriteLine($"\nLote completado:");
        Console.WriteLine($"  Total: {lista.Count}");
        Console.WriteLine($"  Correctos: {correctos}");
        Console.WriteLine($"  Con error: {fallidos}");

        return lista;
    }

    // Procesa una factura individual: extrae el XML Facturae y la reenvía
    // a la plataforma de cumplimiento o al ERP destino.
    private async Task<ResultadoLote> ProcesarFacturaIndividual(string rutaPdf)
    {
        try
        {
            Console.WriteLine($"Procesando: {rutaPdf}");

            var facturae = _extractor.ExtraerFacturae(rutaPdf);
            // Mapea el XML Facturae a la forma DatosFactura del integrador.
            var datos = MapearFacturaeADatos(facturae);
            await _integracion.EnviarASap(datos);

            Console.WriteLine($"OK: {datos.NumeroFactura}");

            return new ResultadoLote
            {
                Ruta = rutaPdf,
                Exito = true,
                NumeroFactura = datos.NumeroFactura
            };
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Fallo: {rutaPdf}");
            return new ResultadoLote
            {
                Ruta = rutaPdf,
                Exito = false,
                Error = ex.Message
            };
        }
    }

    private DatosFactura MapearFacturaeADatos(System.Xml.Linq.XDocument facturae)
    {
        // Mapea los elementos del XML Facturae 3.2.2 a la forma DatosFactura
        // del integrador, navegando contra el esquema CIUS-ES sobre EN 16931.
        return new DatosFactura();
    }
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

// Resultado del procesamiento de una factura individual dentro de un lote.
public class ResultadoLote
{
    public string Ruta { get; set; }
    public bool Exito { get; set; }
    public string NumeroFactura { get; set; }
    public string Error { get; set; }
}

// Procesador de facturas para cierre mensual o trimestral ibérico. Controla
// la concurrencia para no saturar las plataformas externas (FACe, AEAT,
// EDICOM/Sovos/Seres) y respeta los plazos SII de cuatro días naturales.
public class ProcesadorLoteFacturas
{
    private readonly ExtractorFacturae _extractor;
    private readonly IntegracionContable _integracion;
    private readonly int _concurrenciaMaxima;

    public ProcesadorLoteFacturas(string clavePlataforma, string urlPlataforma,
        int concurrenciaMaxima = 5)
    {
        _extractor = new ExtractorFacturae();
        _integracion = new IntegracionContable(clavePlataforma, urlPlataforma);
        _concurrenciaMaxima = concurrenciaMaxima;   // ajustar según límites del proveedor
    }

    // Procesa múltiples facturas en paralelo con concurrencia controlada.
    // Aplica back-pressure mediante semáforo para no saturar el hub destino.
    public async Task<List<ResultadoLote>> ProcesarLote(List<string> rutas)
    {
        // Bolsa concurrente para acumular resultados desde tareas paralelas.
        var resultados = new ConcurrentBag<ResultadoLote>();

        // El semáforo limita cuántas facturas se procesan a la vez —
        // típicamente 3 a 8 según el plan contratado con el hub.
        var semaforo = new SemaphoreSlim(_concurrenciaMaxima);

        var tareas = rutas.Select(async ruta =>
        {
            await semaforo.WaitAsync();
            try
            {
                var resultado = await ProcesarFacturaIndividual(ruta);
                resultados.Add(resultado);
            }
            finally
            {
                // Libera el hueco para que la siguiente factura entre al pool.
                semaforo.Release();
            }
        });

        await Task.WhenAll(tareas);

        var lista = resultados.ToList();
        int correctos = lista.Count(r => r.Exito);
        int fallidos = lista.Count(r => !r.Exito);

        Console.WriteLine($"\nLote completado:");
        Console.WriteLine($"  Total: {lista.Count}");
        Console.WriteLine($"  Correctos: {correctos}");
        Console.WriteLine($"  Con error: {fallidos}");

        return lista;
    }

    // Procesa una factura individual: extrae el XML Facturae y la reenvía
    // a la plataforma de cumplimiento o al ERP destino.
    private async Task<ResultadoLote> ProcesarFacturaIndividual(string rutaPdf)
    {
        try
        {
            Console.WriteLine($"Procesando: {rutaPdf}");

            var facturae = _extractor.ExtraerFacturae(rutaPdf);
            // Mapea el XML Facturae a la forma DatosFactura del integrador.
            var datos = MapearFacturaeADatos(facturae);
            await _integracion.EnviarASap(datos);

            Console.WriteLine($"OK: {datos.NumeroFactura}");

            return new ResultadoLote
            {
                Ruta = rutaPdf,
                Exito = true,
                NumeroFactura = datos.NumeroFactura
            };
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Fallo: {rutaPdf}");
            return new ResultadoLote
            {
                Ruta = rutaPdf,
                Exito = false,
                Error = ex.Message
            };
        }
    }

    private DatosFactura MapearFacturaeADatos(System.Xml.Linq.XDocument facturae)
    {
        // Mapea los elementos del XML Facturae 3.2.2 a la forma DatosFactura
        // del integrador, navegando contra el esquema CIUS-ES sobre EN 16931.
        return new DatosFactura();
    }
}
Imports System
Imports System.Collections.Concurrent
Imports System.Collections.Generic
Imports System.Linq
Imports System.Threading
Imports System.Threading.Tasks

' Resultado del procesamiento de una factura individual dentro de un lote.
Public Class ResultadoLote
    Public Property Ruta As String
    Public Property Exito As Boolean
    Public Property NumeroFactura As String
    Public Property Error As String
End Class

' Procesador de facturas para cierre mensual o trimestral ibérico. Controla
' la concurrencia para no saturar las plataformas externas (FACe, AEAT,
' EDICOM/Sovos/Seres) y respeta los plazos SII de cuatro días naturales.
Public Class ProcesadorLoteFacturas
    Private ReadOnly _extractor As ExtractorFacturae
    Private ReadOnly _integracion As IntegracionContable
    Private ReadOnly _concurrenciaMaxima As Integer

    Public Sub New(clavePlataforma As String, urlPlataforma As String, Optional concurrenciaMaxima As Integer = 5)
        _extractor = New ExtractorFacturae()
        _integracion = New IntegracionContable(clavePlataforma, urlPlataforma)
        _concurrenciaMaxima = concurrenciaMaxima ' ajustar según límites del proveedor
    End Sub

    ' Procesa múltiples facturas en paralelo con concurrencia controlada.
    ' Aplica back-pressure mediante semáforo para no saturar el hub destino.
    Public Async Function ProcesarLote(rutas As List(Of String)) As Task(Of List(Of ResultadoLote))
        ' Bolsa concurrente para acumular resultados desde tareas paralelas.
        Dim resultados = New ConcurrentBag(Of ResultadoLote)()

        ' El semáforo limita cuántas facturas se procesan a la vez —
        ' típicamente 3 a 8 según el plan contratado con el hub.
        Dim semaforo = New SemaphoreSlim(_concurrenciaMaxima)

        Dim tareas = rutas.Select(Async Function(ruta)
                                      Await semaforo.WaitAsync()
                                      Try
                                          Dim resultado = Await ProcesarFacturaIndividual(ruta)
                                          resultados.Add(resultado)
                                      Finally
                                          ' Libera el hueco para que la siguiente factura entre al pool.
                                          semaforo.Release()
                                      End Try
                                  End Function)

        Await Task.WhenAll(tareas)

        Dim lista = resultados.ToList()
        Dim correctos = lista.Count(Function(r) r.Exito)
        Dim fallidos = lista.Count(Function(r) Not r.Exito)

        Console.WriteLine(vbCrLf & "Lote completado:")
        Console.WriteLine($"  Total: {lista.Count}")
        Console.WriteLine($"  Correctos: {correctos}")
        Console.WriteLine($"  Con error: {fallidos}")

        Return lista
    End Function

    ' Procesa una factura individual: extrae el XML Facturae y la reenvía
    ' a la plataforma de cumplimiento o al ERP destino.
    Private Async Function ProcesarFacturaIndividual(rutaPdf As String) As Task(Of ResultadoLote)
        Try
            Console.WriteLine($"Procesando: {rutaPdf}")

            Dim facturae = _extractor.ExtraerFacturae(rutaPdf)
            ' Mapea el XML Facturae a la forma DatosFactura del integrador.
            Dim datos = MapearFacturaeADatos(facturae)
            Await _integracion.EnviarASap(datos)

            Console.WriteLine($"OK: {datos.NumeroFactura}")

            Return New ResultadoLote With {
                .Ruta = rutaPdf,
                .Exito = True,
                .NumeroFactura = datos.NumeroFactura
            }
        Catch ex As Exception
            Console.WriteLine($"Fallo: {rutaPdf}")
            Return New ResultadoLote With {
                .Ruta = rutaPdf,
                .Exito = False,
                .Error = ex.Message
            }
        End Try
    End Function

    Private Function MapearFacturaeADatos(facturae As System.Xml.Linq.XDocument) As DatosFactura
        ' Mapea los elementos del XML Facturae 3.2.2 a la forma DatosFactura
        ' del integrador, navegando contra el esquema CIUS-ES sobre EN 16931.
        Return New DatosFactura()
    End Function
End Class
$vbLabelText   $csharpLabel

El SemaphoreSlim garantiza que no se sature ningún servicio externo ni se agoten los recursos del sistema. Para el contexto ibérico, ajustar _maxConcurrency requiere conocer los límites concretos de la plataforma de cumplimiento utilizada (EDICOM, Sovos, Seres) o de la API de FACe — la documentación pública de FACe fija ventanas de envío que conviene respetar para no provocar rechazos. Para flujos SII (Suministro Inmediato de Información), los plazos son de cuatro días naturales (con calendario laboral) tras la emisión o recepción, lo que implica que el procesamiento por lotes no puede acumular más allá de esa ventana sin riesgo de incumplimiento.


Próximos pasos

La generación y procesamiento de facturas conformes con el conjunto regulatorio ibérico — VeriFactu, TicketBAI en sus tres variantes forales, NaTicket en Navarra, Crea y Crece, Facturae sobre FACe, SII y todo ello bajo el régimen LOPDGDD de la AEPD — es uno de los terrenos técnicos con mayor especificidad nacional dentro del ámbito europeo. IronPDF cubre la capa visual, la incrustación PDF/A-3 + Facturae del patrón híbrido, la firma PAdES sobre el PDF visualizable, y el procesamiento de facturas entrantes mediante extracción del Facturae embebido o de texto cuando el patrón híbrido no está disponible. Las piezas adicionales — firma XAdES sobre XML TicketBAI, gestión del libro de eventos VeriFactu, remisión a la AEAT, conectores hacia FACe y hacia plataformas EDICOM/Sovos/Seres/Voxel — viven dentro del producto del ISV o del integrador y se apoyan en el motor PDF como componente.

Para profundizar en aspectos específicos del flujo: el tutorial generar PDFs desde HTML cubre el motor de renderizado base; el tutorial archivado PDF/A entra en el detalle del patrón híbrido PDF/A-3 + Facturae XML embebido; el tutorial firmas digitales en PDF explora PAdES con certificados FNMT-RCM y eIDAS; el tutorial formularios PDF cubre el procesamiento de Modelos AEAT (303, 390, etc.) en formato PDF AcroForm.

IronPDF es la base sobre la que se monta el cumplimiento PDF español: renderizado Chrome con precisión de píxel sobre HTML/CSS, extracción de texto y adjuntos sin OCR para Facturae embebido, firma PAdES con certificados cualificados, conversión a PDF/A-3 para el patrón híbrido, y adjuntos arbitrarios para la incrustación del XML estructurado. La biblioteca opera on-premise dentro del perímetro de la organización, alineada con la preferencia ibérica por arquitecturas que minimicen exposición LOPDGDD bajo el régimen sancionador AEPD.

¿Listo para empezar? Descargue IronPDF y pruébelo con una versión de prueba gratuita de 30 días. La biblioteca incluye una licencia de desarrollo gratuita que permite evaluar la generación VeriFactu conforme, el patrón híbrido Facturae+PDF/A-3, la firma PAdES con certificado FNMT-RCM y la integración con su ERP ibérico antes de comprometerse con una licencia de producción. Si tiene dudas sobre la integración con el conjunto regulatorio español o sobre la sustitución de iText bajo el régimen sancionador VeriFactu, contacte con nuestro equipo de soporte técnico.

Preguntas Frecuentes

¿Qué obligaciones impone VeriFactu al desarrollador de software de facturación?

VeriFactu, bajo el RDL 15/2025, obliga al software de facturación a generar registros encadenados criptográficamente mediante una `huella` que enlaza cada registro con el anterior. Cada visualización de factura debe incluir el QR de la AEAT y la leyenda mandatoria `VERI*FACTU` (con asterisco en medio) o `Factura verificable en la sede electrónica de la AEAT`. El régimen sancionador apunta directamente al fabricante de software, con multas de hasta 150 000 €/año por comercializar productos no conformes — peculiaridad rara globalmente que cambia la responsabilidad de la cadena de suministro del software.

¿Cómo se firma un ticket TicketBAI en Bizkaia, Gipuzkoa y Araba con IronPDF?

TicketBAI exige firma XAdES encadenada sobre el XML del ticket, gestionada por una biblioteca de firma XML específica con certificado cualificado (típicamente FNMT-RCM o IZENPE) — IronPDF no realiza la firma XAdES propiamente dicha. IronPDF cubre la generación del PDF de visualización del ticket que recibe el cliente final, y opcionalmente la firma PAdES sobre ese PDF como capa adicional de no repudio. Cada Diputación Foral (Bizkaia, Gipuzkoa, Araba) tiene su propia plataforma y esquema XSD, por lo que la integración debe contemplar la fragmentación regional sin tratar TicketBAI como sistema monolítico.

¿Qué es el patrón híbrido PDF/A-3 + Facturae bajo Crea y Crece?

El patrón híbrido consiste en un único archivo PDF/A-3 que contiene la visualización humana de la factura y, embebido como anexo, el XML Facturae 3.2.2 con los mismos datos estructurados. PDF/A-3 (ISO 19005-3) es el primer perfil PDF/A que admite normativamente la incrustación de archivos arbitrarios. Para el receptor bajo Crea y Crece (B2B, mandato 2027/2028), elimina la necesidad de OCR — la extracción se reduce a leer el anexo Facturae.xml. IronPDF genera el PDF/A-3 desde HTML y embebe el XML como anexo con el nombre convencional.

¿Por qué IronPDF es alternativa a iText bajo el régimen sancionador VeriFactu?

iText se distribuye bajo doble licencia: comercial de pago o AGPLv3 gratuita. La distribución bajo AGPL obliga a publicar el código fuente derivado del producto completo, incompatible con el modelo de negocio del ISV ibérico de software de facturación que vende precisamente esa propiedad intelectual. Bajo VeriFactu, el régimen sancionador apunta al fabricante de software, y un componente PDF con licenciamiento incompatible introduce zona gris en la trazabilidad de la cadena. IronPDF se distribuye bajo licenciamiento comercial royalty-free y previsible, sin componente AGPL, simplificando el `due diligence` de procurement.

¿Cómo se integra el QR obligatorio de la AEAT en un PDF generado con IronPDF?

Toda visualización de factura conforme con VeriFactu —factura completa o factura simplificada— debe incluir un código QR generado conforme a la especificación de la AEAT con el payload de verificación (URL de la sede electrónica, datos identificativos mínimos del documento, identificador del registro). IronPDF se combina con IronQR para generar el código y embeberlo en el HTML antes del renderizado, junto con la leyenda textual mandatoria `VERI*FACTU` (preservando el asterisco intermedio) o `Factura verificable en la sede electrónica de la AEAT`. Cualquier paráfrasis invalida el cumplimiento formal.

¿Qué motiva la preferencia ibérica por procesamiento on-premise bajo LOPDGDD?

La AEPD (Agencia Española de Protección de Datos) mantiene un historial sancionador notoriamente más agresivo que las autoridades de control análogas europeas (Garante italiano, CNIL francesa). El tratamiento de facturas con datos personales del receptor —cuando éste es persona física o profesional autónomo— entra dentro de los tratamientos sometidos al RGPD/LOPDGDD. Los responsables ibéricos prefieren arquitecturas on-premise (componentes embebidos en el proceso del integrador, sin envío de documentos a servicios externos en la nube) para minimizar la superficie de exposición regulatoria. IronPDF opera dentro del contenedor del integrador sin requerir conectividad saliente.

¿Cómo se diferencia el SII español del SII chileno en contenido cross-jurisdicción?

SII en España significa Suministro Inmediato de Información — régimen de envío casi en tiempo real de los libros registro de IVA a la AEAT, aplicable a contribuyentes de cierto tamaño. SII en Chile significa Servicio de Impuestos Internos — la autoridad tributaria nacional. En contenido exclusivamente ibérico, `SII` sin cualificación refiere al sistema español. En contenido cross-jurisdicción, conviene cualificar en primera mención: `SII (España)` frente a `SII Chile`. La confusión es común porque ambos términos tienen tradición de uso paralelo en sus respectivos marcos.

¿Qué papel juegan EDICOM, Sovos, Seres y Voxel frente a IronPDF?

Estas plataformas de cumplimiento actúan como hubs intermedios entre el ERP del cliente y las múltiples administraciones (AEAT para VeriFactu y SII, Diputaciones Forales para TicketBAI, FACe para B2G, plataformas privadas autorizadas para Crea y Crece B2B). Son competidores en el espacio del sistema completo de cumplimiento, no en el espacio del componente PDF. IronPDF vive dentro del producto del cliente o del adapter que se conecta a estas plataformas — el patrón habitual es ERP genera factura → IronPDF renderiza PDF/A-3 híbrido con Facturae embebido → conector envía a la plataforma de cumplimiento → plataforma remite a AEAT/FACe/Diputación Foral según corresponda.

Curtis Chau
Escritor Técnico

Curtis Chau tiene una licenciatura en Ciencias de la Computación (Carleton University) y se especializa en el desarrollo front-end con experiencia en Node.js, TypeScript, JavaScript y React. Apasionado por crear interfaces de usuario intuitivas y estéticamente agradables, disfruta trabajando con frameworks modernos y creando manuales bien ...

Leer más
¿Listo para empezar?
Nuget Descargas 18,926,724 | Versión: 2026.5 just released
Still Scrolling Icon

¿Aún desplazándote?

¿Quieres una prueba rápida? PM > Install-Package IronPdf
ejecutar una muestra Mira cómo tu HTML se convierte en PDF.