Overview
Node.js is the quickest way to get your first KSeF invoice into the Polish e-invoicing system through Notaly. You describe the invoice as plain JSON, send it in one POST request, and Notaly takes care of the rest: it generates the FA(3) XML, validates it against the official schema and submits it to KSeF. This page gives you a complete, runnable example using the global fetch that has shipped with Node.js since version 18, so there is nothing to install.
Before you start
Create a free sandbox account and copy your API key from the dashboard. Two details matter in the sandbox. First, the seller NIP is fixed to 2745013933, the test NIP bound to Notaly's test certificate; sending a different seller NIP returns 422 seller_nip_not_authorized before the invoice reaches KSeF and before your three free test invoices are consumed. Second, the Idempotency-Key header is how you make retries safe: resending the same key returns the same invoice instead of a duplicate.
Send the invoice
const NOTALY_API_KEY = process.env.NOTALY_API_KEY;
const 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 }
]
};
const response = await fetch("https://api.getnotaly.com/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${NOTALY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID()
},
body: JSON.stringify(invoice)
});
if (!response.ok) {
const body = await response.json();
console.error(`${response.status}:`, body.error, body.details ?? body.message ?? "");
process.exit(1);
}
const created = await response.json();
console.log(`Invoice ${created.id} is ${created.status}`);Read the response
A successful call returns HTTP 201 with the invoice object. The most important field is status. It is submitted when KSeF has accepted the submission, or draft when KSeF was temporarily unavailable and the invoice was saved so you can retry later. The response also carries id (use it to fetch status), ksefReferenceNumber, and, once the invoice is accepted, upo and pdfUrl. Notaly validates and submits the invoice; VAT rates and tax classification remain your responsibility.
Handle the common errors
Notaly returns structured errors you can branch on. A 422 validation_failed includes a details array telling you exactly which field is wrong. 402 quota_exceeded means you have used your three free sandbox invoices. 401 means the API key is missing or invalid and 404 means the key is valid but the resource is not yours. A 503 ksef_unavailable is transient; retry with backoff.
Track the result
const res = await fetch(`https://api.getnotaly.com/invoices/${created.id}`, {
headers: { Authorization: `Bearer ${NOTALY_API_KEY}` }
});
const invoice = await res.json();
console.log(invoice.status); // draft | submitted | accepted | rejectedThe lifecycle is draft → submitted → accepted or rejected. You can poll GET /invoices/:id, or subscribe to webhooks so Notaly pushes each status change to your endpoint with a signed header.
Sources
Source: getnotaly.com