Skip to content

Sesja wsadowa

10.07.2025

Wysyłka wsadowa umożliwia jednorazowe przesłanie wielu faktur w pojedynczym pliku ZIP, zamiast wysyłać każdą fakturę oddzielnie.

To rozwiązanie przyspiesza i ułatwia przetwarzanie dużej liczby dokumentów — szczególnie dla firm, które generują wiele faktur dziennie.

Każda faktura musi być przygotowana w formacie XML zgodnie z aktualnym schematem opublikowanym przez Ministerstwo Finansów:

  • Paczka ZIP powinna być podzielona na części nie większe niż 100 MB (przed zaszyfrowaniem), które są szyfrowane i przesyłane osobno.
  • Należy podać informacje o każdej części paczki w obiekcie fileParts.

Wymagania wstępne

Aby skorzystać z wysyłki wsadowej, należy najpierw przejść proces uwierzytelnienia i posiadać aktualny token dostępu (accessToken), który uprawnia do korzystania z chronionych zasobów API KSeF.

Zalecenie (korelacja statusów po invoiceHash)
Przed utworzeniem paczki do wysyłki wsadowej zaleca się obliczyć skrót SHA-256 dla każdego pliku XML faktury (oryginał, przed szyfrowaniem) oraz zapisać lokalne mapowanie. Umożliwia to jednoznaczne powiązanie statusów przetwarzania po stronie KSeF z lokalnymi dokumentami źródłowymi (XML) przygotowanymi do wysyłki.

Przed otwarciem sesji oraz wysłaniem faktur wymagane jest:

  • wygenerowanie klucza symetrycznego o długości 256 bitów i wektora inicjującego o długości 128 bitów (IV), dołączanego jako prefiks do szyfrogramu,
  • przygotowanie paczki ZIP,
  • (opcjonalnie, jeśli paczka przekracza dopuszczalny rozmiar) podział paczki ZIP na części,
  • zaszyfrowanie części algorytmem AES-256-CBC z dopełnianiem PKCS#7,
  • zaszyfrowanie klucza symetrycznego algorytmem RSAES-OAEP (padding OAEP z funkcją MGF1 opartą na SHA-256 oraz skrótem SHA-256), przy użyciu klucza publicznego KSeF Ministerstwa Finansów.

Operacje te można zrealizować za pomocą komponentu CryptographyService, dostępnego w oficjalnym kliencie KSeF. Biblioteka ta udostępnia gotowe metody do generowania i szyfrowania kluczy, zgodnie z wymaganiami systemu.

Przykład w języku C#: KSeF.Client.Tests.Core\E2E\BatchSession\BatchSessionE2ETests.cs

csharp
EncryptionData encryptionData = cryptographyService.GetEncryptionData();

Przykład w języku Java: BatchIntegrationTest.java

java
EncryptionData encryptionData = cryptographyService.getEncryptionData();

Wygenerowane dane służą do szyfrowania faktur.

1. Przygotowanie paczki ZIP

Należy utworzyć paczkę ZIP zawierającą wszystkie faktury, które zostaną przesłane w ramach jednej sesji.

Przykład w języku C#: KSeF.Client.Tests.Core\E2E\BatchSession\BatchSessionE2ETests.cs

csharp
(byte[] zipBytes, Client.Core.Models.Sessions.FileMetadata zipMeta) =
    BatchUtils.BuildZip(invoices, cryptographyService);

//BatchUtils.BuildZip
public static (byte[] ZipBytes, FileMetadata Meta) BuildZip(
    IEnumerable<(string FileName, byte[] Content)> files,
    ICryptographyService crypto)
{
    using MemoryStream zipStream = new MemoryStream();
    using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: true);
    
    foreach ((string fileName, byte[] content) in files)
    {
        ZipArchiveEntry entry = archive.CreateEntry(fileName, CompressionLevel.Optimal);
        using Stream entryStream = entry.Open();
        entryStream.Write(content);
    }
    
    archive.Dispose();
    
    byte[] zipBytes = zipStream.ToArray();
    List<byte[]> meta = crypto.GetMetaData(zipBytes);

    return (zipBytes, meta);
}

Przykład w języku Java: BatchIntegrationTest.java

java
byte[] zipBytes = FilesUtil.createZip(invoicesInMemory);

// get ZIP metadata (before crypto)
FileMetadata zipMetadata = defaultCryptographyService.getMetaData(zipBytes);

2. Podział binarny paczki ZIP na części

Ze względu na ograniczenia rozmiaru przesyłanych plików, paczka ZIP powinna być podzielona binarnie na mniejsze części, które będą przesyłane osobno. Każda część powinna mieć unikalną nazwę i numer porządkowy.

Przykład w języku C#: KSeF.Client.Tests.Core\E2E\BatchSession\BatchSessionE2ETests.cs

csharp

 // Pobierz metadane ZIP-a (przed szyfrowaniem)
FileMetadata zipMetadata = cryptographyService.GetMetaData(zipBytes);
int maxPartSize = 100 * 1000 * 1000; // 100 MB
int partCount = (int)Math.Ceiling((double)zipBytes.Length / maxPartSize);
int partSize = (int)Math.Ceiling((double)zipBytes.Length / partCount);
List<byte[]> zipParts = new List<byte[]>();
for (int i = 0; i < partCount; i++)
{
    int start = i * partSize;
    int size = Math.Min(partSize, zipBytes.Length - start);
    if (size <= 0) break;
    byte[] part = new byte[size];
    Array.Copy(zipBytes, start, part, 0, size);
    zipParts.Add(part);
}

Przykład w języku Java: BatchIntegrationTest.java

java
List<byte[]> zipParts = FilesUtil.splitZip(partsCount, zipBytes);

3. Zaszyfrowanie części paczki

Każdą część należy zaszyfrować nowo wygenerowanym kluczem AES‑256‑CBC z dopełnianiem PKCS#7.

Przykład w języku C#: KSeF.Client.Tests.Core\E2E\BatchSession\BatchSessionStreamE2ETests.cs

csharp
List<BatchPartStreamSendingInfo> encryptedParts = new(rawParts.Count);
for (int i = 0; i < rawParts.Count; i++)
{
    using MemoryStream partInput = new(rawParts[i], writable: false);
    MemoryStream encryptedOutput = new();
    await cryptographyService.EncryptStreamWithAES256Async(partInput, encryptedOutput, encryptionData.CipherKey, encryptionData.CipherIv, CancellationToken).ConfigureAwait(false);

    if (encryptedOutput.CanSeek)
    {
        encryptedOutput.Position = 0;
    }

    FileMetadata partMeta = await cryptographyService.GetMetaDataAsync(encryptedOutput, CancellationToken).ConfigureAwait(false);
    if (encryptedOutput.CanSeek)
    {
        encryptedOutput.Position = 0; // reset po odczycie do metadanych
    }

    encryptedParts.Add(new BatchPartStreamSendingInfo
    {
        DataStream = encryptedOutput,
        OrdinalNumber = i + 1,
        Metadata = partMeta
    });
}

Przykład w języku Java: BatchIntegrationTest.java

java
 List<BatchPartSendingInfo> encryptedZipParts = new ArrayList<>();
 for (int i = 0; i < zipParts.size(); i++) {
     byte[] encryptedZipPart = defaultCryptographyService.encryptBytesWithAES256(
             zipParts.get(i),
             cipherKey,
             cipherIv
     );
     FileMetadata zipPartMetadata = defaultCryptographyService.getMetaData(encryptedZipPart);
     encryptedZipParts.add(new BatchPartSendingInfo(encryptedZipPart, zipPartMetadata, (i + 1)));
 }

4. Otwarcie sesji wsadowej

Inicjalizacja nowej sesji wsadowej z podaniem:

  • wersji schematu faktury: FA(2), FA(3)
    określa, którą wersję XSD system będzie stosować do walidacji przesyłanych faktur.
  • zaszyfrowanego klucza symetrycznego
    symetryczny klucz szyfrujący pliki XML, zaszyfrowany kluczem publicznym Ministerstwa Finansów; rekomendowane jest użycie nowo wygenerowanego klucza dla każdej sesji.
  • metadane paczki ZIP i jej części: nazwa pliku, hash, rozmiar oraz lista części (jeśli paczka jest dzielona)

POST /sessions/batch

W odpowiedzi na otwarcie sesji otrzymamy obiekt zawierający referenceNumber, który będzie używany w kolejnych krokach do identyfikacji sesji wsadowej.

Przykład w języku C#: KSeF.Client.Tests.Core\E2E\BatchSession\BatchSessionE2ETests.cs

csharp
Client.Core.Models.Sessions.BatchSession.OpenBatchSessionRequest openBatchRequest =
    BatchUtils.BuildOpenBatchRequest(zipMeta, encryptionData, encryptedParts, systemCode);

Client.Core.Models.Sessions.BatchSession.OpenBatchSessionResponse openBatchSessionResponse =
    await BatchUtils.OpenBatchAsync(KsefClient, openBatchRequest, accessToken).ConfigureAwait(false);

//BatchUtils.BuildOpenBatchRequest
public static OpenBatchSessionRequest BuildOpenBatchRequest(
    FileMetadata zipMeta,
    EncryptionData encryption,
    IEnumerable<BatchPartSendingInfo> encryptedParts,
    SystemCode systemCode = DefaultSystemCode,
    string schemaVersion = DefaultSchemaVersion,
    string value = DefaultValue)
{
    IOpenBatchSessionRequestBuilderBatchFile builder = OpenBatchSessionRequestBuilder
        .Create()
        .WithFormCode(systemCode: SystemCodeHelper.GetValue(systemCode), schemaVersion: schemaVersion, value: value)
        .WithBatchFile(fileSize: zipMeta.FileSize, fileHash: zipMeta.HashSHA);

    foreach (BatchPartSendingInfo p in encryptedParts)
    {
        builder = builder.AddBatchFilePart(
            ordinalNumber: p.OrdinalNumber,
            fileName: $"part_{p.OrdinalNumber}.zip.aes",
            fileSize: p.Metadata.FileSize,
            fileHash: p.Metadata.HashSHA);
    }

    return builder
        .EndBatchFile()
        .WithEncryption(
            encryptedSymmetricKey: encryption.EncryptionInfo.EncryptedSymmetricKey,
            initializationVector: encryption.EncryptionInfo.InitializationVector)
        .Build();
}

//BatchUtils.OpenBatchAsync
public static async Task<OpenBatchSessionResponse> OpenBatchAsync(
    IKSeFClient client,
    OpenBatchSessionRequest openReq,
    string accessToken)
    => await client.OpenBatchSessionAsync(openReq, accessToken).ConfigureAwait(false);

Przykład w języku Java: BatchIntegrationTest.java

java
OpenBatchSessionRequestBuilder builder = OpenBatchSessionRequestBuilder.create()
        .withFormCode(SystemCode.FA_2, SchemaVersion.VERSION_1_0E, SessionValue.FA)
        .withOfflineMode(false)
        .withBatchFile(zipMetadata.getFileSize(), zipMetadata.getHashSHA());

for (int i = 0; i < encryptedZipParts.size(); i++) {
        BatchPartSendingInfo part = encryptedZipParts.get(i);
        builder = builder.addBatchFilePart(i + 1, "faktura_part" + (i + 1) + ".zip.aes",part.getMetadata().getFileSize(), part.getMetadata().getHashSHA());
}

OpenBatchSessionRequest request = builder.endBatchFile()
        .withEncryption(
                        encryptionData.encryptionInfo().getEncryptedSymmetricKey(),
                        encryptionData.encryptionInfo().getInitializationVector()
                )
        .build();

OpenBatchSessionResponse response = ksefClient.openBatchSession(request, accessToken);

Metoda zwraca listę części paczki; dla każdej części podaje adres uploadu (URL), wymaganą metodę HTTP oraz komplet nagłówków, które należy przesłać razem z daną częścią.

5. Przesłanie zadeklarowanych części paczki

Korzystając z danych zwróconych przy otwarciu sesji w partUploadRequests, tj. unikalnego adresu url z kluczem dostępu, metody HTTP (method) oraz wymaganych nagłówków (headers), należy przesłać każdą zadeklarowaną część paczki (fileParts) pod wskazany adres, stosując dokładnie te wartości dla danej części. Łącznikiem pomiędzy deklaracją a instrukcją wysyłki jest właściwość ordinalNumber.

W treści żądania (body) należy umieścić bajty odpowiedniej części pliku (bez opakowania w JSON).

Uwaga: nie należy dodawać do nagłówków token dostępu (accessToken).

Każdą część przesyła się oddzielnym żądaniem HTTP. Zwracane kody odpowiedzi:

  • 201 - poprawne przyjęcie pliku,
  • 400 - błędne dane,
  • 401 - nieprawidłowe uwierzytelnienie,
  • 403 - brak uprawnień do zapisu (np. upłynął czas na zapis).

Przykład w języku C#: KSeF.Client.Tests.Core\E2E\BatchSession\BatchSessionE2ETests.cs

csharp
await KsefClient.SendBatchPartsAsync(openBatchSessionResponse, encryptedParts);

Przykład w języku Java: BatchIntegrationTest.java

java
ksefClient.sendBatchParts(response, encryptedZipParts);

Limit czasu na przesłanie partów w sesji wsadowej
Wysyłka plików w sesji wsadowej jest ograniczona czasowo. Czas ten zależy wyłącznie od liczby zadeklarowanych partów i wynosi 20 minut na każdy part. Każdy dodatkowy part proporcjonalnie zwiększa limit czasu dla każdego parta w paczce.

Łączny czas na wysyłkę każdego parta = liczba partów × 20 minut.
Przykład. Paczka zawiera 2 party – każdy part ma 40 minut na wysyłkę.

Wielkość parta nie ma znaczenia dla ustalenia limitu czasu – jedynym kryterium jest liczba partów zadeklarowana przy otwarciu sesji.

Autoryzacja jest weryfikowana na początku każdego żądania HTTP. Jeżeli w momencie przyjęcia żądania adres jest ważny, operacja przesłania zostaje zrealizowana w całości. Wygaśnięcie ważności w trakcie trwania przesyłania nie przerywa rozpoczętej operacji.

6. Zamknięcie sesji wsadowej

Po przesłaniu wszystkich części paczki należy zamknąć sesję wsadową, co inicjuje asynchronicznie przetwarzanie paczki faktur (szczegóły weryfikacji), oraz generowanie zbiorczego UPO.

POST /sessions/batch/{referenceNumber}/close}]

Przykład w języku C#: KSeF.Client.Tests.Core\E2E\BatchSession\BatchSessionStreamE2ETests.cs

csharp
await KsefClient.CloseBatchSessionAsync(referenceNumber, accessToken);

Przykład w języku Java: BatchIntegrationTest.java

java
ksefClient.closeBatchSession(referenceNumber, accessToken);

Zobacz