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:
| Artifact | Endpoint | Format | Description |
|---|---|---|---|
| QR Code | GET /api/invoices/{id}/qr | PNG (490×490) | Encodes identifier URL |
| XML | GET /api/invoices/{id}/xml | XML | FA(3) invoice as sent to KSeF |
| UPO | POST /api/invoice-status/{id}/download-upo | XML | Official Receipt from KSeF |
| Invoice data | GET /api/invoices/{id} | JSON | All 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):
| Parameter | Default | Description |
|---|---|---|
max_attempts | 20 | Maximum poll attempts |
initial_delay | 2.0s | First delay between polls |
max_delay | 60.0s | Maximum delay between polls |
backoff_factor | 1.5 | Multiplier for each attempt |
timeout | 600.0s | Total 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/jsonRequest:
{
"invoice_ids": [
"698ac8e1-2355-4e08-bda1-b29054263fee",
"1d69f57c-3c5e-4928-840c-9920e787adcc",
"5bf577a5-3335-4c63-a5e0-5d8248b10c67"
]
}| Field | Type | Required | Description |
|---|---|---|---|
invoice_ids | array | ✅ | Up to 100 invoice UUIDs |
company_id | UUID | ❌ | 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.png3.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:
| Status | qr_label value | Meaning |
|---|---|---|
sending (before KSeF confirms) | "OFFLINE" | Invoice not yet confirmed by KSeF |
registered (after KSeF confirms) | "8133793363-20260314-571528C00000-8E" | KSeF number — invoice verified |
OFFLINElabel means the QR code was generated before KSeF assigned a number. The QR still contains the valididentifierURL. Once registered,qr_labelupdates to the KSeF number for printing on invoices.
Invoice Links
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/Q6MltDPzEhCc4CVoyYRNB9HP9gl0XbYfFokTE2qmL9QFormat: {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-downloadFormat: {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:
a0c849e45fb31c5c00d2f1c513de11d38a6d524c3ed10cafdc958f1234d5592eUsed 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-upoendpoint 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:
| Field | queued | sending | registered | failed |
|---|---|---|---|---|
invoice_id | ✅ | ✅ | ✅ | ✅ |
status | ✅ | ✅ | ✅ | ✅ |
ksef_reference_number | ❌ | ✅ | ✅ | ✅/❌ |
ksef_number | ❌ | ❌ | ✅ | ❌ |
ksef_url | ❌ | ❌ | ✅ | ❌ |
identifier | ❌ | ✅ | ✅ | ❌ |
qr_code | ❌ | ✅ | ✅ | ❌ |
qr_label | ❌ | OFFLINE | ksef_number | ❌ |
fingerprint | ❌ | ✅ | ✅ | ❌ |
sent_at | ❌ | ✅ | ✅ | ✅ |
registered_at | ❌ | ❌ | ✅ | ❌ |
error_message | ❌ | ❌ | ❌ | ✅ |
Recommended Polling Strategy
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
| HTTP | Error | Cause |
|---|---|---|
| 200 | — | Status retrieved / UPO downloaded |
| 202 | — | Background poll started |
| 401 | Not authenticated | Missing/expired JWT |
| 404 | Invoice {id} not found | Invalid invoice_id |
| 404 | UPO not found for invoice {id} | UPO not yet downloaded from KSeF |
Endpoint Reference
Status Polling
| Method | Endpoint | KSeF 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
| Method | Endpoint | Format | Description |
|---|---|---|---|
| GET | /api/invoices/{id}/qr | PNG | QR code image (490×490) |
| GET | /api/invoices/{id}/xml | XML | FA(3) invoice XML |
| GET | /api/invoices/{id}/upo | XML/JSON | Stored UPO |
| POST | /api/invoice-status/{id}/download-upo | JSON | Download UPO from KSeF |
Invoice Data
| Method | Endpoint | Description |
|---|---|---|
| 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