Guides
WooCommerce and PrestaShop as API clients
The WooCommerce plugin uses the same public API you do. For PrestaShop we show how to connect a store directly.
WooCommerce: the KSeF Faktury plugin
The "KSeF Faktury for WooCommerce" plugin is an ordinary client of this API. It passed tests on KSeF TEST (classic and block checkout, refunds, HPOS); it will be listed on WordPress.org after launch, and early access participants get it sooner.
- A "I want a VAT invoice" box and a NIP field with a checksum check in classic and block checkout. It can also read the NIP from an existing field (e.g.
_billing_nip,vat_number). - Once an order with a NIP is paid, it sends it in the background (Action Scheduler) with an
Idempotency-Keyand retries on network errors, 429 and 5xx. Cash on delivery is sent on completion. - A WooCommerce refund becomes a correction.
- The order screen shows the status, invoice and KSeF numbers and PDF links; the buyer sees the PDF on the order page.
- Only orders with a NIP leave the store, with only the data needed: no IP address, user agent, phone or customer id.
- Requirements: PHP 7.4+, WooCommerce 8.9+, HPOS compatible.
The plugin sends the order in the WooCommerce REST API v3 format as it is. You can do the same in your own code:
curl -X POST https://app.ksefshop.pl/v1/invoices \
-H "Authorization: Bearer $KSEF_API_KEY" \
-H "Idempotency-Key: woo-order-727" \
-H "Content-Type: application/json" \
-d '{
"woocommerce_order": {
"id": 727,
"number": "727",
"status": "processing",
"currency": "PLN",
"date_created_gmt": "2026-10-19T09:12:40",
"date_paid_gmt": "2026-10-19T09:13:05",
"billing": {
"first_name": "Jan",
"last_name": "Kowalski",
"company": "Testowa Firma sp. z o.o.",
"address_1": "ul. Prosta 20",
"city": "Warszawa",
"postcode": "00-850",
"country": "PL",
"email": "jan@example.com"
},
"payment_method": "cod",
"meta_data": [
{
"key": "_ksef_buyer_nip",
"value": "5260250274"
}
],
"line_items": [
{
"id": 41,
"name": "Kubek ceramiczny 350 ml",
"quantity": 2,
"subtotal": "79.67",
"subtotal_tax": "18.33",
"total": "79.67",
"total_tax": "18.33",
"taxes": [
{
"id": 1,
"total": "18.33"
}
]
}
],
"tax_lines": [
{
"id": 51,
"rate_id": 1,
"rate_percent": 23
}
],
"shipping_lines": []
}
}'<?php
$payload = [
'woocommerce_order' => [
'id' => 727,
'number' => '727',
'status' => 'processing',
'currency' => 'PLN',
'date_created_gmt' => '2026-10-19T09:12:40',
'date_paid_gmt' => '2026-10-19T09:13:05',
'billing' => [
'first_name' => 'Jan',
'last_name' => 'Kowalski',
'company' => 'Testowa Firma sp. z o.o.',
'address_1' => 'ul. Prosta 20',
'city' => 'Warszawa',
'postcode' => '00-850',
'country' => 'PL',
'email' => 'jan@example.com',
],
'payment_method' => 'cod',
'meta_data' => [
['key' => '_ksef_buyer_nip', 'value' => '5260250274'],
],
'line_items' => [
[
'id' => 41,
'name' => 'Kubek ceramiczny 350 ml',
'quantity' => 2,
'subtotal' => '79.67',
'subtotal_tax' => '18.33',
'total' => '79.67',
'total_tax' => '18.33',
'taxes' => [
['id' => 1, 'total' => '18.33'],
],
],
],
'tax_lines' => [
['id' => 51, 'rate_id' => 1, 'rate_percent' => 23],
],
'shipping_lines' => [],
],
];
$ch = curl_init('https://app.ksefshop.pl/v1/invoices');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('KSEF_API_KEY'),
'Idempotency-Key: woo-order-727',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message']);
}
echo var_export($data['id'], true), ' ', var_export($data['status'], true), PHP_EOL;const res = await fetch("https://app.ksefshop.pl/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KSEF_API_KEY}`,
"Idempotency-Key": "woo-order-727",
"Content-Type": "application/json",
},
body: JSON.stringify({
woocommerce_order: {
id: 727,
number: "727",
status: "processing",
currency: "PLN",
date_created_gmt: "2026-10-19T09:12:40",
date_paid_gmt: "2026-10-19T09:13:05",
billing: {
first_name: "Jan",
last_name: "Kowalski",
company: "Testowa Firma sp. z o.o.",
address_1: "ul. Prosta 20",
city: "Warszawa",
postcode: "00-850",
country: "PL",
email: "jan@example.com",
},
payment_method: "cod",
meta_data: [
{ key: "_ksef_buyer_nip", value: "5260250274" },
],
line_items: [
{
id: 41,
name: "Kubek ceramiczny 350 ml",
quantity: 2,
subtotal: "79.67",
subtotal_tax: "18.33",
total: "79.67",
total_tax: "18.33",
taxes: [
{ id: 1, total: "18.33" },
],
},
],
tax_lines: [
{ id: 51, rate_id: 1, rate_percent: 23 },
],
shipping_lines: [],
},
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.id, data.status);import os
import requests
resp = requests.post(
"https://app.ksefshop.pl/v1/invoices",
headers={
"Authorization": f"Bearer {os.environ['KSEF_API_KEY']}",
"Idempotency-Key": "woo-order-727",
},
json={
"woocommerce_order": {
"id": 727,
"number": "727",
"status": "processing",
"currency": "PLN",
"date_created_gmt": "2026-10-19T09:12:40",
"date_paid_gmt": "2026-10-19T09:13:05",
"billing": {
"first_name": "Jan",
"last_name": "Kowalski",
"company": "Testowa Firma sp. z o.o.",
"address_1": "ul. Prosta 20",
"city": "Warszawa",
"postcode": "00-850",
"country": "PL",
"email": "jan@example.com",
},
"payment_method": "cod",
"meta_data": [
{"key": "_ksef_buyer_nip", "value": "5260250274"},
],
"line_items": [
{
"id": 41,
"name": "Kubek ceramiczny 350 ml",
"quantity": 2,
"subtotal": "79.67",
"subtotal_tax": "18.33",
"total": "79.67",
"total_tax": "18.33",
"taxes": [
{"id": 1, "total": "18.33"},
],
},
],
"tax_lines": [
{"id": 51, "rate_id": 1, "rate_percent": 23},
],
"shipping_lines": [],
},
},
timeout=30,
)
data = resp.json()
if not resp.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["id"], data["status"])We read the NIP from meta_data: _ksef_buyer_nip (the plugin's field), the block checkout field, _billing_nip or vat_number. The VAT rate comes from tax_lines.rate_percent.
PrestaShop: directly through the API
There is no ready PrestaShop module yet. Until then a PrestaShop store connects to the API directly: once payment is confirmed (the actionPaymentConfirmation hook) it sends the order in the neutral format. Below is a module sketch for your developer to finish.
<?php
// modules/ksefapi/ksefapi.php - sketch: paid orders with a NIP -> POST /v1/invoices
class KsefApi extends Module
{
public function __construct()
{
$this->name = 'ksefapi';
$this->version = '0.1.0';
$this->author = 'Your company';
parent::__construct();
$this->displayName = 'KSeF Faktury API';
}
public function install()
{
return parent::install() && $this->registerHook('actionPaymentConfirmation');
}
public function hookActionPaymentConfirmation(array $params)
{
$order = new Order((int) $params['id_order']);
$address = new Address((int) $order->id_address_invoice);
if (trim((string) $address->vat_number) === '') {
return; // consumer without a NIP: no KSeF invoice
}
$lines = [];
foreach ($order->getProducts() as $p) {
$lines[] = [
'name' => $p['product_name'],
'sku' => $p['product_reference'],
'quantity' => (int) $p['product_quantity'],
'unit_price' => number_format((float) $p['unit_price_tax_incl'], 2, '.', ''),
'vat_rate' => (string) (int) round((float) $p['tax_rate']),
];
}
$shipping = [];
if ((float) $order->total_shipping_tax_incl > 0) {
$shipping[] = [
'name' => (new Carrier((int) $order->id_carrier))->name,
'price' => number_format((float) $order->total_shipping_tax_incl, 2, '.', ''),
'vat_rate' => (string) (int) round((float) $order->carrier_tax_rate),
];
}
$payload = ['order' => [
'external_id' => 'PS-' . $order->id,
'number' => $order->reference,
'paid_at' => gmdate('c'),
'currency' => (new Currency((int) $order->id_currency))->iso_code,
'buyer' => [
'nip' => $address->vat_number,
'name' => $address->company ?: trim($address->firstname . ' ' . $address->lastname),
'address_line1' => $address->address1,
'address_line2' => trim($address->postcode . ' ' . $address->city),
'email' => (new Customer((int) $order->id_customer))->email,
],
'lines' => $lines,
'shipping' => $shipping,
]];
// In production send from a queue or cron and retry with the same key.
$ch = curl_init('https://app.ksefshop.pl/v1/invoices');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . Configuration::get('KSEFAPI_KEY'),
'Idempotency-Key: order-PS-' . $order->id,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$invoice = json_decode((string) curl_exec($ch), true);
curl_close($ch);
if (isset($invoice['id'])) {
Configuration::updateValue('KSEFAPI_INV_' . $order->id, $invoice['id']);
}
}
}Before production:
- Send from a queue or cron with retries rather than during the payment. Pattern: idempotency.
- Spread cart rules over the lines as
discount. - Send refunds (e.g. the
actionOrderSlipAddhook) toPOST /v1/invoices/{id}/corrections. Corrections - Store the KSeF number and
pdf_urlon the order after theinvoice.acceptedevent. Webhooks
Other systems
The API takes an order from any system that knows the order is paid: your own store, an ERP or a marketplace integration. A Shopify store does not need the API: the KSeF Faktury app is enough.