Invoice Submission (Batch)
Bulk invoice submission via ZIP upload, batch sessions, and parallel processing.
Overview
Batch submission handles multiple invoices in a single API call. The Gateway splits them into chunks (max 100 per chunk, up to 3 concurrent), processes them through the Worker, and tracks per-invoice results.
Client Gateway Worker KSeF
│ │ │ │
│ POST /batch/... │ │ │
│ (3-100 invoices) │ │ │
├────────────────────────▶│ │ │
│ │ split into chunks │ │
│ 202 {batch_id, items} │ (100/chunk max) │ │
│◀────────────────────────│ │ │
│ │ Kafka: batch_chunk │ │
│ ├─────────────────────────▶│ │
│ │ │ for each invoice: │
│ │ │ encrypt + POST ──────▶│
│ │ │◀──── reference number │
│ │ │ poll → registered │
│ │ │ update DB │
│ GET /batch/{id} │ │ │
│ (poll for progress) │ │ │
├────────────────────────▶│ │ │
│ {submitted: 3/3} │ │ │
│◀────────────────────────│ │ │Two submission modes:
| Mode | Endpoint | Input | When to use |
|---|---|---|---|
| Raw XML | POST /api/batch/queue-raw-xml | Array of XML strings | ERP generates own FA(3) XML |
| By IDs | POST /api/batch/submit | Array of invoice UUIDs | Invoices already in DB (from draft/incoming) |
Submit Batch — Raw XML
POST /api/batch/queue-raw-xml
Authorization: Bearer {token}
Content-Type: application/jsonRequest:
{
"company_id": "22919080-f3fe-4aff-8d71-f6493afc956a",
"invoices": [
{ "xml_content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Faktura xmlns=\"http://crd.gov.pl/wzor/2025/06/25/13775/\">...invoice 1...</Faktura>" },
{ "xml_content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Faktura ...>...invoice 2...</Faktura>" },
{ "xml_content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Faktura ...>...invoice 3...</Faktura>" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
company_id | UUID | ✅ | Company ID (must belong to your tenant) |
invoices | array | ✅ | 1–100 items, each with xml_content |
invoices[].xml_content | string | ✅ | Full FA(3) XML, UTF-8 |
Response (202 Accepted):
{
"batch_id": "a0a85fd5-4f78-4a81-b01d-e1d34ebaa085",
"status": "queued",
"total_invoices": 3,
"total_chunks": 1,
"source": "raw_xml",
"items": [
{
"invoice_id": "0b6265a5-50d0-48aa-9369-4dfcff2e062f",
"invoice_number": null,
"chunk_number": 1,
"status": "pending",
"ksef_reference_number": null,
"ksef_number": null,
"error_message": null,
"submitted_at": null,
"completed_at": null
},
{ "invoice_id": "052b50e4-...", "chunk_number": 1, "status": "pending" },
{ "invoice_id": "b01b6f83-...", "chunk_number": 1, "status": "pending" }
],
"created_at": "2026-03-14T10:26:40.768277Z"
}Each invoice gets an
invoice_idimmediately — use these to poll individual results or via the batch endpoints.
For more than 100 invoices: Send multiple requests:
POST /batch/queue-raw-xml (invoices[0..99]) → batch_id_1
POST /batch/queue-raw-xml (invoices[100..199]) → batch_id_2
POST /batch/queue-raw-xml (invoices[200..249]) → batch_id_3Submit Batch — By IDs
Submit invoices that already exist in the database (e.g., created via incoming webhooks with auto_submit: false, or from draft).
POST /api/batch/submit
Authorization: Bearer {token}
Content-Type: application/jsonRequest:
{
"company_id": "22919080-f3fe-4aff-8d71-f6493afc956a",
"invoice_ids": [
"0b6265a5-50d0-48aa-9369-4dfcff2e062f",
"052b50e4-633a-4919-89eb-65a9054599cb"
],
"is_test_mode": false
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
company_id | UUID | ✅ | — | Company ID |
invoice_ids | array | ✅ | — | List of existing invoice UUIDs |
is_test_mode | boolean | ❌ | false | Use KSeF test environment |
Response (202): Same BatchSubmitResponse as raw XML.
Duplicate detection: If an invoice was already registered in KSeF, the batch item will fail with error:
"Duplikat faktury: Faktura o numerze KSeF: ... została już prawidłowo przesłana do systemu". This is a KSeF-side check — the Gateway marks the item asfailedwith the error message.
Monitor Batch Progress
4.1 Get Batch Status
GET /api/batch/{batch_id}
Authorization: Bearer {token}Response (200):
{
"batch_id": "a0a85fd5-4f78-4a81-b01d-e1d34ebaa085",
"company_id": "22919080-f3fe-4aff-8d71-f6493afc956a",
"total_invoices": 3,
"total_chunks": 1,
"status": "completed",
"source": "raw_xml",
"invoices_submitted": 3,
"invoices_failed": 0,
"created_at": "2026-03-14T10:26:40.768277Z",
"updated_at": "2026-03-14T10:26:46.437460Z",
"completed_at": "2026-03-14T10:26:46.444016Z",
"error_message": null,
"chunks": [
{
"chunk_number": 1,
"invoice_count": 3,
"status": "completed",
"submitted": 3,
"failed": 0
}
]
}4.2 Batch Statuses
| Status | Description | Final? |
|---|---|---|
queued | Waiting for Worker to start | ❌ |
processing | Worker is submitting invoices | ❌ |
completed | All invoices processed, 0 failed | ✅ |
partial | Some succeeded, some failed | ✅ |
failed | All invoices failed | ✅ |
4.3 Chunk Breakdown
Large batches are split into chunks. Each chunk has independent status and counters:
"chunks": [
{ "chunk_number": 1, "invoice_count": 100, "status": "completed", "submitted": 100, "failed": 0 },
{ "chunk_number": 2, "invoice_count": 100, "status": "completed", "submitted": 98, "failed": 2 },
{ "chunk_number": 3, "invoice_count": 50, "status": "processing", "submitted": 30, "failed": 0 }
]Per-Invoice Results
5.1 Get All Items
GET /api/batch/{batch_id}/items
Authorization: Bearer {token}Response (200):
{
"batch_id": "a0a85fd5-4f78-4a81-b01d-e1d34ebaa085",
"total": 3,
"items": [
{
"invoice_id": "0b6265a5-50d0-48aa-9369-4dfcff2e062f",
"invoice_number": "FV/BATCH/2026/03/001",
"chunk_number": 1,
"status": "registered",
"ksef_reference_number": "20260314-EE-23DBE64000-D05ADEBD45-C8",
"ksef_number": "8133793363-20260314-507828C00000-FF",
"error_message": null,
"submitted_at": "2026-03-14T10:26:40.914339Z",
"completed_at": "2026-03-14T10:26:46.209708Z"
},
{
"invoice_id": "052b50e4-633a-4919-89eb-65a9054599cb",
"invoice_number": "FV/BATCH/2026/03/002",
"chunk_number": 1,
"status": "registered",
"ksef_reference_number": "20260314-EE-23DBEE2000-D055FDE888-CB",
"ksef_number": "8133793363-20260314-5078A3000000-6B",
"error_message": null,
"submitted_at": "2026-03-14T10:26:41.038766Z",
"completed_at": "2026-03-14T10:26:46.306030Z"
},
{ "...": "..." }
]
}5.2 Filter by Status
GET /api/batch/{batch_id}/items?status=failed&limit=50&offset=0
Authorization: Bearer {token}| Parameter | Type | Description |
|---|---|---|
status | string | pending, registered, failed |
limit | int | Results per page |
offset | int | Pagination offset |
Example — failed items with error messages:
{
"batch_id": "8ce27191-27a9-401f-887f-3278e3f767e8",
"total": 2,
"items": [
{
"invoice_id": "0b6265a5-...",
"invoice_number": "FV/BATCH/2026/03/001",
"status": "failed",
"ksef_number": "8133793363-20260314-507828C00000-FF",
"error_message": "Duplikat faktury: Duplikat faktury. Faktura o numerze KSeF: 8133793363-20260314-507828C00000-FF została już prawidłowo przesłana do systemu w sesji: 20260314-SO-239E7B9000-..."
}
]
}Retry Failed Invoices
POST /api/batch/{batch_id}/retry-failed
Authorization: Bearer {token}Creates a new batch containing only the failed invoices from the original batch. The new batch goes through the same processing pipeline.
Response (202): New BatchSubmitResponse with new batch_id.
Error — no retryable items (400):
{ "detail": "No failed invoices to retry" }Error — duplicates cannot be retried (400):
{ "detail": "No retryable invoices — 2 already registered in KSeF (duplicates)" }Retry is intelligent: invoices that failed due to KSeF duplicates are excluded (they're already registered). Only genuinely failed invoices (e.g., session errors, validation errors) are retried.
List Batches
GET /api/batch/?company_id={id}&limit={n}&offset={n}
Authorization: Bearer {token}| Parameter | Type | Description |
|---|---|---|
company_id | UUID | Filter by company (optional) |
limit | int | Results per page |
offset | int | Pagination offset |
Response (200):
{
"total": 10,
"batches": [
{ "batch_id": "a0a85fd5-...", "status": "completed", "source": "raw_xml", "total_invoices": 3, "invoices_submitted": 3, "invoices_failed": 0 },
{ "batch_id": "fada76ca-...", "status": "completed", "source": "raw_xml", "total_invoices": 100, "invoices_submitted": 100, "invoices_failed": 0 },
{ "batch_id": "8ce27191-...", "status": "failed", "source": "ids", "total_invoices": 2, "invoices_submitted": 0, "invoices_failed": 2 },
{ "batch_id": "1cdca391-...", "status": "failed", "source": "raw_xml", "total_invoices": 5, "invoices_submitted": 0, "invoices_failed": 5 }
]
}Error Responses
| HTTP | Error | Cause |
|---|---|---|
| 202 | — | Batch accepted, processing started |
| 400 | No failed invoices to retry | Retry on batch with no failures |
| 400 | No retryable invoices — N already registered | All failures are duplicates |
| 403 | Access denied. Required role: admin or accountant | Insufficient role |
| 403 | Access denied to this company | Wrong tenant |
| 422 | List should have at least 1 item | Empty invoices array |
| 422 | Validation error | Missing company_id, invalid UUIDs |
Endpoint Reference
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/batch/queue-raw-xml | admin/accountant | Submit batch of raw XML invoices |
| POST | /api/batch/submit | admin/accountant | Submit batch by invoice IDs |
| GET | /api/batch/ | viewer+ | List all batches |
| GET | /api/batch/{batch_id} | viewer+ | Get batch status + chunks |
| GET | /api/batch/{batch_id}/items | viewer+ | Per-invoice results (filter: ?status=) |
| POST | /api/batch/{batch_id}/retry-failed | admin/accountant | Retry failed invoices |
Complete Example
#!/bin/bash
TOKEN="your_jwt_token"
BASE="https://demo.ksefapi.com"
COMPANY_ID="22919080-f3fe-4aff-8d71-f6493afc956a"
# 1. Submit batch of 3 XML invoices
BATCH=$(curl -s -X POST "$BASE/api/batch/queue-raw-xml" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"company_id\": \"$COMPANY_ID\",
\"invoices\": [
{\"xml_content\": \"<?xml version='1.0'?><Faktura xmlns='http://crd.gov.pl/wzor/2025/06/25/13775/'>...invoice 1...</Faktura>\"},
{\"xml_content\": \"<?xml version='1.0'?><Faktura ...>...invoice 2...</Faktura>\"},
{\"xml_content\": \"<?xml version='1.0'?><Faktura ...>...invoice 3...</Faktura>\"}
]
}")
BATCH_ID=$(echo "$BATCH" | jq -r '.batch_id')
echo "Batch created: $BATCH_ID"
# 2. Poll for completion (check every 5s)
while true; do
STATUS=$(curl -s "$BASE/api/batch/$BATCH_ID" \
-H "Authorization: Bearer $TOKEN" | jq -r '.status')
echo "Batch status: $STATUS"
[ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] || [ "$STATUS" = "partial" ] && break
sleep 5
done
# 3. Check per-invoice results
curl -s "$BASE/api/batch/$BATCH_ID/items" \
-H "Authorization: Bearer $TOKEN" | jq '.items[] | {invoice_number, status, ksef_number, error_message}'
# 4. If any failed, check errors
curl -s "$BASE/api/batch/$BATCH_ID/items?status=failed" \
-H "Authorization: Bearer $TOKEN" | jq '.items[] | {invoice_number, error_message}'
# 5. Retry failed (if applicable)
curl -s -X POST "$BASE/api/batch/$BATCH_ID/retry-failed" \
-H "Authorization: Bearer $TOKEN" | jq .Important Notes
Chunking: Batches are automatically split into chunks of max 100 invoices. Up to 3 chunks are processed concurrently. A batch of 250 invoices becomes 3 chunks (100 + 100 + 50).
Processing time: Based on test results:
- 3 invoices: ~6 seconds
- 5 invoices: ~6 seconds
- 50 invoices: ~80 seconds
- 100 invoices: ~210 seconds
XML validation: Each XML is validated before acceptance (namespace, well-formed, Podmiot1 NIP matches company). If any XML is invalid, the entire batch is rejected (fail-fast).
Duplicate handling: KSeF detects duplicates by invoice content hash. If you resubmit the same invoice, it fails with "Duplikat faktury" error. The retry-failed endpoint is smart enough to skip duplicates.
Source tracking: Each batch records its source (raw_xml or ids) for audit purposes.
Per-invoice polling: You can also poll individual invoices from a batch using their invoice_id via GET /api/invoices/{id} (Status, QR & UPO).
Outgoing webhooks: When batch invoices are registered, outgoing webhooks fire for each invoice individually (invoice.registered event). A batch.completed event fires when the entire batch finishes. See Process 12.
Next steps:
- Check invoice status, QR codes, UPO → Status, QR & UPO
- Set up webhook notifications → Process 12
Test Results (2026-03-14)
| Batch | Source | Invoices | Result | Time |
|---|---|---|---|---|
a0a85fd5 | raw_xml | 3 | ✅ 3/3 registered | ~6s |
8ce27191 | ids | 2 | ❌ 2/2 failed (duplicates) | ~6s |
fada76ca | raw_xml | 100 | ✅ 100/100 registered | (historical) |
618630f8 | raw_xml | 50 | ✅ 50/50 registered | (historical) |
Total: 10 batches in tenant, 180 invoices processed, including successful 100-invoice batch.