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