API reference

Notaly API reference

Everything you need to issue e-invoices to KSeF from your code: authentication, the invoice object, statuses, file downloads, signed webhooks and errors. JSON over HTTPS, one API key.

Updated 8 min read

Base URL and authentication

All requests go to https://api.getnotaly.com over HTTPS with JSON bodies. Authenticate with your API key in either header:

headers
Authorization: Bearer $NOTALY_API_KEY
# or
X-API-Key: $NOTALY_API_KEY

You get a key when you sign up; create, rotate and revoke keys in the dashboard. The full key is shown once, and only its hash is stored. Requests are limited to 60 per minute per key; above that the API answers 429 with Retry-After: 60.

Create an invoice

POST/invoices

Validates the payload, generates FA(3) XML and submits it to KSeF. Returns 201 with the invoice object. Send an Idempotency-Key header to make retries safe: the same key returns the original invoice instead of creating a new one.

FieldTypeNotes
numberstringYour invoice number, e.g. FV/2026/10/001
issueDatestringYYYY-MM-DD
currencystringISO 4217, e.g. PLN
seller, buyerobjectnip (10 digits, valid checksum), name, address.line1, optional address.line2, address.country (ISO 3166 alpha-2)
items[]array1–1000 lines: name, optional unit, quantity, unitNetPrice, vatRate (23, 8, 5 or 0)
POST/invoices201 Created
{
  "id": "inv_8f2c41d0a9b34e7c9d1e2f3a4b5c6d7e",
  "status": "submitted",
  "number": "FV/2026/10/001",
  "ksefReferenceNumber": "20261012-EE-2A41B3C7D9-1F",
  "ksefNumber": null,
  "upo": null,
  "pdfUrl": "/invoices/inv_8f2c…/pdf",
  "error": null,
  "history": [ { "status": "draft", "at": "…", "source": "api" },
               { "status": "submitted", "at": "…", "source": "ksef" } ],
  "createdAt": "2026-10-12T07:23:07.000Z",
  "updatedAt": "2026-10-12T07:23:08.000Z"
}

If KSeF is unavailable, the invoice is stored as draft with "error": "ksef_unavailable", and no data is lost.

Retrieve an invoice

GET/invoices/{id}

Returns the same invoice object. While the invoice is submitted, this call also checks KSeF and records the decision, so polling it is a valid alternative to webhooks.

StatusMeaning
draftXML generated, not sent to KSeF yet
submittedSent to KSeF; stays here while KSeF processes it
acceptedAccepted: ksefNumber and upo are set
rejectedRejected: error holds a stable code such as ksef_rejected:<code>

Download XML and PDF

GET/invoices/{id}/xml

The exact FA(3) XML sent to KSeF (application/xml). See the FA(3) XML guide for its structure.

GET/invoices/{id}/pdf

A PDF visualisation (application/pdf) with the KSeF verification QR code (KOD I). The legal invoice is the XML; the PDF is for people.

Webhooks

Add an HTTPS endpoint in the dashboard under Webhooks. Its signing secret is shown once. Notaly sends a POST when an invoice changes state:

EventWhen
invoice.acceptedKSeF accepted the invoice; includes ksefNumber, upo and an absolute pdfUrl
invoice.rejectedKSeF rejected the invoice; error holds the code
invoice.errorThe invoice could not be processed
POSTyour endpointinvoice.accepted
{
  "id": "evt_inv_8f2c…_accepted",
  "type": "invoice.accepted",
  "createdAt": "2026-10-12T07:24:02.000Z",
  "data": { "invoiceId": "inv_8f2c…", "status": "accepted",
            "ksefReferenceNumber": "20261012-EE-…", "ksefNumber": "2745013933-20261012-…",
            "upo": "…", "error": null, "pdfUrl": "https://api.getnotaly.com/invoices/inv_8f2c…/pdf" }
}

Verify the signature

Each delivery carries X-Webhook-Id, X-Webhook-Timestamp and X-Webhook-Signature: t=<unix>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" keyed with your endpoint secret.

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret: string, rawBody: string, header: string): boolean {
  const p = Object.fromEntries(header.split(",").map((x) => x.split("=", 2)));
  const t = Number(p.t);
  if (!p.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5-minute window
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(p.v1, "hex");
  return got.length === expected.length && timingSafeEqual(got, expected);
}

Retries and delivery

  • Answer with any 2xx to acknowledge. Any other status, a redirect or a timeout is retried.
  • Up to 8 attempts with exponential backoff (30 s, 60 s, 2 min… capped at 1 hour, with jitter). The body and event id stay identical across retries.
  • Delivery is at-least-once: deduplicate on X-Webhook-Id.
  • Endpoints must be public HTTPS URLs; private and internal addresses are refused.

Errors

Errors are JSON with a stable error code. Raw KSeF messages are never returned.

HTTPerrorWhat to do
401unauthorizedMissing or invalid API key
402quota_exceededSandbox used its 3 test invoices; upgrade to Starter or Scale
404not_foundUnknown id, or the invoice was created with a different API key
422validation_failedFix the fields listed in details
422seller_nip_not_authorizedIn the sandbox, seller.nip must be 2745013933
429rate_limitedWait for Retry-After seconds

Invoice-level codes in the error field: ksef_unavailable (KSeF did not answer; the invoice stays draft), ksef_rejected:<code> (rejected by KSeF) and ksef_error.

Sandbox and plans

PlanPriceKSeF environmentInvoices
SandboxFreeTEST, seller NIP 27450139333 in total
Starter€49/monthYour KSeF connectionUp to 200 per month
Scale€199/monthYour KSeF connectionUp to 1,000 per month

Frequently asked questions

How do I authenticate?

Send your API key as Authorization: Bearer <key> (or X-API-Key: <key>). Keys are created at sign-up and in the dashboard; the full key is shown once.

Can I retry POST /invoices safely?

Yes. Send an Idempotency-Key header; repeating the request with the same key returns the original invoice instead of creating a new one.

What are the invoice statuses?

draft → submitted → accepted | rejected. While KSeF processes the submission internally, the invoice stays submitted.

How do I verify a webhook signature?

Compute HMAC-SHA256 of "<t>.<raw body>" with your endpoint secret, compare it in constant time with v1 from the X-Webhook-Signature header and reject timestamps older than 5 minutes.

What are the sandbox limits?

3 free test invoices on KSeF TEST per account, with seller NIP 2745013933. Upgrade to Starter (€49/month) or Scale (€199/month) for more.

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.