My App

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:

ModeEndpointInputWhen to use
Raw XMLPOST /api/batch/queue-raw-xmlArray of XML stringsERP generates own FA(3) XML
By IDsPOST /api/batch/submitArray of invoice UUIDsInvoices already in DB (from draft/incoming)

Submit Batch — Raw XML

POST /api/batch/queue-raw-xml
Authorization: Bearer {token}
Content-Type: application/json

Request:

{
  "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>" }
  ]
}
FieldTypeRequiredDescription
company_idUUID✅Company ID (must belong to your tenant)
invoicesarray✅1–100 items, each with xml_content
invoices[].xml_contentstring✅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_id immediately — 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_3

Submit 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/json

Request:

{
  "company_id": "22919080-f3fe-4aff-8d71-f6493afc956a",
  "invoice_ids": [
    "0b6265a5-50d0-48aa-9369-4dfcff2e062f",
    "052b50e4-633a-4919-89eb-65a9054599cb"
  ],
  "is_test_mode": false
}
FieldTypeRequiredDefaultDescription
company_idUUID✅—Company ID
invoice_idsarray✅—List of existing invoice UUIDs
is_test_modeboolean❌falseUse 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 as failed with 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

StatusDescriptionFinal?
queuedWaiting for Worker to start❌
processingWorker is submitting invoices❌
completedAll invoices processed, 0 failed✅
partialSome succeeded, some failed✅
failedAll 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}
ParameterTypeDescription
statusstringpending, registered, failed
limitintResults per page
offsetintPagination 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}
ParameterTypeDescription
company_idUUIDFilter by company (optional)
limitintResults per page
offsetintPagination 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

HTTPErrorCause
202—Batch accepted, processing started
400No failed invoices to retryRetry on batch with no failures
400No retryable invoices — N already registeredAll failures are duplicates
403Access denied. Required role: admin or accountantInsufficient role
403Access denied to this companyWrong tenant
422List should have at least 1 itemEmpty invoices array
422Validation errorMissing company_id, invalid UUIDs

Endpoint Reference

MethodEndpointRoleDescription
POST/api/batch/queue-raw-xmladmin/accountantSubmit batch of raw XML invoices
POST/api/batch/submitadmin/accountantSubmit 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}/itemsviewer+Per-invoice results (filter: ?status=)
POST/api/batch/{batch_id}/retry-failedadmin/accountantRetry 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:


Test Results (2026-03-14)

BatchSourceInvoicesResultTime
a0a85fd5raw_xml3✅ 3/3 registered~6s
8ce27191ids2❌ 2/2 failed (duplicates)~6s
fada76caraw_xml100✅ 100/100 registered(historical)
618630f8raw_xml50✅ 50/50 registered(historical)

Total: 10 batches in tenant, 180 invoices processed, including successful 100-invoice batch.


On this page