Integration guide

KSeF API PHP integration guide

Send a KSeF FA(3) invoice from PHP with the Notaly API using cURL: POST /invoices, error handling and status tracking.

Updated

Overview

PHP remains one of the most common languages for invoicing backends, and Notaly lets you plug KSeF into an existing PHP application in a few lines. You send invoice data as a JSON array to https://api.getnotaly.com/invoices; Notaly produces the FA(3) XML, checks it against the official schema and submits it to KSeF. The example below uses PHP's built-in cURL extension, so it works without Composer or any third-party package.

Prerequisites

Sign up for a free sandbox account and copy the API key. The sandbox only accepts the seller NIP 2745013933 (the platform's test NIP); a different NIP triggers 422 seller_nip_not_authorized without using one of your three free test invoices. Store the key in an environment variable read with getenv().

Submit an invoice

<?php
$key = getenv('NOTALY_API_KEY');

$invoice = [
    'number' => 'FV/2026/10/001',
    'issueDate' => '2026-10-12',
    'currency' => 'PLN',
    'seller' => ['nip' => '2745013933', 'name' => 'Example Sp. z o.o. (FICTIONAL)', 'address' => ['line1' => 'ul. Testowa 1, 00-001 Warszawa', 'country' => 'PL']],
    'buyer' => ['nip' => '1234563218', 'name' => 'Test Customer S.A. (FICTIONAL)', 'address' => ['line1' => 'ul. Probna 2, 30-001 Kraków', 'country' => 'PL']],
    'items' => [
        ['name' => 'SaaS subscription - October', 'unit' => 'pcs.', 'quantity' => 1, 'unitNetPrice' => 1000, 'vatRate' => 23],
        ['name' => 'Support', 'unit' => 'hrs.', 'quantity' => 2, 'unitNetPrice' => 150.5, 'vatRate' => 8],
    ],
];

$ch = curl_init('https://api.getnotaly.com/invoices');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $key,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($invoice),
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$created = json_decode($body, true);
if ($status >= 400) {
    fwrite(STDERR, $status . ': ' . ($created['error'] ?? 'unknown') . PHP_EOL);
    exit(1);
}
echo 'Invoice ' . $created['id'] . ' is ' . $created['status'] . PHP_EOL;

curl_setopt_array configures the request in one place: POST method, JSON headers and the encoded payload. After the call, curl_getinfo($ch, CURLINFO_RESPONSE_CODE) gives you the HTTP status so you can separate success from failure before decoding the body.

Interpret the reply

On success you get HTTP 201 and a JSON object. status is submitted if KSeF accepted the submission, or draft if KSeF was temporarily unavailable and the invoice was saved for retry. The id field is what you use later to poll status; accepted invoices also expose upo and pdfUrl. Notaly validates and submits the invoice; VAT rates and tax classification remain your responsibility.

Deal with errors

Read the structured error field instead of matching human-readable text. validation_failed (422) includes a details array with the exact offending fields. quota_exceeded (402) means the sandbox allowance of three invoices is used. 401 is a missing or invalid key, 404 is a valid key for the wrong resource, and 503 ksef_unavailable is a transient condition worth retrying with backoff.

Follow the invoice to completion

Poll GET /invoices/{id} with the same bearer token, or set up webhooks. The lifecycle is draft → submitted → accepted or rejected; a rejected invoice carries an error code such as ksef_rejected:<code>.

Sources

Source: getnotaly.com

Frequently asked questions

Which PHP version and extensions do I need?

PHP 7.4+ with the cURL extension enabled, which is standard in most environments.

Can I use Guzzle instead of cURL?

Yes. Any HTTP client works as long as it sends JSON with the Authorization: Bearer header.

Issue your first KSeF test invoice today

Create a sandbox account: API key in seconds, 3 free test invoices on KSeF TEST, no credit card.