My App

Status, QR Codes & UPO

Invoice status tracking, QR code generation, UPO retrieval, and polling strategies.


Overview

After submitting an invoice, you need to track its progress through KSeF and retrieve artifacts (QR code, UPO, links). This process covers everything that happens after submission.

Submit invoice
     │
     ▼
┌──────────┐     ┌──────────┐     ┌────────────────┐
│ QUEUED/  │────▶│ SENDING  │────▶│  REGISTERED    │
│ SENDING  │     │          │     │                │
└──────────┘     └──────────┘     │  ksef_number ✅│
     │                            │  qr_code ✅   │
     │                            │  fingerprint ✅│
     │                            │  ksef_url ✅  │
     ▼                            │  upo ✅       │
┌──────────┐                      └────────────────┘
│  FAILED  │
│error_msg │
└──────────┘

Available artifacts after registration:

ArtifactEndpointFormatDescription
QR CodeGET /api/invoices/{id}/qrPNG (490×490)Encodes identifier URL
XMLGET /api/invoices/{id}/xmlXMLFA(3) invoice as sent to KSeF
UPOPOST /api/invoice-status/{id}/download-upoXMLOfficial Receipt from KSeF
Invoice dataGET /api/invoices/{id}JSONAll metadata + links

Status Polling

2.1 Get Status from Database (Fast, No KSeF Call)

GET /api/invoice-status/{invoice_id}/status
Authorization: Bearer {token}

Reads current status from the Gateway database. No KSeF API call — instant response. Use this for dashboards and frequent checks.

Response (200):

{
  "invoice_id": "f7ffe8f7-54fa-4376-899e-3454ca2e393b",
  "ksef_reference_number": "20260314-EE-27167B4000-D5DE143A4C-FA",
  "status": "registered",
  "ksef_number": "8133793363-20260314-571528C00000-8E",
  "processing_code": null,
  "processing_description": null,
  "timestamp": "2026-03-14T11:23:15.250912Z"
}

2.2 Poll Status from KSeF (Active Check)

POST /api/invoice-status/{invoice_id}/poll-status
Authorization: Bearer {token}

Also available as GET (same behavior).

Calls the KSeF API to check actual status. Uses exponential backoff internally (2s → 3s → 4.5s → ... max 60s). Updates the database with the result.

Response (200):

{
  "invoice_id": "f7ffe8f7-54fa-4376-899e-3454ca2e393b",
  "ksef_reference_number": "20260314-EE-27167B4000-D5DE143A4C-FA",
  "status": "registered",
  "ksef_number": "8133793363-20260314-571528C00000-8E",
  "processing_code": "200",
  "processing_description": "Zarejestrowano (potwierdzone przez metadata query)",
  "timestamp": "2026-03-14T16:26:22.051554Z"
}

Polling configuration (defaults):

ParameterDefaultDescription
max_attempts20Maximum poll attempts
initial_delay2.0sFirst delay between polls
max_delay60.0sMaximum delay between polls
backoff_factor1.5Multiplier for each attempt
timeout600.0sTotal timeout (10 minutes)

Backoff sequence: 2s → 3s → 4.5s → 6.75s → 10.1s → 15.2s → 22.8s → 34.2s → 51.3s → 60s → 60s ...

2.3 Poll Status in Background (Async)

POST /api/invoice-status/{invoice_id}/poll-status-async
Authorization: Bearer {token}

Starts a background task to poll KSeF. Returns immediately.

Response (202):

{
  "message": "Status polling started in background",
  "invoice_id": "f7ffe8f7-54fa-4376-899e-3454ca2e393b"
}

Use GET /api/invoice-status/{id}/status to check when the background poll completes.

2.4 Bulk Poll (Multiple Invoices)

POST /api/invoice-status/bulk-poll-status
Authorization: Bearer {token}
Content-Type: application/json

Request:

{
  "invoice_ids": [
    "698ac8e1-2355-4e08-bda1-b29054263fee",
    "1d69f57c-3c5e-4928-840c-9920e787adcc",
    "5bf577a5-3335-4c63-a5e0-5d8248b10c67"
  ]
}
FieldTypeRequiredDescription
invoice_idsarray✅Up to 100 invoice UUIDs
company_idUUID❌Filter by company (query param)

Response (200):

{
  "total": 3,
  "processing": 0,
  "registered": 3,
  "failed": 0,
  "results": [
    {
      "invoice_id": "698ac8e1-2355-4e08-bda1-b29054263fee",
      "status": "registered",
      "ksef_number": "8133793363-20260314-4B85A3000000-CE"
    },
    {
      "invoice_id": "1d69f57c-3c5e-4928-840c-9920e787adcc",
      "status": "registered",
      "ksef_number": "8133793363-20260314-4CCA28C00000-32"
    },
    {
      "invoice_id": "5bf577a5-3335-4c63-a5e0-5d8248b10c67",
      "status": "registered",
      "ksef_number": "8133793363-20260314-4FFAA8C00000-2E"
    }
  ]
}

Summary counts (total, processing, registered, failed) make it easy to check if all invoices are done without iterating results.


QR Codes

3.1 Download QR Code

GET /api/invoices/{invoice_id}/qr
Authorization: Bearer {token}

Response: PNG image (490×490 pixels, 1-bit grayscale, ~1.1 KB).

The QR code encodes the identifier URL — when scanned, it opens the invoice in the KSeF portal.

# Download QR as file
curl -s https://demo.ksefapi.com/api/invoices/{id}/qr \
  -H "Authorization: Bearer $TOKEN" \
  -o invoice_qr.png

3.2 QR in Invoice Response

When you retrieve an invoice via GET /api/invoices/{id}, the response includes qr_code as Base64-encoded PNG:

{
  "qr_code": "iVBORw0KGgo...(base64 PNG)",
  "qr_url": "/api/invoices/{id}/qr"
}

3.3 QR Label Lifecycle

The qr_label field changes based on invoice status:

Statusqr_label valueMeaning
sending (before KSeF confirms)"OFFLINE"Invoice not yet confirmed by KSeF
registered (after KSeF confirms)"8133793363-20260314-571528C00000-8E"KSeF number — invoice verified

OFFLINE label means the QR code was generated before KSeF assigned a number. The QR still contains the valid identifier URL. Once registered, qr_label updates to the KSeF number for printing on invoices.


After registration, each invoice has three URLs:

4.1 identifier

The canonical invoice identifier, encoded in the QR code:

https://qr-demo.ksef.mf.gov.pl/invoice/8133793363/14-03-2026/Q6MltDPzEhCc4CVoyYRNB9HP9gl0XbYfFokTE2qmL9Q

Format: {portal}/invoice/{NIP}/{date}/{hash}

Available immediately after submission (before KSeF registration).

4.2 ksef_url

Full verification link including KSeF number:

https://qr-demo.ksef.mf.gov.pl/client-app/invoice/8133793363/14-03-2026/Q6MltDPzEhCc.../8133793363-20260314-571528C00000-8E/verify-download

Format: {portal}/client-app/invoice/{NIP}/{date}/{hash}/{ksef_number}/verify-download

Available only after KSeF registration (when ksef_number is assigned).

4.3 fingerprint

SHA-256 hash of the invoice XML content:

a0c849e45fb31c5c00d2f1c513de11d38a6d524c3ed10cafdc958f1234d5592e

Used for verification — proves the XML hasn't been tampered with after submission.

4.4 API Resource URLs

Relative URLs for downloading invoice artifacts:

{
  "xml_url": "/api/invoices/{id}/xml",
  "qr_url": "/api/invoices/{id}/qr",
  "upo_url": "/api/invoice-status/{id}/download-upo"
}

UPO (Official Receipt)

UPO (Urzędowe Poświadczenie Odbioru) is the official KSeF receipt confirming invoice registration. It has legal significance — proof that the invoice was accepted by the tax authority.

5.1 Download UPO

POST /api/invoice-status/{invoice_id}/download-upo
Authorization: Bearer {token}

Response (200):

{
  "invoice_id": "698ac8e1-2355-4e08-bda1-b29054263fee",
  "ksef_reference_number": "20260314-EE-21715A2000-D05678D784-5A",
  "ksef_number": "8133793363-20260314-4B85A3000000-CE",
  "upo_xml": "<?xml version='1.0'?>...(~7300 chars of UPO XML)...",
  "downloaded_at": "2026-03-14T09:45:00Z"
}

5.2 Get UPO via Invoice Endpoint

GET /api/invoices/{invoice_id}/upo
Authorization: Bearer {token}

Returns stored UPO XML directly.

Error (404): "UPO not found for invoice {id}" — UPO hasn't been downloaded yet or session expired before download.

5.3 UPO Availability

UPO can be downloaded when:

  • Invoice status is registered
  • A KSeF session is active (or was active when the invoice was processed)

Known limitation: If the KSeF session expires (~1 hour) before UPO is downloaded, the download-upo endpoint may return empty data. The Worker automatically downloads UPO immediately after registration, so this is rare. If UPO is missing, open a new session (Sessions) and retry the download.


Full Invoice Response (All Fields)

GET /api/invoices/{invoice_id} returns the complete picture:

{
  "invoice_id": "f7ffe8f7-54fa-4376-899e-3454ca2e393b",
  "company_id": "22919080-f3fe-4aff-8d71-f6493afc956a",
  "invoice_number": "FV/WEBHOOK/2026/03/001",
  "invoice_type_code": "VAT",
  "invoice_date": "2026-03-14",
  "sale_date": "2026-03-14",
  "status": "registered",
  "ksef_number": "8133793363-20260314-571528C00000-8E",
  "ksef_reference_number": "20260314-EE-27167B4000-D5DE143A4C-FA",
  "ksef_url": "https://qr-demo.ksef.mf.gov.pl/client-app/invoice/.../verify-download",
  "identifier": "https://qr-demo.ksef.mf.gov.pl/invoice/8133793363/14-03-2026/oMhJ5F-...",
  "qr_label": "8133793363-20260314-571528C00000-8E",
  "fingerprint": "a0c849e45fb31c5c00d2f1c513de11d38a6d524c3ed10cafdc958f1234d5592e",
  "xml_url": "/api/invoices/f7ffe8f7-.../xml",
  "qr_url": "/api/invoices/f7ffe8f7-.../qr",
  "upo_url": "/api/invoice-status/f7ffe8f7-.../download-upo",
  "created_at": "2026-03-14T11:23:06.425056Z",
  "sent_at": "2026-03-14T11:23:06.741483Z",
  "registered_at": "2026-03-14T16:26:36.675863Z",
  "error_code": null,
  "error_message": null
}

Field availability by status:

Fieldqueuedsendingregisteredfailed
invoice_id✅✅✅✅
status✅✅✅✅
ksef_reference_number❌✅✅✅/❌
ksef_number❌❌✅❌
ksef_url❌❌✅❌
identifier❌✅✅❌
qr_code❌✅✅❌
qr_label❌OFFLINEksef_number❌
fingerprint❌✅✅❌
sent_at❌✅✅✅
registered_at❌❌✅❌
error_message❌❌❌✅

For single invoice (sync submission)

# Submit sync → get reference number immediately
RESP=$(curl -s -X POST "$BASE/api/invoices/submit-sync" ...)
INV_ID=$(echo "$RESP" | jq -r '.invoice_id')

# Wait 3-5 seconds, then check DB status
sleep 5
curl -s "$BASE/api/invoice-status/$INV_ID/status" \
  -H "Authorization: Bearer $TOKEN" | jq '{status, ksef_number}'

For async submission

# Submit async → get invoice_id
RESP=$(curl -s -X POST "$BASE/api/invoices" ...)
INV_ID=$(echo "$RESP" | jq -r '.invoice_id')

# Option A: Active poll (blocks until final status)
curl -s -X POST "$BASE/api/invoice-status/$INV_ID/poll-status" \
  -H "Authorization: Bearer $TOKEN"

# Option B: Background poll + check later
curl -s -X POST "$BASE/api/invoice-status/$INV_ID/poll-status-async" \
  -H "Authorization: Bearer $TOKEN"
# ... do other work ...
sleep 10
curl -s "$BASE/api/invoice-status/$INV_ID/status" \
  -H "Authorization: Bearer $TOKEN"

For batch

# After batch completes, bulk-poll all invoice IDs
IDS=$(curl -s "$BASE/api/batch/$BATCH_ID/items" \
  -H "Authorization: Bearer $TOKEN" | jq '[.items[].invoice_id]')

curl -s -X POST "$BASE/api/invoice-status/bulk-poll-status" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"invoice_ids\": $IDS}"

Best practice: Use webhooks

Instead of polling, set up an outgoing webhook (Process 12) to receive invoice.registered and invoice.failed events. This eliminates polling entirely.


Error Responses

HTTPErrorCause
200—Status retrieved / UPO downloaded
202—Background poll started
401Not authenticatedMissing/expired JWT
404Invoice {id} not foundInvalid invoice_id
404UPO not found for invoice {id}UPO not yet downloaded from KSeF

Endpoint Reference

Status Polling

MethodEndpointKSeF Call?Description
GET/api/invoice-status/{id}/status❌Read from DB only (fast)
POST/api/invoice-status/{id}/poll-status✅Active poll with backoff
GET/api/invoice-status/{id}/poll-status✅Active poll (GET variant)
POST/api/invoice-status/{id}/poll-status-async✅ (background)Returns 202 immediately
POST/api/invoice-status/bulk-poll-status✅Poll up to 100 invoices

Artifacts

MethodEndpointFormatDescription
GET/api/invoices/{id}/qrPNGQR code image (490×490)
GET/api/invoices/{id}/xmlXMLFA(3) invoice XML
GET/api/invoices/{id}/upoXML/JSONStored UPO
POST/api/invoice-status/{id}/download-upoJSONDownload UPO from KSeF

Invoice Data

MethodEndpointDescription
GET/api/invoices/{id}Full invoice details + all links

Complete Example

#!/bin/bash
TOKEN="your_jwt_token"
BASE="https://demo.ksefapi.com"
INVOICE_ID="your_invoice_id"

# 1. Quick DB status check
curl -s "$BASE/api/invoice-status/$INVOICE_ID/status" \
  -H "Authorization: Bearer $TOKEN" | jq '{status, ksef_number}'

# 2. If still processing, active poll
curl -s -X POST "$BASE/api/invoice-status/$INVOICE_ID/poll-status" \
  -H "Authorization: Bearer $TOKEN" | jq '{status, ksef_number, processing_description}'

# 3. Get full invoice with all links
curl -s "$BASE/api/invoices/$INVOICE_ID" \
  -H "Authorization: Bearer $TOKEN" | jq '{ksef_number, ksef_url, identifier, qr_label, fingerprint}'

# 4. Download QR code
curl -s "$BASE/api/invoices/$INVOICE_ID/qr" \
  -H "Authorization: Bearer $TOKEN" -o invoice_qr.png

# 5. Download XML
curl -s "$BASE/api/invoices/$INVOICE_ID/xml" \
  -H "Authorization: Bearer $TOKEN" -o invoice.xml

# 6. Download UPO
curl -s -X POST "$BASE/api/invoice-status/$INVOICE_ID/download-upo" \
  -H "Authorization: Bearer $TOKEN" | jq '{ksef_number, upo_xml_length: (.upo_xml | length)}'

Known Issues

UPO availability after session expiry: If the KSeF session expires (~1h) before UPO is retrieved, the download-upo endpoint may return empty data. Workaround: open a new session and retry, or rely on the Worker's automatic UPO download immediately after registration.

Bulk poll webhook interaction: When bulk-polling invoices that have already triggered outgoing webhooks, the endpoint may attempt to create duplicate webhook deliveries. This is a known idempotency edge case being fixed.

Next steps:

  • Search invoices in KSeF → Endpoints
  • Set up webhook notifications instead of polling → Process 12

On this page