Sending Invoices to KSeF
KSeF Pro for Magento 2 generates FA(2) XML from Magento invoice objects, signs them with XAdES-BES digital signatures, and manages the full KSeF session lifecycle using Magento's native cron infrastructure and observer pattern.
Automatic Submission Flow
1. Magento invoice created (Admin UI, REST API, or GraphQL)
2. sales_order_invoice_save_after observer fires
3. Rule engine evaluates invoice (store scope, NIP, currency, customer group)
4. FA(2) XML generated from Magento invoice data
5. XML signed with XAdES-BES (PHP OpenSSL)
6. Job inserted into ksefpl_queue (status: PENDING)
7. Observer returns — invoice save completes instantly
--- Magento Cron (background) ---
8. ksefpl_process_queue (every 2 min): opens KSeF session, submits batch
9. ksefpl_poll_status (every 1 min): polls MF for session processing status
10. ksefpl_download_upo (every 3 min): downloads UPO when processingCode=200
11. KSeF-ID written to ksefpl_invoice_registry and Magento invoice extension attribute
Steps 1–6 complete in milliseconds during the invoice save. Steps 8–11 run asynchronously in the background. The Admin user and REST API caller never wait for KSeF API latency.
FA(2) XML Generation
KSeF Pro maps Magento invoice data to the FA(2) logical structure:
| Magento Source | FA(2) Element | Notes |
|---|---|---|
| Invoice::getCreatedAt() | <DataWystawienia> | Converted to Europe/Warsaw timezone |
| KSeF Pro seller config | <Podmiot1> | NIP, company name, address |
| Order::getCustomTaxvat() + billing address | <Podmiot2> | B2B: NIP; B2C: BRAK identifier |
| InvoiceItem collection | <FaWiersz> rows | One row per SKU |
| Tax amounts by rate | <StawkaPodatku> groups | Aggregated by mapped Polish VAT code |
| Invoice::getGrandTotal() | <P_15> | Total gross in PLN |
| Order::getOrderCurrencyCode() | <KodWaluty> + <KursWaluty> | Non-PLN orders only |
| Invoice::getIncrementId() | <NrFaKorygowanej> | Correction invoices (KOR) only |
Tax Amount Aggregation
Magento stores tax amounts per tax rule — each rule can apply a different rate to different product categories. KSeF Pro aggregates these by the Polish VAT code mapped to each rule:
Magento tax rule "Standard 23%" → mapped to code "23"
Magento tax rule "Software Export 0%" → mapped to code "0"
FA(2) output:
<StawkaPodatku>
<Stawka>23</Stawka>
<Podstawa>1000.00</Podstawa>
<Kwota>230.00</Kwota>
</StawkaPodatku>
<StawkaPodatku>
<Stawka>0</Stawka>
<Podstawa>500.00</Podstawa>
<Kwota>0.00</Kwota>
</StawkaPodatku>
If two Magento tax rules are mapped to the same Polish code, their amounts are summed into a single <StawkaPodatku> group. This is correct — FA(2) requires one group per rate, not per Magento rule.
XML Preview in Admin
Before enabling production, review the generated FA(2) for an existing invoice:
- Go to Sales → Invoices and open any invoice
- Click "Preview FA(2) XML" in the KSeF Pro panel (right column)
- Verify: seller NIP, buyer NIP or
BRAK, line items,<StawkaPodatku>groups, and<P_15>total

Pay particular attention to:
<P_15>matching the Magento invoice grand total (in PLN)- Each
<FaWiersz>row using the correct<Stawka>code <Podmiot1>NIP matching your seller configuration<Podmiot2>containing the buyer NIP for B2B orders
XAdES-BES Signing
Before submission, KSeF Pro signs each FA(2) document with an XAdES-BES enveloped signature:
- Canonicalize the FA(2) XML using the C14N algorithm
- Compute SHA-256 digest of the canonical form
- Derive signing key material from the KSeF token (decrypted via
EncryptorInterface) - Sign with RSA-SHA256 using PHP
openssl_sign() - Embed the
<ds:Signature>block in the FA(2) XML before the closing root tag - Base64-encode the entire signed document for API transport
No qualified certificate (kwalifikowany podpis elektroniczny) is required for invoice signing. The KSeF portal token provides sufficient key material for XAdES-BES. Qualified certificates are only required for token issuance.
The signing service is PlugKit\KsefPl\Service\XmlSigner. If signing fails (error 10100), check PHP OpenSSL:
php -r "echo openssl_get_curve_names() ? 'OpenSSL OK' : openssl_error_string();"
php -m | grep opensslCron-Based Session Lifecycle
ksefpl_process_queue (every 2 minutes)
- Queries
ksefpl_queuefor up to 50PENDINGjobs - Opens a KSeF session:
POST /api/online/Session/InitSigned ← { sessionToken, referenceNumber } - Submits each invoice:
Each
POST /api/online/Session/Invoice/Send → { invoiceHash: {...}, invoicePayload: { type: "plain", invoiceBody: "BASE64_XML" } } ← { elementReferenceNumber }elementReferenceNumberis stored inksefpl_queuealongside the invoice ID. - Closes the session:
POST /api/online/Session/CloseSigned ← { processingCode: 200 } - Updates jobs from
PENDINGtoSUBMITTED - Stores the session
referenceNumberinksefpl_sessions
ksefpl_poll_status (every 1 minute)
For each SUBMITTED session:
GET /api/common/Status/{referenceNumber}
← { processingCode: 200, upoReferenceNumber: "..." } when ready
When processingCode=200, marks the session as ready for UPO download.
ksefpl_download_upo (every 3 minutes)
For each session with UPO available:
GET /api/common/Upo/{upoReferenceNumber}
← UPO XML
- Saves UPO to
var/ksefpl/upo/{year}/{month}/{invoice-id}.xml - Extracts KSeF-IDs for each invoice in the session
- Writes KSeF-IDs to
ksefpl_invoice_registryand to the Magento invoice'sksefpl_ksef_idextension attribute - Updates audit log entries to
ACCEPTED - Dispatches
ksefpl_ksef_id_receivedMagento event (consumed by Fakturownia Pro in Paired Mode)

Session Timeout Risk
KSeF sessions expire after 30 minutes of inactivity. Risk factors:
- Large batches (50 invoices) with slow Magento processing take longer than expected
- MF server 503 responses during maintenance delay session close
Mitigation: enable "One invoice per session" in Advanced settings. Each invoice gets its own session — close happens within seconds of submit, making timeout impossible.
Manual Submission
Submit individual invoices immediately from the Admin:
- Go to Sales → Invoices and open an invoice
- In the KSeF Pro panel, click "Submit to KSeF now"
- KSeF Pro opens a single-invoice session synchronously — the Admin page waits for completion
- Result (KSeF-ID or error code) appears in the panel immediately
Or via CLI:
php bin/magento ksefpl:submit --invoice-id=INV-00001234Credit Memos (Correction Invoices)
When a Magento credit memo is created for an order with a KSeF-ID, KSeF Pro generates a correction invoice (KOR):
<RodzajFaktury>KOR</RodzajFaktury>
<DaneFaKorygowanej>
<DataFaKorygowanej>2025-03-01</DataFaKorygowanej>
<NrFaKorygowanej>ORIGINAL_KSEF_ID</NrFaKorygowanej>
</DaneFaKorygowanej>The correction is submitted to KSeF via the same cron session lifecycle and receives its own KSeF-ID. Both the original and correction appear in the audit log as linked records.
If no KSeF-ID exists for the order (invoice was never submitted or submission failed), the credit memo is queued but cannot be submitted until the original invoice has a KSeF-ID. The queue entry stays in BLOCKED_NO_KSEF_ID status until the parent submission completes.
Magento Cron Configuration
KSeF Pro cron jobs run under Magento's default cron group. Verify cron is active:
# Check job registration
php bin/magento cron:run --group=default --dry-run 2>&1 | grep ksefpl
# View execution history
# Admin: System → Tools → Cron Schedule → filter by job_code = ksefpl_*For production environments, the server crontab must include:
# /etc/cron.d/magento
* * * * * www-data php /var/www/magento/bin/magento cron:run --group=default 2>&1On Adobe Commerce Cloud, Magento cron is configured automatically — no server crontab changes needed.
Cron Queue Diagnostic Commands
# Show queue statistics
php bin/magento ksefpl:queue-status
# Process queue immediately (bypasses cron schedule)
php bin/magento ksefpl:process-queue
# Retry all failed submissions
php bin/magento ksefpl:retry-failed
# Test KSeF connection
php bin/magento ksefpl:test-connectionCurrency Conversion
For non-PLN Magento stores:
| Scenario | KSeF Pro Behaviour |
|---|---|
| PLN order | Amounts used directly; no <KursWaluty> |
| EUR order, rate stored on order | Stored rate used; <KursWaluty> populated |
| EUR order, no stored rate | NBP API fetched at submission time |
| Unsupported currency | Manual fallback rate from Advanced settings; warning logged |
The NBP rate is fetched from https://api.nbp.pl/api/exchangerates/rates/A/{currency}?format=json. Exchange rates are cached in Magento's cache backend for 24 hours. Keep Magento's built-in currency import running (Stores → Currency → Import) to maintain accurate rates in directory_currency_rate.