Skip to main content

Send UBL JSON invoices to KSeF

Send a UBL JSON invoice from your ERP to KSeF through A-Cube Transfer. Transfer converts the file to KSeF FA(3), and A-Cube's Polish invoicing API submits it to KSeF. You will upload the file, check processing through the API, read the KSeF outcome and download the UPO.

Before you start​

  • A-Cube has enabled sandbox REST upload and tracking access for Poland, and configured your UBL JSON conversion.
  • Your seller is registered and enabled in the Polish invoicing service, with the sandbox setup agreed with A-Cube.
  • You have one test invoice saved as invoice.json, in the UBL JSON layout configured for Poland. Use the test identifiers and invoice number agreed with A-Cube.
  • You have curl and Python 3. The commands below run in a shell on macOS or Linux.

UBL JSON is a specific input layout, not any JSON representation of an invoice. A-Cube confirms your sample during onboarding.

The flow​

StepServiceWhat you keep or check
Sign inCommon APIA sandbox bearer token
Upload the fileTransfer APIUpload id and your e2e_id
Wait for processingTransfer APIUpload status and document violations
Find the created invoiceTransfer APIEach document's transmission_uuid
Wait for KSeF's answerPolish invoicing APIInvoice status, KSeF reference number and UPO

202 Accepted confirms that Transfer registered the upload. completed confirms that the invoicing service accepted the document. Neither confirms that KSeF accepted the invoice.

Step 1: Get a sandbox token​

Sign in with your A-Cube credentials and environment: sandbox. Copy the returned token:

curl --fail-with-body -sS https://common.api.acubeapi.com/login \
-H 'Content-Type: application/json' \
-d '{"email": "<your-email>", "password": "<your-password>", "environment": "sandbox"}'
{"token": "<your-token>"}

Set the token and the two sandbox service URLs for the next commands:

TOKEN='<your-token>'
TRANSFER_URL='https://transfer-sandbox.api.acubeapi.com'
INVOICING_URL='https://pl-sandbox.api.acubeapi.com'

The token lasts 24 hours. If a request returns 401, sign in again. Production uses separate access: sign in with environment: production, use https://transfer.api.acubeapi.com for Transfer and https://pl.api.acubeapi.com for Polish invoicing.

Step 2: Upload your invoice​

Use an End-to-end ID from your ERP to find this upload later. Build the JSON request with the whole file encoded in base64:

python3 -c 'import base64,json,pathlib; print(json.dumps({
"country": "PL",
"file_name": "invoice.json",
"content": base64.b64encode(pathlib.Path("invoice.json").read_bytes()).decode("ascii"),
"e2e_id": "ERP-2026-00042"
}))' > upload-request.json

curl --fail-with-body -sS -D upload-headers.txt -o upload-receipt.json \
-X POST "$TRANSFER_URL/upload" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
--data-binary @upload-request.json

Check the HTTP status in upload-headers.txt and the body in upload-receipt.json. On 202, the response includes:

Location: /uploads/<upload-id>
{"id": "<upload-id>", "e2e_id": "ERP-2026-00042", "status": "pending"}

Save the upload ID:

UPLOAD_ID=$(python3 -c 'import json; print(json.load(open("upload-receipt.json"))["id"])')

Every accepted request creates a new upload. e2e_id is a correlation value, not an idempotency key. If you get an error instead of 202, follow Upload errors before proceeding. For a timeout or 503, see Uncertain results and retries.

Step 3: Wait for Transfer to finish​

Read the upload by its receipt ID:

curl --fail-with-body -sS "$TRANSFER_URL/uploads/$UPLOAD_ID" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'
Upload statusNext action
pending, ingested, or reserved processingWait and read again.
completedRead the documents to obtain the created invoice's UUID.
partialRead all documents: some succeeded and some failed.
failedRead all documents and their violations before correcting the file.

As a starting point for your application, poll Transfer every 10 seconds, then use longer intervals if processing continues. This is a polling policy, not a processing-time guarantee. Stop frequent polling after a bounded period, keep the upload ID, and resume checks later or contact A-Cube if it remains in progress. Do not upload the file again just because processing is slow.

Step 4: Read the documents and handle errors​

Retrieve all documents for this upload, including errors found before an invoice was created:

curl --fail-with-body -sS "$TRANSFER_URL/uploads/$UPLOAD_ID/transfer-documents?itemsPerPage=100&page=1" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'

For a transmitted invoice, the useful fields are:

{
"id": "<document-id>",
"status": "transmitted",
"transmission_uuid": "<invoice-uuid>",
"transmission_outcome": "ok",
"violations": []
}

The response is an array; this example shows one entry. Save each transmitted invoice's transmission_uuid. One file can produce several invoices, each with its own UUID. Read subsequent pages until you receive fewer than itemsPerPage entries. See Pagination.

If an entry is failed, read its violations: each includes a code and message, with field and row details when available. Reading only role=target can miss conversion errors reported on the input file. Use Violation codes to decide what to fix.

For a partially successful file, continue tracking every transmitted invoice. Correct and upload only the failed invoices, with a new file name. Resending the whole batch can send successful invoices twice.

Step 5: Check the KSeF outcome​

Set the invoice UUID from the transmitted document, then query the Polish invoicing API:

INVOICE_UUID='<transmission-uuid>'

curl --fail-with-body -sS "$INVOICING_URL/invoices/$INVOICE_UUID/status" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'

This endpoint returns status, message and upo (the UPO encoded in base64, when available). For example:

{"status": "valid", "message": null, "upo": "<base64-encoded-UPO>"}
Invoice statusNext action
new, in_queue, processingKeep checking the invoice, not the Transfer upload.
validKSeF accepted the invoice. Read its reference number and download its UPO.
invalidKSeF rejected the invoice. The polling response does not include the rejection reason: contact A-Cube with the invoice UUID.
draft, blocked, rejectedThe invoice needs attention in A-Cube's invoicing service. Check the invoice status definitions before continuing.

For KSeF, start with one check every two minutes while the invoice is in progress. If it has not changed after an hour, reduce the frequency and contact A-Cube with the invoice UUID. These intervals are recommendations, not an SLA.

Read the invoice record for ksefReferenceNumber, acquisition, and hasUpo:

curl --fail-with-body -sS "$INVOICING_URL/invoices/$INVOICE_UUID" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'

KSeF's later answer does not change the Transfer document's transmitted status. A successful Transfer upload can still lead to an invalid invoice in KSeF.

Step 6: Download the UPO​

After KSeF accepts the invoice and the UPO is available, download its official XML receipt:

curl --fail-with-body -sS "$INVOICING_URL/invoices/$INVOICE_UUID/upo" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/xml' \
-o invoice-upo.xml

Store the ERP reference, upload ID, invoice UUID, KSeF reference number and UPO together. The test is complete when you can trace your file through these IDs and read its final KSeF result.

Uncertain results and retries​

If the upload request times out, search for your ERP reference before retrying:

curl --fail-with-body -sS --get "$TRANSFER_URL/uploads" \
--data-urlencode 'e2e_id=ERP-2026-00042' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'

The search is an exact, case-sensitive match and can return several uploads. Inspect their IDs and times. An empty result immediately after a timeout does not prove that the request will never finish: wait and check again before deciding to retry.

For 503, use the receipt ID in the error response. A failed upload can be retried; if it remains pending, contact A-Cube before retrying. Once it is ingested or later, Transfer has the file. Continue tracking it instead of uploading it again.

  • CSV to SdI: send an Italian standard CSV and follow each invoice's SdI outcome.
  • SFTP uploads: upload using SFTP, find the upload by file name and time with the tracking API, then follow steps 3 onwards. SFTP uploads have no client End-to-end ID.
  • Notifications: see Transfer webhooks for processing callbacks and their availability. For KSeF events, use Poland's webhook system. Configure notifications separately if your integration needs them.