API usage examples
Below you will find practical examples of sending an FA(3) invoice in XML format to KSeF Service using different technologies: PHP, Node.js, Angular, .NET, Java, C++ Builder, React, PowerShell 5.1 or newer, and Bash/Linux. Each example calls the Swagger-compliant endpoint: POST /api/v1/invoices/send
Swagger UI documentation:
https://ksefservice.pl/docs/swagger/
PHP (cURL) — multipart/form-data
Send XML as multipart/form-data with fields:
clientId, login, password, api_env, file.
Content-Type: multipart/form-data manually.
cURL will automatically add the correct boundary.
Endpoint
POST https://ksefservice.pl/api/v1/invoices/send
Code
<?php declare(strict_types=1);
// === CONFIG ===
$url = 'https://ksefservice.pl/api/v1/invoices/send';
$clientId = '3c996a3f-7504-424e-851b-eaec2258777b';
$login = 'biuro@bsd-net.pl';
$password = 'PANEL_PASSWORD';
$apiEnv = 'TEST'; // TEST | DEMO | PROD
$xmlPath = __DIR__ . '/invoice_FA3.xml';
if (!file_exists($xmlPath)) {
http_response_code(400);
exit("File not found: {$xmlPath}\n");
}
$ch = curl_init($url);
$file = new CURLFile($xmlPath, 'application/xml', basename($xmlPath));
$postFields = [
'clientId' => $clientId,
'login' => $login,
'password' => $password,
'api_env' => $apiEnv,
'file' => $file,
];
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_CONNECTTIMEOUT => 20,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Accept: application/json'
],
]);
$response = curl_exec($ch);
if ($response === false) {
$err = curl_error($ch);
$eno = curl_errno($ch);
$ch = null; //curl_close($ch); This is deprecated in PHP 8.5.
http_response_code(500);
exit("cURL error ({$eno}): {$err}\n");
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$ch = null; //curl_close($ch); This is deprecated in PHP 8.5.
$rawHeaders = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
echo "HTTP: {$httpCode}\n";
echo "=== HEADERS ===\n{$rawHeaders}\n";
echo "=== BODY ===\n{$body}\n";
$decoded = json_decode($body, true);
if (json_last_error() === JSON_ERROR_NONE) {
echo "\n=== JSON (pretty) ===\n";
echo json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
}
Node.js — multipart/form-data upload (FA(3) XML) + UPO save
Example for Node.js 18+ using built-in
fetch, FormData, and Blob.
The script sends an XML invoice to:
POST https://ksefservice.pl/api/v1/invoices/send,
saves the full JSON response, saves the returned UPO XML, and writes metadata files to disk.
Content-Type: multipart/form-data manually.
fetch will automatically add the correct boundary.
This example requires Node.js 18+.
Endpoint
POST https://ksefservice.pl/api/v1/invoices/send
Code
const fs = require("fs");
const path = require("path");
async function sendInvoiceAndSaveUpo() {
try {
const filePath = path.join(__dirname, "invoice_FA3.xml");
if (!fs.existsSync(filePath)) {
throw new Error("Nie znaleziono pliku invoice_FA3.xml");
}
if (
typeof fetch === "undefined" ||
typeof FormData === "undefined" ||
typeof Blob === "undefined"
) {
throw new Error(
"Ten skrypt wymaga Node.js 18+ (wbudowane fetch, FormData, Blob)."
);
}
const form = new FormData();
form.append("clientId", "TWÓJ_CLIENT_ID");
form.append("login", "TWÓJ_LOGIN");
form.append("password", "TWOJE_HASŁO");
form.append("api_env", "TEST"); // TEST | DEMO | PROD
const fileBuffer = fs.readFileSync(filePath);
const fileBlob = new Blob([fileBuffer], { type: "application/xml" });
form.append("file", fileBlob, "invoice_FA3.xml");
const response = await fetch("https://ksefservice.pl/api/v1/invoices/send", {
method: "POST",
body: form
});
const resultText = await response.text();
if (!resultText || !resultText.trim()) {
throw new Error("Brak odpowiedzi z API");
}
let result;
try {
result = JSON.parse(resultText);
} catch (e) {
throw new Error("Odpowiedź nie jest poprawnym JSON-em: " + resultText);
}
const outDir = path.join(__dirname, "output");
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
fs.writeFileSync(
path.join(outDir, "response.json"),
JSON.stringify(result, null, 2),
"utf8"
);
console.log("Zapisano pełną odpowiedź do output/response.json");
const data = result.data || {};
const code = Number(data.code || response.status || 0);
if (!response.ok || code !== 200) {
console.error(
"API zwróciło błąd:",
result.message || data.description || "Nieznany błąd"
);
return;
}
console.log(result.message || "Faktura została poprawnie wysłana.");
const upoXml = data.upoXml || data.upo_xml || data.raw_upo || null;
const ksefNumber = String(data.ksefNumber || "brak_ksef");
const invoiceReference = String(data.invoiceReference || "brak_ref");
const sessionReference = String(data.sessionReference || "brak_sesji");
if (!upoXml) {
console.warn("Brak pola upoXml / upo_xml / raw_upo w odpowiedzi.");
return;
}
const safeKsefNumber = ksefNumber.replace(/[^a-zA-Z0-9-_]/g, "_");
const safeInvoiceReference = invoiceReference.replace(/[^a-zA-Z0-9-_]/g, "_");
const safeSessionReference = sessionReference.replace(/[^a-zA-Z0-9-_]/g, "_");
const upoFileName = `UPO_${safeKsefNumber}_${safeInvoiceReference}.xml`;
const upoPath = path.join(outDir, upoFileName);
fs.writeFileSync(upoPath, upoXml, "utf8");
console.log("UPO zapisano do pliku:");
console.log(upoPath);
const currentFilePath = path.join(__dirname, "upo.xml");
fs.writeFileSync(currentFilePath, upoXml, "utf8");
console.log("UPO zapisano także do bieżącego pliku:");
console.log(currentFilePath);
const metaFileName = `META_${safeSessionReference}_${safeInvoiceReference}.json`;
const metaPath = path.join(outDir, metaFileName);
fs.writeFileSync(
metaPath,
JSON.stringify(
{
message: result.message || "",
sessionReference: data.sessionReference || "",
invoiceReference: data.invoiceReference || "",
ksefNumber: data.ksefNumber || "",
code: data.code || "",
name: data.name || "",
description: data.description || "",
detailsHint: data.detailsHint || ""
},
null,
2
),
"utf8"
);
console.log("Zapisano metadane do pliku:");
console.log(metaPath);
console.log("Numer KSeF:", ksefNumber);
console.log("Invoice Reference:", invoiceReference);
console.log("Session Reference:", sessionReference);
} catch (error) {
console.error("Błąd:", error.message);
}
}
sendInvoiceAndSaveUpo();
Run
node sendInvoiceAndSaveUpo.js
Angular — multipart/form-data upload (FA(3) XML)
Angular service (HttpClient) + component with Bootstrap form, XML upload, and progress bar.
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpEvent } from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';
export type KsefEnv = 'TEST' | 'DEMO' | 'PROD';
export interface CreateInvoicePayload {
clientId: string;
login: string;
password: string;
api_env: KsefEnv;
file: File;
}
@Injectable({ providedIn: 'root' })
export class KsefInvoicesService {
private readonly baseUrl = 'https://ksefservice.pl/api/v1';
constructor(private http: HttpClient) {}
createInvoice(payload: CreateInvoicePayload): Observable<HttpEvent<any>> {
const form = new FormData();
form.append('clientId', payload.clientId);
form.append('login', payload.login);
form.append('password', payload.password);
form.append('api_env', payload.api_env);
form.append('file', payload.file, payload.file.name);
return this.http.post<any>(`${this.baseUrl}/invoices/send`, form, {
observe: 'events',
reportProgress: true
}).pipe(
catchError((err: HttpErrorResponse) => throwError(() => err))
);
}
}
import { Component } from '@angular/core';
import { HttpEventType, HttpErrorResponse } from '@angular/common/http';
import { KsefEnv, KsefInvoicesService } from '../../services/ksef-invoices.service';
@Component({
selector: 'app-ksef-invoice-upload',
templateUrl: './ksef-invoice-upload.component.html',
})
export class KsefInvoiceUploadComponent {
clientId = '3c996a3f-7504-424e-851b-eaec2258777b';
login = 'biuro@bsd-net.pl';
password = '';
api_env: KsefEnv = 'TEST';
file: File | null = null;
uploading = false;
progress = 0;
successMsg = '';
errorMsg = '';
responseBody: any = null;
constructor(private ksef: KsefInvoicesService) {}
onFileChange(evt: Event) {
this.errorMsg = '';
this.successMsg = '';
this.responseBody = null;
const input = evt.target as HTMLInputElement;
const f = input.files?.[0] ?? null;
if (!f) {
this.file = null;
return;
}
const isXmlByType = f.type === 'application/xml' || f.type === 'text/xml' || f.type === '';
const isXmlByName = f.name.toLowerCase().endsWith('.xml');
if (!isXmlByType && !isXmlByName) {
this.file = null;
this.errorMsg = 'Select an XML file (e.g. invoice_FA3.xml).';
input.value = '';
return;
}
this.file = f;
}
submit() {
this.errorMsg = '';
this.successMsg = '';
this.responseBody = null;
this.progress = 0;
if (!this.clientId || !this.login || !this.password || !this.api_env) {
this.errorMsg = 'Fill in clientId, login, password, and environment.';
return;
}
if (!this.file) {
this.errorMsg = 'Select the invoice XML file.';
return;
}
this.uploading = true;
this.ksef.createInvoice({
clientId: this.clientId,
login: this.login,
password: this.password,
api_env: this.api_env,
file: this.file
}).subscribe({
next: (event) => {
if (event.type === HttpEventType.UploadProgress) {
const total = event.total ?? 0;
this.progress = total > 0 ? Math.round((event.loaded / total) * 100) : 0;
}
if (event.type === HttpEventType.Response) {
this.uploading = false;
this.progress = 100;
this.responseBody = event.body;
this.successMsg = 'The invoice has been sent.';
}
},
error: (err: HttpErrorResponse) => {
this.uploading = false;
this.progress = 0;
const serverMsg =
(typeof err.error === 'string' ? err.error : (err.error?.message ?? '')) ||
err.message;
this.errorMsg = `Upload error (${err.status}): ${serverMsg}`;
this.responseBody = err.error ?? null;
}
});
}
}
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title mb-3">Send FA(3) invoice to KSeF Service</h5>
<div *ngIf="successMsg" class="alert alert-success">{{ successMsg }}</div>
<div *ngIf="errorMsg" class="alert alert-danger">{{ errorMsg }}</div>
<div class="mb-3">
<label class="form-label">clientId</label>
<input class="form-control" [(ngModel)]="clientId" placeholder="Client UUID" />
</div>
<div class="mb-3">
<label class="form-label">login</label>
<input class="form-control" [(ngModel)]="login" placeholder="e.g. biuro@bsd-net.pl" />
</div>
<div class="mb-3">
<label class="form-label">password</label>
<input class="form-control" type="password" [(ngModel)]="password" placeholder="PANEL_PASSWORD" />
</div>
<div class="mb-3">
<label class="form-label">api_env</label>
<select class="form-select" [(ngModel)]="api_env">
<option value="TEST">TEST</option>
<option value="DEMO">DEMO</option>
<option value="PROD">PROD</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">XML file</label>
<input class="form-control" type="file" accept=".xml,application/xml,text/xml" (change)="onFileChange($event)" />
<div class="form-text" *ngIf="file">Selected: <b>{{ file.name }}</b> ({{ file.size }} B)</div>
</div>
<div *ngIf="uploading" class="mb-3">
<div class="progress">
<div class="progress-bar" role="progressbar"
[style.width.%]="progress"
[attr.aria-valuenow]="progress"
aria-valuemin="0" aria-valuemax="100">
{{ progress }}%
</div>
</div>
</div>
<button class="btn btn-primary"
[disabled]="uploading"
(click)="submit()">
{{ uploading ? 'Uploading...' : 'Send' }}
</button>
</div>
</div>
<div *ngIf="responseBody" class="card mt-3">
<div class="card-body">
<h6 class="mb-2">API response</h6>
<pre class="mb-0">{{ responseBody | json }}</pre>
</div>
</div>
</div>
</div>
</div>
provideHttpClient() and import FormsModule in the app configuration.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { KsefInvoiceUploadComponent } from './components/ksef-invoice-upload/ksef-invoice-upload.component';
@NgModule({
declarations: [AppComponent, KsefInvoiceUploadComponent],
imports: [BrowserModule, HttpClientModule, FormsModule],
bootstrap: [AppComponent],
})
export class AppModule {}
<!-- app.component.html -->
<app-ksef-invoice-upload></app-ksef-invoice-upload>
.NET (ASP.NET Core 6/7/8) — direct KSeF Service API call
This example shows a direct call to the Swagger endpoint:
POST https://ksefservice.pl/api/v1/invoices/send.
API endpoint
POST https://ksefservice.pl/api/v1/invoices/send
Content-Type: multipart/form-data
using Microsoft.AspNetCore.Http;
using System.ComponentModel.DataAnnotations;
public class SendInvoiceRequest
{
[Required]
public Guid ClientId { get; set; }
[Required, EmailAddress]
public string Login { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
[Required]
public string Api_Env { get; set; } = "TEST"; // TEST | DEMO | PROD
[Required]
public IFormFile File { get; set; } = default!;
}
Content-Type manually.
Set Content-Type only for the file part.
using System.Net.Http.Headers;
public class KsefService
{
private readonly HttpClient _httpClient;
public KsefService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> SendInvoiceAsync(SendInvoiceRequest req)
{
using var form = new MultipartFormDataContent();
form.Add(new StringContent(req.ClientId.ToString()), "clientId");
form.Add(new StringContent(req.Login), "login");
form.Add(new StringContent(req.Password), "password");
form.Add(new StringContent(req.Api_Env), "api_env");
using var stream = req.File.OpenReadStream();
using var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
form.Add(fileContent, "file", req.File.FileName);
using var response = await _httpClient.PostAsync(
"https://ksefservice.pl/api/v1/invoices/send",
form
);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new ApplicationException($"API error: {response.StatusCode} - {body}");
return body;
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<KsefService>(client => {
client.Timeout = TimeSpan.FromSeconds(60);
});
var app = builder.Build();
app.Run();
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<KsefService>(client => {
client.Timeout = TimeSpan.FromSeconds(60);
});
var app = builder.Build();
app.MapPost("/send-example", async (KsefService ksefService) => {
var path = "invoice_FA3.xml";
if (!File.Exists(path))
return Results.BadRequest("File invoice_FA3.xml not found");
await using var stream = File.OpenRead(path);
IFormFile formFile = new FormFile(stream, 0, stream.Length, "file", "invoice_FA3.xml")
{
Headers = new HeaderDictionary(),
ContentType = "application/xml"
};
var request = new SendInvoiceRequest
{
ClientId = Guid.Parse("3c996a3f-7504-424e-851b-eaec2258777b"),
Login = "biuro@bsd-net.pl",
Password = "PANEL_PASSWORD",
Api_Env = "TEST",
File = formFile
};
var result = await ksefService.SendInvoiceAsync(request);
return Results.Content(result, "application/json");
});
app.Run();
Java — multipart/form-data upload (FA(3) XML)
Example in plain Java using java.net.http.HttpClient (Java 11+).
The upload works similarly to curl -F and sends the request to:
POST https://ksefservice.pl/api/v1/invoices/send.
api_env field:
TEST, DEMO, or PROD.
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;
public class KsefSendInvoiceExample {
public static void main(String[] args) throws Exception {
String url = "https://ksefservice.pl/api/v1/invoices/send";
String clientId = "3c996a3f-7504-424e-851b-eaec2258777b";
String login = "biuro@bsd-net.pl";
String password = "PANEL_PASSWORD";
String apiEnv = "TEST"; // TEST | DEMO | PROD
Path xmlFile = Path.of("invoice_FA3.xml");
if (!Files.exists(xmlFile)) {
throw new IllegalArgumentException("XML file not found: " + xmlFile.toAbsolutePath());
}
String boundary = "Boundary-" + UUID.randomUUID();
byte[] requestBody = buildMultipartBody(
boundary,
clientId,
login,
password,
apiEnv,
xmlFile
);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(20))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(60))
.header("Accept", "application/json")
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(requestBody))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println("HTTP: " + response.statusCode());
System.out.println("=== BODY ===");
System.out.println(response.body());
}
private static byte[] buildMultipartBody(
String boundary,
String clientId,
String login,
String password,
String apiEnv,
Path xmlFile
) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeFormField(baos, boundary, "clientId", clientId);
writeFormField(baos, boundary, "login", login);
writeFormField(baos, boundary, "password", password);
writeFormField(baos, boundary, "api_env", apiEnv);
writeFileField(
baos,
boundary,
"file",
xmlFile.getFileName().toString(),
"application/xml",
Files.readAllBytes(xmlFile)
);
baos.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
return baos.toByteArray();
}
private static void writeFormField(ByteArrayOutputStream baos, String boundary, String name, String value) throws IOException {
baos.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
baos.write(("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n").getBytes(StandardCharsets.UTF_8));
baos.write(value.getBytes(StandardCharsets.UTF_8));
baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
}
private static void writeFileField(
ByteArrayOutputStream baos,
String boundary,
String fieldName,
String fileName,
String contentType,
byte[] fileContent
) throws IOException {
baos.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
baos.write(("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"\r\n").getBytes(StandardCharsets.UTF_8));
baos.write(("Content-Type: " + contentType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
baos.write(fileContent);
baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
}
}
- Java 11+
invoice_FA3.xmlfile in the working directory- Internet access
javac KsefSendInvoiceExample.java
java KsefSendInvoiceExample
The API returns JSON, for example information about successful submission, the KSeF number, processing status, or a validation error.
{
"success": true,
"stage": "ksef_ok",
"message": "The FA(3) invoice was successfully sent to KSeF.",
"data": {
"ksefNumber": "5771876968-20251209-0100805B192F-B3",
"sessionReference": "20251209-SO-178C9DF000-9498D48B5B-BB",
"invoiceReference": "20251209-EE-178CA6F000-554110E66B-16"
}
}
C++ Builder (VCL / FMX, Indy) — multipart/form-data (FA(3) XML)
Sends multipart/form-data with text fields and an XML file to:
POST https://ksefservice.pl/api/v1/invoices/send. Indy + OpenSSL, no external libraries.
- TIdHTTP →
IdHTTP1 - TIdSSLIOHandlerSocketOpenSSL →
IdSSLIOHandlerSocketOpenSSL1 - TButton →
BtnSend - TMemo →
MemoLog - TOpenDialog →
OpenDialog1
IdHTTP1->IOHandler = IdSSLIOHandlerSocketOpenSSL1;
libssl-1_1.dll and libcrypto-1_1.dll
or the equivalents for your version.
// e.g. in FormCreate
IdHTTP1->IOHandler = IdSSLIOHandlerSocketOpenSSL1;
IdSSLIOHandlerSocketOpenSSL1->SSLOptions->Method = sslvTLSv1_2;
IdSSLIOHandlerSocketOpenSSL1->SSLOptions->Mode = sslmClient;
IdHTTP1->HandleRedirects = true;
IdHTTP1->ReadTimeout = 60000;
void __fastcall TForm1::BtnSendClick(TObject *Sender) {
if (!OpenDialog1->Execute())
return;
SendInvoice(
"https://ksefservice.pl/api/v1/invoices/send",
"3c996a3f-7504-424e-851b-eaec2258777b",
"biuro@bsd-net.pl",
"PANEL_PASSWORD",
"TEST", // TEST | DEMO | PROD
OpenDialog1->FileName
);
}
IdHTTP1->Request->ContentType = form->RequestContentType;
#include <IdHTTP.hpp>
#include <IdMultipartFormData.hpp>
#include <IdSSLOpenSSL.hpp>
#include <memory>
void __fastcall TForm1::SendInvoice(
const String& url,
const String& clientId,
const String& login,
const String& password,
const String& apiEnv,
const String& xmlPath
) {
if (!xmlPath.LowerCase().EndsWith(".xml"))
{
MemoLog->Lines->Add("Error: select an XML file (.xml).");
return;
}
std::unique_ptr<TIdMultiPartFormDataStream> form(new TIdMultiPartFormDataStream());
try
{
form->AddFormField("clientId", clientId);
form->AddFormField("login", login);
form->AddFormField("password", password);
form->AddFormField("api_env", apiEnv);
form->AddFile("file", xmlPath, "application/xml");
IdHTTP1->Request->Accept = "application/json";
IdHTTP1->Request->ContentType = form->RequestContentType;
MemoLog->Lines->Add("Sending invoice...");
String response = IdHTTP1->Post(url, form.get());
MemoLog->Lines->Add("OK:");
MemoLog->Lines->Add(response);
}
catch (const EIdHTTPProtocolException &e)
{
MemoLog->Lines->Add("HTTP ERROR:");
MemoLog->Lines->Add(e.ErrorMessage);
MemoLog->Lines->Add(e.ErrorText);
}
catch (const Exception &e)
{
MemoLog->Lines->Add("ERROR:");
MemoLog->Lines->Add(e.Message);
}
}
C++ Builder (Windows) — running curl.exe (POST + -F + XML)
The CreateProcessW + pipe variant captures stdout/stderr into a memo.
#include <System.SysUtils.hpp>
#include <Winapi.Windows.hpp>
#include <memory>
static bool RunCurlInvoices(
const String& curlExe,
const String& xmlPath,
const String& clientId,
const String& login,
const String& password,
const String& apiEnv,
String& outStdout,
DWORD timeoutMs = 60000
) {
outStdout = "";
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE hRead = NULL, hWrite = NULL;
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return false;
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);
String cmd =
L"\"" + curlExe + L"\""
L" -sS -i --fail-with-body"
L" -X POST \"https://ksefservice.pl/api/v1/invoices/send\""
L" -F \"clientId=" + clientId + L"\""
L" -F \"login=" + login + L"\""
L" -F \"password=" + password + L"\""
L" -F \"api_env=" + apiEnv + L"\""
L" -F \"file=@" + xmlPath + L";type=application/xml\"";
STARTUPINFOW si{};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdOutput = hWrite;
si.hStdError = hWrite;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
PROCESS_INFORMATION pi{};
std::unique_ptr<wchar_t[]> buf(new wchar_t[cmd.Length() + 1]);
wcscpy(buf.get(), cmd.c_str());
BOOL ok = CreateProcessW(
NULL,
buf.get(),
NULL, NULL,
TRUE,
CREATE_NO_WINDOW,
NULL, NULL,
&si, &pi
);
CloseHandle(hWrite);
if (!ok) {
CloseHandle(hRead);
return false;
}
const DWORD start = GetTickCount();
char tmp[4096];
DWORD bytesRead = 0;
while (true)
{
if (GetTickCount() - start > timeoutMs) {
TerminateProcess(pi.hProcess, 1);
break;
}
DWORD exitCode = STILL_ACTIVE;
GetExitCodeProcess(pi.hProcess, &exitCode);
DWORD avail = 0;
if (PeekNamedPipe(hRead, NULL, 0, NULL, &avail, NULL) && avail > 0) {
if (ReadFile(hRead, tmp, sizeof(tmp)-1, &bytesRead, NULL) && bytesRead > 0) {
tmp[bytesRead] = 0;
outStdout += String(AnsiString(tmp));
}
} else {
if (exitCode != STILL_ACTIVE) break;
Sleep(30);
}
}
WaitForSingleObject(pi.hProcess, 1000);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(hRead);
return true;
}
void __fastcall TForm1::Button1Click(TObject *Sender) {
String output;
bool ok = RunCurlInvoices(
L"curl",
L"C:\\temp\\invoice_FA3.xml",
L"3c996a3f-7504-424e-851b-eaec2258777b",
L"biuro@bsd-net.pl",
L"PANEL_PASSWORD",
L"TEST", // TEST | DEMO | PROD
output,
60000
);
Memo1->Lines->Text = output;
if (!ok) {
ShowMessage(L"Failed to start curl (check PATH or the path to curl.exe).");
}
}
React — multipart/form-data upload (FA(3) XML)
React component + Bootstrap. The upload works similarly to curl -F.
fetch does not provide real upload progress like XHR. Only “Uploading...” status is available.
import React, { useMemo, useState } from "react";
export default function KsefInvoiceUpload() {
const [clientId, setClientId] = useState("3c996a3f-7504-424e-851b-eaec2258777b");
const [login, setLogin] = useState("biuro@bsd-net.pl");
const [password, setPassword] = useState("");
const [apiEnv, setApiEnv] = useState("TEST");
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const [progressText, setProgressText] = useState("");
const [successMsg, setSuccessMsg] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [responseBody, setResponseBody] = useState(null);
const canSubmit = useMemo(() => {
return clientId && login && password && apiEnv && file && !loading;
}, [clientId, login, password, apiEnv, file, loading]);
const onFileChange = (e) => {
setSuccessMsg("");
setErrorMsg("");
setResponseBody(null);
const f = e.target.files?.[0] || null;
if (!f) {
setFile(null);
return;
}
const nameOk = f.name.toLowerCase().endsWith(".xml");
const typeOk = f.type === "application/xml" || f.type === "text/xml" || f.type === "";
if (!nameOk && !typeOk) {
setFile(null);
setErrorMsg("Select an XML file (e.g. invoice_FA3.xml).");
e.target.value = "";
return;
}
setFile(f);
};
const submit = async () => {
setSuccessMsg("");
setErrorMsg("");
setResponseBody(null);
setProgressText("");
if (!clientId || !login || !password || !apiEnv) {
setErrorMsg("Fill in clientId, login, password, and environment.");
return;
}
if (!file) {
setErrorMsg("Select the invoice XML file.");
return;
}
setLoading(true);
setProgressText("Uploading...");
try {
const form = new FormData();
form.append("clientId", clientId);
form.append("login", login);
form.append("password", password);
form.append("api_env", apiEnv);
form.append("file", file, file.name);
const res = await fetch("https://ksefservice.pl/api/v1/invoices/send", {
method: "POST",
body: form,
});
const text = await res.text();
let parsed = text;
try { parsed = JSON.parse(text); } catch {}
if (!res.ok) {
setErrorMsg(`Upload error (${res.status}): ${typeof parsed === "string" ? parsed : (parsed?.message || "HTTP error")}`);
setResponseBody(parsed);
return;
}
setSuccessMsg("The invoice has been sent.");
setResponseBody(parsed);
} catch (err) {
setErrorMsg(`Error: ${err?.message || String(err)}`);
} finally {
setLoading(false);
setProgressText("");
}
};
return (
<div className="container py-4">
<div className="row justify-content-center">
<div className="col-12 col-lg-8">
<div className="card shadow-sm">
<div className="card-body">
<h5 className="card-title mb-3">Send FA(3) invoice to KSeF Service</h5>
{successMsg && <div className="alert alert-success">{successMsg}</div>}
{errorMsg && <div className="alert alert-danger">{errorMsg}</div>}
<div className="mb-3">
<label className="form-label">clientId</label>
<input className="form-control" value={clientId} onChange={(e) => setClientId(e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">login</label>
<input className="form-control" value={login} onChange={(e) => setLogin(e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">password</label>
<input className="form-control" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">api_env</label>
<select className="form-select" value={apiEnv} onChange={(e) => setApiEnv(e.target.value)}>
<option value="TEST">TEST</option>
<option value="DEMO">DEMO</option>
<option value="PROD">PROD</option>
</select>
</div>
<div className="mb-3">
<label className="form-label">XML file</label>
<input className="form-control" type="file" accept=".xml,application/xml,text/xml" onChange={onFileChange} />
{file && <div className="form-text">Selected: <b>{file.name}</b> ({file.size} B)</div>}
</div>
{loading && <div className="alert alert-info py-2">{progressText || "Uploading..."}</div>}
<button className="btn btn-primary" disabled={!canSubmit} onClick={submit}>
{loading ? "Uploading..." : "Send"}
</button>
</div>
</div>
{responseBody != null && (
<div className="card mt-3">
<div className="card-body">
<h6 className="mb-2">API response</h6>
<pre className="mb-0">{typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody, null, 2)}</pre>
</div>
</div>
)}
</div>
</div>
</div>
);
}
PowerShell — how to run the KSeFService.ps1 script
Run the script in PowerShell from the directory where the
KSeFService.ps1 file is located.
If you do not provide any parameters, the script will run in Interactive mode.
.\KSeFService.ps1
Interactive mode is used for sending single XML files.
.\KSeFService.ps1 -Mode Interactive
Batch mode is used for sending ZIP packages and XML files,
which the script will automatically compress into ZIP.
.\KSeFService.ps1 -Mode Batch
If you want to set clientId, login,
password, api_env, and the input/output directories again,
use the -Reconfigure parameter.
.\KSeFService.ps1 -Reconfigure
.\KSeFService.ps1 -Mode Batch -Reconfigure
You can override the environment only for the current run using
the -EnvOverride parameter.
Available values:
TESTPRODDEMO
Examples:
.\KSeFService.ps1 -Mode Interactive -EnvOverride DEMO
.\KSeFService.ps1 -Mode Batch -EnvOverride TEST
To install a task for Interactive mode:
.\KSeFService.ps1 -Mode Interactive -InstallTask
To install a task for Batch mode:
.\KSeFService.ps1 -Mode Batch -InstallTask
To remove a task for Interactive mode:
.\KSeFService.ps1 -Mode Interactive -UninstallTask
To remove a task for Batch mode:
.\KSeFService.ps1 -Mode Batch -UninstallTask
If PowerShell blocks the script, use:
powershell.exe -ExecutionPolicy Bypass -File .\KSeFService.ps1 -Mode Interactive
powershell.exe -ExecutionPolicy Bypass -File .\KSeFService.ps1 -Mode Batch
On the first run, the script will ask for:
clientIdloginpasswordapi_env(TEST/PROD/DEMO)input directoryoutput directoryvalidate_xsdoptionwaitFinaloption
The configuration will be saved in the Windows registry under the key:
HKCU\Software\KSeFServicePS
Interactive mode:
- searches for XML files in the input directory,
- sends each file separately,
- saves JSON responses,
- saves UPO if returned by the API,
- archives input files to
archive\sentorarchive\error.
Batch mode:
- searches for ZIP and XML files in the input directory,
- automatically compresses XML files into ZIP,
- sends packages to the batch endpoint,
- saves JSON responses,
- optionally saves UPO for documents returned by the API,
- archives input files to
archive\sentorarchive\error.
Interactive mode:
powershell.exe -ExecutionPolicy Bypass -File .\KSeFService.ps1 -Mode Interactive
Batch mode:
powershell.exe -ExecutionPolicy Bypass -File .\KSeFService.ps1 -Mode Batch
Batch mode in DEMO environment:
powershell.exe -ExecutionPolicy Bypass -File .\KSeFService.ps1 -Mode Batch -EnvOverride DEMO
Reconfiguration:
powershell.exe -ExecutionPolicy Bypass -File .\KSeFService.ps1 -Reconfigure
- The script requires PowerShell 5.1 or newer.
- Running PowerShell as administrator may be required to install scheduled tasks.
- The user password is stored in the registry in encrypted form for the current Windows user.
- The
DEMO,TEST, orPRODenvironment can be set during configuration or once with the-EnvOverrideparameter.
Bash / Linux Debian — how to use the KSeFService.sh script
The KSeFService.sh script is used for automatic sending of XML invoices to KSeF Service
on Linux Debian. It supports interactive and batch modes,
saving configuration on first run, file archiving, and optional automation via cron.
Required tools:
bashcurlzip
Optional:
jq– more convenient JSON analysis and UPO saving,secret-tool– secure password storage in keyring,crontab– automatic script execution.
Example installation on Debian:
sudo apt update
sudo apt install curl zip jq libsecret-tools cron
First, grant execute permission to the file:
chmod +x KSeFService.sh
Then run the script:
./KSeFService.sh --mode interactive
On the first run, the script will ask for:
clientIdloginpasswordapi_env(TEST/PROD/DEMO)input directoryoutput directoryvalidate_xsdfor batch modewaitfor batch mode
The configuration is saved in the file:
~/.config/ksefservice/config.env
The password can be stored in libsecret or in the configuration file
as a fallback. The configuration file has 600 permissions.
Mode interactive:
- searches for all
*.xmlfiles in the input directory, - sends each file separately to the interactive endpoint,
- saves the JSON response log,
- saves UPO if the API returns
upo_xml, - archives the file to
archive/sentorarchive/error.
Mode batch:
- searches for
*.zipand*.xmlfiles, - automatically compresses XML files into ZIP,
- sends packages to the batch endpoint,
- passes
waitandvalidate_xsdfrom configuration, - saves the JSON log,
- saves UPO for documents returned by the API, if available,
- archives the source file to
archive/sentorarchive/error.
Interactive mode:
./KSeFService.sh --mode interactive
Batch mode:
./KSeFService.sh --mode batch
One-time DEMO environment override:
./KSeFService.sh --mode batch --env DEMO
Reconfiguration:
./KSeFService.sh --reconfigure
Place files to be sent in the input directory:
*.xmlforinteractive,*.zipor*.xmlforbatch.
The script saves the following in the output directory:
- API response logs,
- UPO files,
.error.txterror files.
Input file archiving works to the following directories:
OUTPUT/archive/sent
OUTPUT/archive/error
Available environments:
TESTPRODDEMO
You can set the environment during configuration or one time with:
--env TEST
--env PROD
--env DEMO
The script can add an automatic task to crontab.
Install a task for batch mode:
./KSeFService.sh --mode batch --install-task
Install a task for interactive mode:
./KSeFService.sh --mode interactive --install-task
Remove a task:
./KSeFService.sh --mode batch --uninstall-task
Example output files:
interactive_name_20260319_210000.jsonbatch_name_20260319_210000.jsoninteractive_name_20260319_210000.error.txtbatch_name_20260319_210000.error.txtUPO_name_20260319_210000.xmlUPO_batch_name_1_20260319_210000.xml
Thanks to archiving, input files are moved after processing and are not sent again on the next run.
- The script is intended for Linux Debian.
- A working
cronis required for proper automation. - If you do not have
secret-tool, the password will be stored in the configuration file as a fallback. batchmode automatically compresses XML into ZIP.- You can set the
TEST,PROD, orDEMOenvironment in the configuration or one time with the--envparameter.