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
curland 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
| Step | Service | What you keep or check |
|---|---|---|
| Sign in | Common API | A sandbox bearer token |
| Upload the file | Transfer API | Upload id and your e2e_id |
| Wait for processing | Transfer API | Upload status and document violations |
| Find the created invoice | Transfer API | Each document's transmission_uuid |
| Wait for KSeF's answer | Polish invoicing API | Invoice 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 status | Next action |
|---|---|
pending, ingested, or reserved processing | Wait and read again. |
completed | Read the documents to obtain the created invoice's UUID. |
partial | Read all documents: some succeeded and some failed. |
failed | Read 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 status | Next action |
|---|---|
new, in_queue, processing | Keep checking the invoice, not the Transfer upload. |
valid | KSeF accepted the invoice. Read its reference number and download its UPO. |
invalid | KSeF rejected the invoice. The polling response does not include the rejection reason: contact A-Cube with the invoice UUID. |
draft, blocked, rejected | The 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.
Related guides
- 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.