Send CSV invoices to SdI
Send a CSV from your ERP to SdI through A-Cube Transfer. Transfer converts each invoice to FatturaPA, and A-Cube's Italian invoicing API handles the submission to SdI. You will prepare one test invoice, upload the file, check processing through the API and follow its SdI outcome.
Before you start
- A-Cube has enabled sandbox REST upload and tracking access for Italy, and configured your seller data for the standard CSV.
- You have sandbox access to the Italian invoicing API, with the test data agreed with A-Cube.
- You have
curland Python 3. The commands below run in a shell on macOS or Linux.
Sandbox tests the integration without sending real invoices to SdI. Use it to check conversion, errors and the invoice's test status. Real SdI submission requires separately enabled production access.
The flow
| Step | Service | What you keep or check |
|---|---|---|
| Prepare the CSV | Your ERP | One invoice in the Italian standard layout |
| Sign in and upload | Common API and Transfer API | Sandbox token, upload id and your e2e_id |
| Wait for processing | Transfer API | Upload status and document violations |
| Find the created invoice | Transfer API | The document's transmission_uuid |
| Follow the SdI outcome | Italian invoicing API | Invoice marking and notifications |
202 Accepted confirms registration of the upload. completed confirms that the Italian invoicing service accepted the invoice. Neither confirms delivery by SdI.
Step 1: Prepare one invoice
Download the Italian standard CSV sample and save your prepared file as invoice.csv.
The sample contains two invoices, including a draft. For this tutorial:
- Keep the header and the two rows with
invoice_sequence=1. Remove the row withinvoice_sequence=2. - Replace the buyer identifiers and address with the sandbox values agreed with A-Cube.
- Set a new
invoice_numberand the test dates on both rows. Dates used/m/Y, for example09/10/2026. - Keep
invoice_draft=FALSEon both rows. ATRUEvalue creates a draft, which is not sent to SdI until you send it through the Italian invoicing API. - Keep the comma separator, header names and dot decimal separator. Edit the CSV in a text editor to avoid changing its format.
Rows with the same invoice_sequence belong to one invoice. line_number distinguishes its lines. Seller data comes from your onboarding configuration; buyer data comes from the CSV. See the Italian CSV layout for the rules and mapping reference.
Step 2: Get a sandbox token
Sign in with your A-Cube credentials and environment: sandbox:
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"}'
Copy the returned token and set the two sandbox service URLs:
TOKEN='<your-token>'
TRANSFER_URL='https://transfer-sandbox.api.acubeapi.com'
INVOICING_URL='https://api-sandbox.acubeapi.com'
The token lasts 24 hours. If a request returns 401, sign in again.
Step 3: Upload the CSV
Use an End-to-end ID from your ERP to find this upload later. Build the request with the whole file encoded in base64:
python3 -c 'import base64,json,pathlib; print(json.dumps({
"country": "IT",
"file_name": "invoice.csv",
"content": base64.b64encode(pathlib.Path("invoice.csv").read_bytes()).decode("ascii"),
"e2e_id": "ERP-IT-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, you receive:
{"id": "<upload-id>", "e2e_id": "ERP-IT-2026-00042", "status": "pending"}
The Location header points to /uploads/<upload-id>. Save the ID:
UPLOAD_ID=$(python3 -c 'import json; print(json.load(open("upload-receipt.json"))["id"])')
If the response is not 202, follow Upload errors before proceeding. For a timeout or 503, see Uncertain results and retries.
Step 4: Wait for processing and read the invoice UUID
Read the upload:
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 invoice UUID. |
partial | Read all documents: some invoices succeeded and some failed. |
failed | Read all documents and their violations before correcting the file. |
Start with a check every 10 seconds, then use longer intervals if processing continues. Stop frequent polling after a bounded period and retain the upload ID for later checks or support. These intervals are recommendations, not processing-time guarantees. Do not upload again just because processing is slow.
Retrieve all documents, including errors found while reading or converting the CSV:
curl --fail-with-body -sS "$TRANSFER_URL/uploads/$UPLOAD_ID/transfer-documents?itemsPerPage=100&page=1" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'
The response is an array. A successfully transmitted invoice includes:
{
"id": "<document-id>",
"status": "transmitted",
"transmission_uuid": "<invoice-uuid>",
"transmission_outcome": "ok",
"violations": []
}
Save the transmission_uuid of the transmitted invoice. It is the invoice UUID in the Italian invoicing API. The CSV file is also listed, so not every entry is an invoice.
For a failed entry, read violations, including the code, message and CSV row details when available. Conversion errors can be reported on the input CSV before any invoice is created. Use Violation codes to decide what to fix.
If you later send a batch, keep the UUID of every transmitted invoice and read subsequent pages until you receive fewer than itemsPerPage entries. For a partially successful batch, correct and upload only the failed invoices with a new file name.
Step 5: Follow the SdI outcome
Set the UUID from the transmitted document and read the invoice in the Italian invoicing API:
INVOICE_UUID='<transmission-uuid>'
curl --fail-with-body -sS "$INVOICING_URL/invoices/$INVOICE_UUID" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'
Read the invoice's marking. The main values in the SdI flow are:
marking | Meaning and next action |
|---|---|
waiting, sent | Sending or awaiting SdI's answer. Keep checking this invoice. |
quarantena | SdI could not be reached. A-Cube retries; keep checking the invoice. |
delivered, delivered-pa | SdI delivered the invoice to the recipient. |
not-delivered | SdI could not deliver the invoice to the recipient. Read the notification. |
rejected | SdI rejected the invoice. Read the rejection details before correcting and resending it. |
invoice-error | A-Cube could not complete the submission after its retries. Contact A-Cube with the UUID. |
Sandbox does not contact SdI. Check the test status expected for your sandbox setup; a sandbox result does not prove real delivery. For the complete status list and production behavior, see SdI outcome.
For an in-progress invoice, check periodically with longer intervals than the Transfer processing checks. Keep the UUID and reduce polling frequency if it remains in progress. An unexpected status needs investigation, not an automatic resend.
SdI's later answer does not change the Transfer document's transmitted status. A successful Transfer upload can still lead to a rejected invoice in the Italian invoicing API.
Read the SdI notification types: NS is a rejection, RC a delivery receipt, and MC a delivery failure. Store the ERP reference, upload ID, invoice UUID and outcome together.
Uncertain results and retries
e2e_id is a correlation value, not an idempotency key. Every accepted upload request creates a new upload.
If the upload request times out, search by your ERP reference before retrying:
curl --fail-with-body -sS --get "$TRANSFER_URL/uploads" \
--data-urlencode 'e2e_id=ERP-IT-2026-00042' \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json'
The search is exact and case-sensitive, and it can return several uploads. Inspect their IDs and times. An empty result immediately after a timeout does not prove the request will never finish: wait and check again.
For 503, use the receipt ID in the error response. A failed upload can be retried; if it stays pending, contact A-Cube before retrying. Once it is ingested or later, Transfer has the file. Continue tracking it instead of uploading it again.
For a partially successful CSV, resend only the corrected failed invoices. For an invoice rejected later by SdI, follow the SdI outcome guidance; this is separate from a Transfer processing failure.
Go to production
After the sandbox checks, confirm that A-Cube enabled production access and seller configuration. Sign in with environment: production, use https://transfer.api.acubeapi.com for Transfer and https://api.acubeapi.com for Italian invoicing, and prepare production invoice data.
Follow the same upload and processing checks, then continue until you can read the real SdI outcome. Keep checking every invoice in a batch, even when its upload is already completed.
Related guides
- UBL JSON to KSeF: send a Polish invoice and retrieve its KSeF outcome and UPO.
- Transfer webhooks: configure processing callbacks where supported. Confirm correlation and error coverage with A-Cube.
- Italy's webhooks: receive SdI events from the Italian invoicing service.