Documentation Index
Fetch the complete documentation index at: https://mintlify.com/mdiago/VeriFactu/llms.txt
Use this file to discover all available pages before exploring further.
The AEAT mandates (Artículo 16, Orden HAC/1177/2024) that billing systems respect wait times between submissions and batch at most 1,000 records per request. InvoiceQueue implements this contract automatically in a background thread, so your application can enqueue records at any rate without manually managing timing or batch sizes.
Cómo funciona la cola
InvoiceQueue inherits from IntervalWorker, which ticks every 5 seconds on a dedicated background thread. On each tick it inspects the per-seller queues and decides whether to submit:
| Trigger | Condition |
|---|
| Límite de registros | 1,000 or more records have accumulated for a seller since the last submission. |
| Tiempo de espera superado | The wait interval communicated by the AEAT in the previous response has elapsed. |
The default initial wait is 60 seconds. After every successful submission the AEAT returns TiempoEsperaEnvio in its response; this value replaces the current wait for that seller’s queue until the next response arrives.
Internally, records are grouped by seller NIF into individual SellerQueue instances, each maintaining its own watermark and timer. InvoiceQueue iterates over all active SellerQueue instances on every tick, calling SellerQueue.Process() on any that are ready.
Key objects
InvoiceQueue.ActiveInvoiceQueue — the singleton background queue, created and started automatically when the class is first referenced. Only one instance can exist at a time.
InvoiceQueue.Add(InvoiceAction) — enqueues an InvoiceEntry (alta) or InvoiceCancellation (anulación). Throws if the item is already posted, has validation errors, or if Exit() has been called.
InvoiceQueue.Count — returns the total number of records pending across all seller queues.
InvoiceQueue.Exit() — static method that signals a graceful shutdown. If the queue is already empty it stops the background thread immediately; if records are still pending, the flag is set and the thread shuts down automatically after the last batch is delivered.
InvoiceQueue.SentFinished — optional Action<List<InvoiceAction>, RespuestaRegFactuSistemaFacturacion> callback invoked after each successful batch.
InvoiceQueue.SentError — optional Action<List<InvoiceAction>, Exception> callback invoked when a batch submission throws.
Ejemplo completo
The following example enqueues 1,005 registration records and then 10 cancellation records for the same seller. With logging enabled, you can observe how the queue performs exactly two submissions: the first when the 1,000-record batch limit is reached, and the second approximately 60 seconds later (after the AEAT wait period elapses) for the remaining 15 records.
// Activo el log
Settings.Current.LoggingEnabled = true;
// Deshabilito la validación de NIF en línea con la AEAT
Settings.Current.SkipNifAeatValidation = true;
// Deshabilito la validación de NIFs intracomunitarios
Settings.Current.SkipViesVatNumberValidation = true;
var testId = "08";
int start = 0;
// Añado 1.005 registros de alta
for (int i = 1; i < 1006; i++)
{
var invoice = new Invoice($"TEST{testId}" + $"{start + i}".PadLeft(8, '0'), new DateTime(2024, 10, 29), "B10795649")
{
InvoiceType = TipoFactura.F1,
SellerName = "KIVU SOLUTIONS SL",
BuyerID = "B44531218",
BuyerName = "WEFINZ SOLUTIONS SL",
Text = "PRESTACION SERVICIOS DESARROLLO SOFTWARE",
TaxItems = new List<TaxItem>() {
new TaxItem()
{
TaxScheme = ClaveRegimen.RegimenGeneral,
TaxType = CalificacionOperacion.S1,
TaxRate = 4,
TaxBase = 10,
TaxAmount = 0.4m
},
new TaxItem()
{
TaxScheme = ClaveRegimen.RegimenGeneral,
TaxType = CalificacionOperacion.S1,
TaxRate = 21,
TaxBase = 100,
TaxAmount = 21
}
}
};
var invoiceEntry = new InvoiceEntry(invoice);
InvoiceQueue.ActiveInvoiceQueue.Add(invoiceEntry);
}
// Añado la cancelación de los primeros 10 registros
for (int i = 1; i < 11; i++)
{
var invoice = new Invoice($"TEST{testId}" + $"{start + i}".PadLeft(8, '0'), new DateTime(2024, 10, 29), "B10795649")
{
InvoiceType = TipoFactura.F1,
SellerName = "KIVU SOLUTIONS SL",
BuyerID = "B44531218",
BuyerName = "WEFINZ SOLUTIONS SL",
Text = "PRESTACION SERVICIOS DESARROLLO SOFTWARE",
TaxItems = new List<TaxItem>() {
new TaxItem()
{
TaxScheme = ClaveRegimen.RegimenGeneral,
TaxType = CalificacionOperacion.S1,
TaxRate = 4,
TaxBase = 10,
TaxAmount = 0.4m
},
new TaxItem()
{
TaxScheme = ClaveRegimen.RegimenGeneral,
TaxType = CalificacionOperacion.S1,
TaxRate = 21,
TaxBase = 100,
TaxAmount = 21
}
}
};
var invoiceCancellation = new InvoiceCancellation(invoice);
InvoiceQueue.ActiveInvoiceQueue.Add(invoiceCancellation);
}
With logging enabled the output will look like this. Line [000001] shows the queue firing because the 1,000-record batch threshold was reached. After the AEAT responds (line [000006]), the next execution window is set 60 seconds into the future. At [000007] the timer expires and the remaining 15 records are sent.
[000001] 2024-11-15 17:31:48: Ejecutando por cola (B10795649) tras tiempo espera en segundos: 60 desde 01/01/0001 0:00:00 hasta 01/01/0001 0:01:00
[000002] 2024-11-15 17:31:48: Actualizando datos de la cadena de bloques (B10795649) en 1000 elementos 15/11/2024 17:31:48
[000003] 2024-11-15 17:31:49: Finalizada actualización de datos de la cadena de bloques en 1000 elementos 15/11/2024 17:31:49
[000004] 2024-11-15 17:31:49: Enviando datos a la AEAT B10795649 de 1000 elementos 15/11/2024 17:31:49
[000005] 2024-11-15 17:31:52: Finalizado envío de datos B10795649 a la AEAT de 1000 elementos (quedan 15 registros) 15/11/2024 17:31:52
[000006] 2024-11-15 17:31:52: Establecido momento próxima ejecución B10795649 (LastProcessMoment: 15/11/2024 17:31:52 + CurrentWaitSecods: 60) = 15/11/2024 17:32:52
[000007] 2024-11-15 17:32:53: Ejecutando por cola (B10795649) tras tiempo espera en segundos: 60 desde 15/11/2024 17:31:52 hasta 15/11/2024 17:32:52
[000008] 2024-11-15 17:32:53: Actualizando datos de la cadena de bloques (B10795649) en 15 elementos 15/11/2024 17:32:53
[000009] 2024-11-15 17:32:53: Finalizada actualización de datos de la cadena de bloques en 15 elementos 15/11/2024 17:32:53
[000010] 2024-11-15 17:32:53: Enviando datos a la AEAT B10795649 de 15 elementos 15/11/2024 17:32:53
[000011] 2024-11-15 17:32:53: Finalizado envío de datos B10795649 a la AEAT de 15 elementos (quedan 0 registros) 15/11/2024 17:32:53
[000012] 2024-11-15 17:32:53: Establecido momento próxima ejecución B10795649 (LastProcessMoment: 15/11/2024 17:32:53 + CurrentWaitSecods: 60) = 15/11/2024 17:33:53
Cerrar la cola correctamente
InvoiceQueue runs on a background thread that is independent of your application’s main thread. Before your application exits, you must signal the queue to shut down. Exit() is safe to call at any time — if Count is already zero it will immediately call End() on the underlying IntervalWorker and set ActiveInvoiceQueue to null; if records are still pending it sets a closing flag and the background thread finishes the shutdown automatically once the queue drains.
// Cerramos la cola asegurándonos previamente de que no queda nada pendiente
if (InvoiceQueue.ActiveInvoiceQueue.Count == 0)
InvoiceQueue.Exit();
Always call InvoiceQueue.Exit() before your application terminates. Because the queue runs on a non-foreground background thread, .NET may allow the process to exit while records are still pending. Exit() sets a closing flag and, if the queue is already empty, shuts the background thread down immediately. If records are still pending, the background thread continues processing and calls DoExit() automatically once the queue drains to zero. After Exit() is called, any subsequent call to Add() throws an InvalidOperationException.
Propiedades y miembros de InvoiceQueue
| Miembro | Tipo | Descripción |
|---|
ActiveInvoiceQueue | static InvoiceQueue | Singleton. The single active queue instance for the process. Created and started automatically on first access. |
Count | int | Total number of records currently pending across all seller queues. |
Add(InvoiceAction) | void | Enqueues an InvoiceEntry or InvoiceCancellation. Validates the record and throws on errors. |
Exit() | static void | Signals graceful shutdown. If the queue is empty it stops immediately; if records are pending, shutdown is deferred until the queue drains. Subsequent Add() calls throw. |
SentFinished | static Action<List<InvoiceAction>, RespuestaRegFactuSistemaFacturacion> | Optional callback fired after each successful batch submission. |
SentError | static Action<List<InvoiceAction>, Exception> | Optional callback fired when a batch submission throws an exception. |
Cuándo usar InvoiceQueue vs InvoiceEntry.Save()
| Criterio | InvoiceEntry.Save() | InvoiceQueue |
|---|
| Execution model | Synchronous, blocks the calling thread until the AEAT responds. | Asynchronous; records are processed in a dedicated background thread. |
| Batching | Single record per HTTP request. | Up to 1,000 records per request, automatically batched. |
| Flow control | Manual — caller must implement wait logic between calls. | Automatic — respects TiempoEsperaEnvio from AEAT responses. |
| Volume | Suitable for low-volume scenarios (single invoices, ad-hoc corrections). | Required for high-volume scenarios or any system that must comply with Artículo 16, Orden HAC/1177/2024. |
| Error handling | Response available immediately on the InvoiceEntry object. | Errors surfaced via the optional SentError callback. |
| Shutdown | Nothing extra needed. | Must call InvoiceQueue.Exit() before the application exits. |