Translation. Original: pobieranie-faktur/przyrostowe-pobieranie-faktur.md
Инкрементальное получение счетов-фактур
21.11.2025
Введение
Инкрементальное получение счетов-фактур, основанное на экспорте пакетов (POST /invoices/exports), является рекомендуемым механизмом синхронизации между центральным репозиторием KSeF и локальными базами данных внешних систем.
Ключевую роль здесь играет механизм High Water Mark (HWM) - стабильная точка во времени, до которой система гарантирует полноту данных.
Архитектура решения
Инкрементальное получение основано на трех ключевых компонентах:
- Синхронизация во временных окнах - использование смежных временных окон с учетом HWM обеспечивает непрерывность и отсутствие пропусков
- Обработка лимитов API - управление темпом вызовов, обработка HTTP 429 и Retry-After.
- Дедупликация - устранение дубликатов на основе метаданных из файлов
_metadata.json.
Базовый метод: POST /invoices/exports инициирует асинхронный экспорт. После завершения обработки статус операции предоставляет уникальные URL-адреса для загрузки частей пакета.
Синхронизация во временных окнах (Windowing)
Концепция
Получение счетов-фактур происходит в смежных временных окнах с использованием даты PermanentStorageHwmDate. Чтобы включить механизм HWM, необходимо установить параметр restrictToPermanentStorageHwmDate = true в запросе экспорта. Каждое следующее окно начинается точно в момент завершения предыдущего с учетом HWM (за исключением ситуации, описанной в разделе Механизм High Water Mark (HWM) и обработка усеченных пакетов). Под "моментом завершения" понимается:
- значение
dateRange.to, когда оно было указано, или PermanentStorageHwmDate, когдаdateRange.toпропущено.
Такой подход обеспечивает непрерывность диапазонов и устраняет риск пропуска какого-либо счета-фактуры. Счета-фактуры должны получаться отдельно для каждого типа субъекта (Podmiot 1, Podmiot 2, Podmiot 3, Podmiot upoważniony), встречающегося в документе. Итерация по субъектам обеспечивает полноту данных - компания может выступать в разных ролях на счетах-фактурах.
Пример на языке C#: KSeF.Client.Tests.Core\E2E\Invoice\IncrementalInvoiceRetrievalE2ETests.cs
// Словарь для отслеживания точки продолжения для каждого SubjectType
Dictionary<InvoiceSubjectType, DateTime?> continuationPoints = new();
IReadOnlyList<(DateTime From, DateTime To)> windows = BuildIncrementalWindows(batchCreationStart, batchCreationCompleted);
// Создание плана экспорта - кортежи (временное окно, тип субъекта)
IEnumerable<InvoiceSubjectType> subjectTypes = Enum.GetValues<InvoiceSubjectType>().Where(x => x != InvoiceSubjectType.SubjectAuthorized);
IOrderedEnumerable<ExportTask> exportTasks = windows
.SelectMany(window => subjectTypes, (window, subjectType) => new ExportTask(window.From, window.To, subjectType))
.OrderBy(task => task.From)
.ThenBy(task => task.SubjectType);
foreach (ExportTask task in exportTasks)
{
DateTime effectiveFrom = GetEffectiveStartDate(continuationPoints, task.SubjectType, task.From);
OperationResponse? exportResponse = await InitiateInvoiceExportAsync(effectiveFrom, task.To, task.SubjectType);
// Дальнейшая обработка экспорта...Пример на языке java: IncrementalInvoiceRetrieveIntegrationTest.java
Map<InvoiceQuerySubjectType, OffsetDateTime> continuationPoints = new HashMap<>();
List<TimeWindows> timeWindows = buildIncrementalWindows(batchCreationStart, batchCreationCompleted);
List<InvoiceQuerySubjectType> subjectTypes = Arrays.stream(InvoiceQuerySubjectType.values())
.filter(x -> x != InvoiceQuerySubjectType.SUBJECTAUTHORIZED)
.toList();
List<ExportTask> exportTasks = timeWindows.stream()
.flatMap(window -> subjectTypes.stream()
.map(subjectType -> new ExportTask(window.getFrom(), window.getTo(), subjectType)))
.sorted(Comparator.comparing(ExportTask::getFrom)
.thenComparing(ExportTask::getSubjectType))
.toList();
exportTasks.forEach(task -> {
EncryptionData encryptionData = defaultCryptographyService.getEncryptionData();
OffsetDateTime effectiveFrom = getEffectiveStartDate(continuationPoints, task.getSubjectType(), task.getFrom());
String operationReferenceNumber = initiateInvoiceExportAsync(effectiveFrom, task.getTo(),
task.getSubjectType(), accessToken, encryptionData.encryptionInfo());
// Дальнейшая обработка экспорта...Рекомендуемые размеры окон
- Частота и лимиты
POST/invoice/exportsтребует указания типа субъекта (Podmiot 1,Podmiot 2,Podmiot 3,Podmiot upoważniony). Согласно лимитам API можно инициировать максимум 20 экспортов в час; расписание должно распределять этот пул между выбранными типами субъектов. - Стратегия расписания
В режиме непрерывной синхронизации можно принять 4 экспорта/ч на каждый тип субъекта. На практике ролиPodmiot 3иPodmiot upoważnionyобычно встречаются реже и могут запускаться спорадически, например, раз в сутки в ночном окне. - Минимальный интервал
Циклический интервал не должен быть короче 15 минут для каждого типа субъекта (согласно рекомендациям в лимитах API). - Размер окна В сценарии непрерывной синхронизации рекомендуется вызов экспорта без указанной даты окончания (
DateRange.Toпропущено). В таком случае система KSeF готовит максимально большой, согласованный пакет в пределах лимитов алгоритма (количество счетов-фактур, размер данных после сжатия), что ограничивает количество вызовов и нагрузку с обеих сторон. КогдаIsTruncated = true, следующий вызов нужно начинать сLastPermanentStorageDate, когдаIsTruncated = false, следующий вызов нужно начинать с возвращенногоPermanentStorageHwmDate. - Отсутствие наложения Диапазоны должны быть смежными; конец одного окна является началом следующего.
- Контрольная точка Точка продолжения, определяемая HWM -
PermanentStorageHwmDateилиLastPermanentStorageDateдля усеченных пакетов составляет начало следующего окна.
Датой получения счета-фактуры является дата присвоения номера KSeF. Номер присваивается во время обработки счета-фактуры со стороны KSeF и не зависит от момента загрузки во внешнюю систему.
Обработка лимитов API
Лимиты по типу эндпойнтов
Все эндпойнты, связанные с получением счетов-фактур, подлежат строгим лимитам API, определенным в документации Лимиты API. Эти лимиты являются обязательными и должны соблюдаться каждой реализацией инкрементального получения.
В случае превышения лимитов система возвращает код HTTP 429 (Too Many Requests) вместе с заголовком Retry-After, указывающим время ожидания перед следующей попыткой.
Инициализация экспорта счетов-фактур
Ключевое значение даты PermanentStorage
Для инкрементального получения счетов-фактур необходимо использовать дату типа PermanentStorage, которая обеспечивает достоверность данных. Означает момент постоянной материализации записи, устойчива к асинхронным задержкам процесса принятия данных и позволяет безопасно определять окна приращения. Таким образом, другие типы дат (как Issue или Invoicing) могут привести к непредсказуемому поведению при инкрементальной синхронизации.
Пример на языке C#: KSeF.Client.Tests.Core\E2E\Invoice\IncrementalInvoiceRetrievalE2ETests.cs
EncryptionData exportEncryption = CryptographyService.GetEncryptionData();
InvoiceQueryFilters filters = new()
{
SubjectType = subjectType,
DateRange = new DateRange
{
DateType = DateType.PermanentStorage,
From = windowFromUtc,
To = windowToUtc,
RestrictToPermanentStorageHwmDate = true
}
};
InvoiceExportRequest request = new()
{
Filters = filters,
Encryption = exportEncryption.EncryptionInfo
};
OperationResponse response = awat KsefClient.ExportInvoicesAsync(request, _accessToken, ct, includeMetadata: true);Пример на языке java: IncrementalInvoiceRetrieveIntegrationTest.java
EncryptionData encryptionData = defaultCryptographyService.getEncryptionData();
InvoiceExportFilters filters = new InvoiceExportFilters();
filters.setSubjectType(subjectType);
filters.setDateRange(new InvoiceQueryDateRange(
InvoiceQueryDateType.PERMANENTSTORAGE, windowFrom, windowTo)
);
InvoiceExportRequest request = new InvoiceExportRequest();
request.setFilters(filters);
request.setEncryption(encryptionInfo);
InitAsyncInvoicesQueryResponse response = ksefClient.initAsyncQueryInvoice(request, accessToken);Получение и обработка пакетов
После завершения экспорта пакет счетов-фактур доступен для загрузки как зашифрованный архив ZIP, разделенный на части. Процесс загрузки и обработки включает:
- Загрузка частей - каждая часть загружается отдельно с URL-адресов, возвращенных в статусе операции.
- Расшифровка AES-256 - каждая часть расшифровывается с использованием ключа и IV, сгенерированных при инициализации экспорта.
- Сборка пакета - расшифрованные части объединяются в один поток данных.
- Распаковка ZIP - архив содержит XML-файлы счетов-фактур и файл
_metadata.json.
Файл _metadata.json
Содержимое файла _metadata.json представляет собой JSON-объект со свойством invoices (массив элементов типа InvoiceMetadata, как возвращаемый POST /invoices/query/metadata). Этот файл является ключевым для механизма дедупликации, поскольку содержит номера KSeF всех счетов-фактур в пакете.
Включение метаданных (до 27.10.2025)
Чтобы включить файл _metadata.json, необходимо добавить заголовок к запросу экспорта:
X-KSeF-Feature: include-metadataС 27.10.2025 пакет экспорта всегда будет содержать файл _metadata.json без необходимости добавления заголовка.
Пример на языке C#:
KSeF.Client.Tests.Core\E2E\Invoice\IncrementalInvoiceRetrievalE2ETests.cs
KSeF.Client.Tests.Utils\BatchSessionUtils.cs
List<InvoiceSummary> metadataSummaries = new();
Dictionary<string, string> invoiceXmlFiles = new(StringComparer.OrdinalIgnoreCase);
// Получение, расшифровка и объединение всех частей в один поток
using MemoryStream decryptedArchiveStream = await BatchUtils.DownloadAndDecryptPackagePartsAsync(
package.Parts,
encryptionData,
CryptographyService,
cancellationToken: CancellationToken)
.ConfigureAwait(false);
// Распаковка ZIP
Dictionary<string, string> unzippedFiles = await BatchUtils.UnzipAsync(decryptedArchiveStream, CancellationToken).ConfigureAwait(false);
foreach ((string fileName, string content) in unzippedFiles)
{
if (fileName.Equals(MetadataEntryName, StringComparison.OrdinalIgnoreCase))
{
InvoicePackageMetadata? metadata = JsonSerializer.Deserialize<InvoicePackageMetadata>(content, MetadataSerializerOptions);
if (metadata?.Invoices != null)
{
metadataSummaries.AddRange(metadata.Invoices);
}
}
else if (fileName.EndsWith(XmlFileExtension, StringComparison.OrdinalIgnoreCase))
{
invoiceXmlFiles[fileName] = content;
}
}Пример на языке java: IncrementalInvoiceRetrieveIntegrationTest.java
List<InvoicePackagePart> parts = invoiceExportStatus.getPackageParts().getParts();
byte[] mergedZip = FilesUtil.mergeZipParts(
encryptionData,
parts,
part -> ksefClient.downloadPackagePart(part),
(encryptedPackagePart, key, iv) -> defaultCryptographyService.decryptBytesWithAes256(encryptedPackagePart, key, iv)
);
Map<String, String> downloadedFiles = FilesUtil.unzip(mergedZip);
String metadataJson = downloadedFiles.keySet()
.stream()
.filter(fileName -> fileName.endsWith(".json"))
.findFirst()
.map(downloadedFiles::get)
.orElse(null);
InvoicePackageMetadata invoicePackageMetadata = objectMapper.readValue(metadataJson, InvoicePackageMetadata.class);Механизм High Water Mark (HWM) и обработка усеченных пакетов (IsTruncated)
Концепция HWM
High Water Mark (HWM) - это механизм, обеспечивающий оптимальное управление начальными точками для последующих экспортов при инкрементальном получении счетов-фактур. HWM состоит из двух взаимодополняющих компонентов:
PermanentStorageHwmDate- стабильная верхняя граница данных, включенных в пакет, представляющая момент, до которого система гарантирует полноту данных.LastPermanentStorageDate- дата последнего счета-фактуры в пакете, используемая когда пакет был усечен (IsTruncated = true).
Преимущества механизма HWM
- Минимизация дубликатов - HWM значительно сокращает количество дубликатов между последующими пакетами
- Оптимизация производительности - уменьшает нагрузку на механизм дедупликации
- Сохранение полноты - обеспечивает, что никакие счета-фактуры не будут пропущены
- Стабильность синхронизации - предоставляет надежные точки продолжения для длительных процессов
Стратегия продолжения пакетов
Флаг IsTruncated = true устанавливается, когда при построении пакета достигнуты лимиты алгоритма (количество счетов-фактур или размер данных после сжатия). В таком случае в статусе операции доступны оба свойства HWM. Механизм HWM использует следующую иерархию приоритетов для определения точки продолжения. Чтобы сохранить непрерывность получения и не пропустить ни одного документа, следующий вызов экспорта должен начинаться с:
- Усеченный пакет (
IsTruncated = true) - следующий вызов начинается сLastPermanentStorageDate - Стабильный HWM - использование
PermanentStorageHwmDateкак начальной точки для следующего окна
- следующее окно начинается в той же точке, где закончилось (смежность); возможные дубликаты будут удалены на этапе дедупликации на основе номеров KSeF из _metadata.json. Ниже приведен пример поддержания точки продолжения:
Пример на языке C#: KSeF.Client.Tests.Core\E2E\Invoice\IncrementalInvoiceRetrievalE2ETests.cs
private static void UpdateContinuationPointIfNeeded(
Dictionary<InvoiceSubjectType, DateTime?> continuationPoints,
InvoiceSubjectType subjectType,
InvoiceExportPackage package)
{
// Приоритет 1: Усеченный пакет - LastPermanentStorageDate
if (package.IsTruncated && package.LastPermanentStorageDate.HasValue)
{
continuationPoints[subjectType] = package.LastPermanentStorageDate.Value.UtcDateTime;
}
// Приоритет 2: Стабильный HWM как граница следующего окна
else if (package.PermanentStorageHwmDate.HasValue)
{
continuationPoints[subjectType] = package.PermanentStorageHwmDate.Value.UtcDateTime;
}
else
{
// Диапазон полностью обработан - удаление точки продолжения
continuationPoints.Remove(subjectType);
}
}Пример на языке java: IncrementalInvoiceRetrieveIntegrationTest.java
private void updateContinuationPointIfNeeded(Map<InvoiceQuerySubjectType, OffsetDateTime> continuationPoints,
InvoiceQuerySubjectType subjectType,
InvoiceExportPackage invoiceExportPackage) {
if (Boolean.TRUE.equals(invoiceExportPackage.getIsTruncated()) && Objects.nonNull(invoiceExportPackage.getLastPermanentStorageDate())) {
continuationPoints.put(subjectType, invoiceExportPackage.getLastPermanentStorageDate());
} else {
continuationPoints.remove(subjectType);
}
}Дедупликация счетов-фактур
Стратегия дедупликации
Дедупликация происходит на основе номеров KSeF, содержащихся в файле _metadata.json:
Пример на языке C#: KSeF.Client.Tests.Core\E2E\Invoice\IncrementalInvoiceRetrievalE2ETests.cs
Dictionary<string, InvoiceSummary> uniqueInvoices = new(StringComparer.OrdinalIgnoreCase);
bool hasDuplicates = false;
// Обработка метаданных из пакета - добавление уникальных счетов-фактур и обнаружение дубликатов
hasDuplicates = packageResult.MetadataSummaries
.DistinctBy(s => s.KsefNumber, StringComparer.OrdinalIgnoreCase)
.Any(summary => !uniqueInvoices.TryAdd(summary.KsefNumber, summary));Пример на языке java: IncrementalInvoiceRetrieveIntegrationTest.java
hasDuplicates.set(packageProcessingResult.getInvoiceMetadataList()
.stream()
.anyMatch(summary -> uniqueInvoices.containsKey(summary.getKsefNumber())));
packageProcessingResult.getInvoiceMetadataList()
.stream()
.distinct()
.forEach(summary -> uniqueInvoices.put(summary.getKsefNumber(), summary));