Skip to main content

Italy — SDI (FatturaPA) and Documento Commerciale

Research doc for the Italy fiscalization adapter. Internal reference for engineers maintaining or extending rust/adapters/italy/.

Status: Implemented (legacy Phase-0 wedge). Adapter at rust/adapters/italy/ (crate italy). Strategic context: Italy is the largest of our four legacy launch countries by addressable market and the most architecturally awkward — the merchant population splits across two regulators-in-one: Sistema di Interscambio (SDI) for B2B/B2G electronic invoicing, and the Documento Commerciale (DC) channel via Servizio Software (SSW) for B2C retail. Both run on Agenzia delle Entrate (AdE) infrastructure but use different schemas, different endpoints, different auth surfaces, and different submission windows. The adapter supports both via a single Adapter struct with a system discriminator. See docs/architecture/italy-documento-commerciale.md for the deep DC design and apps/docs-internal/docs/guides/italy.md for the integration walkthrough.


1. Regulatory scope

Two coexisting regimes

RegimeOperatorScopeTrigger
SDI / FatturaPASistema di Interscambio (run by Sogei for AdE)B2B and B2G electronic invoicingInvoice issuance under Italian VAT, fatturazione elettronica obbligatoria since 2019-01-01
Documento Commerciale (DC)AdE Servizio Software (SSW)B2C retail — replaces the paper scontrino / ricevuta fiscaleCash-equivalent payment to a final consumer at point-of-sale

The two are mutually exclusive per transaction: an Italian retail sale is either a Fattura (goes through SDI) or a Documento Commerciale (goes through SSW). The merchant chooses based on whether the customer requests an invoice or a commercial-document receipt.

SDI — who must submit

Per Decreto Legislativo 127/2015 and amendments through 2024:

EffectiveScope
2014-06-06B2G (invoicing to Public Administration) — initial
2019-01-01All B2B + B2C invoices issued by Italian VAT-resident taxpayers (fatturazione elettronica obbligatoria)
2022-07-01Cross-border (intra-EU and extra-EU) supplies via Esterometro / TD17–TD19 codes
2024-01-01Forfettario (small taxpayer) regime threshold dropped — all Italian taxpayers in scope, no minimum revenue

Exemptions: only specific carve-outs (sanitary services subject to patient privacy, certain agriculture under flat-rate regimes, certain diplomatic supplies).

DC — who must submit

Per Legislative Decree 1/2024 and Provvedimento AdE of 2025-03-07:

  • Any taxpayer issuing a commercial document in lieu of the previous scontrino (which has been retired since 2020-01-01 in favor of corrispettivi telematici).
  • The hardware-RT (Registratore Telematico) path predates SSW; SSW is the new software-only alternative.
  • Zyntem's path is SSW (introduced by D.Lgs. 1/2024). Hardware-RT is out of scope for our adapter — those are physical fiscal printers certified by AdE; we don't compete in that space.

Thresholds

  • No transaction-amount threshold for either regime. Every invoice / commercial document is in scope regardless of amount.
  • Daily transmission cap for DC: file size ≤ 1 MB per submission; ~tens of thousands of receipts in practice fit comfortably below this.

2. API surface

SDI — REST + token

Endpoints (rust/adapters/italy/src/sdi_client.rs:6-7):

EnvironmentBase URL
Productionhttps://api.fatturapa.gov.it/servizi/fatturapa/v1
Sandboxhttps://api.fatturapa.gov.it/servizi/fatturapa/v1-test

Operations (sdi_client.rs:11-25):

MethodPathPurpose
POST/fattureSubmit a FatturaPA XML; returns identificativoSdI
GET/fatture/{sdi_id}/statoPoll processing status
GET/fatture/{sdi_id}/esitoRetrieve the outcome notification XML

Wire format: XML body, Content-Type: application/xml, Bearer-token auth in HTTP header. There's also a legacy SOAP / SDICoop / SDIFTP profile for high-volume issuers, but our adapter uses the REST path exclusively.

DC — SSW REST

Endpoints (rust/adapters/italy/src/dc_client.rs:6-7):

EnvironmentBase URL
Productionhttps://api.corrispettivi.agenziaentrate.gov.it/v1
Sandboxhttps://api.corrispettivi.agenziaentrate.gov.it/v1-test

Operations (dc_client.rs:18-32):

MethodPathPurpose
POST/corrispettiviSubmit a sealed corrispettivi XML; returns idPresaInCarico
GET/corrispettivi/{id}/statoPoll status (PRONTA / IN_ELABORAZIONE / NON_DISPONIBILE)
GET/corrispettivi/{id}/esitoRetrieve outcome XML

Wire format: binary body (application/octet-stream) — the sealed XML payload is byte-exact, no Content-Type: application/xml, no JSON envelope. The seal (PAdES/CAdES electronic seal) is what AdE verifies, not the XML structure alone.

Schema namespaces

  • FatturaPA v1.2.2 (fatturapa.rs:5): http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2.
  • DC v1.0 (dc.rs:5): http://ivaservizi.agenziaentrate.gov.it/docs/xsd/corrispettivi/v1.0.
  • XMLDSig (both): http://www.w3.org/2000/09/xmldsig#.
  • SDI notifications v1.0 (fatturapa.rs:10): http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.0 — used for RC / NS / MC notification XML.

Format-version selection

SDI requires <FormatoTrasmissione> to identify the recipient class (config.rs:9-10):

CodeRecipient
FPA12Pubblica Amministrazione (B2G)
FPR12Privati (B2B/B2C — the common case)

DC uses versione="1.0" on the root <DocumentoCommerciale> element (dc.rs:13-25). The DSW10 format identifier referenced in the architecture doc is the SSW software-solution formal-version code; our emitter uses 1.0 — consistent with the AdE samples and golden fixtures we conform to.


3. Authentication

SDI — channel credentials + bearer token

Per auth.rs:8-13:

pub struct Credentials {
pub channel_id: String,
pub channel_password: String,
pub api_token: String,
}

The channel_id + channel_password pair authorizes us as a transmission channel in SDI's registry. The api_token is the short-lived bearer used on individual REST calls. Channel registration is a one-time, out-of-band onboarding with AdE (multi-week paperwork); the bearer is refreshable.

We store credentials on disk per credential_id and load them lazily through the AuthStore trait (auth.rs:34-101). FileAuthStore caches loaded creds in memory under a RwLock<HashMap<String, Credentials>> and validates that the supplied credential_id is a plain filename (no path traversal — auth.rs:73-79):

let clean = Path::new(credential_id)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
if clean != credential_id {
return Err(AuthError::InvalidId(credential_id.to_string()));
}

SDI — XML signing (CAdES-BES)

The FatturaPA production path requires a qualified electronic signature wrapping the XML — typically CAdES-BES (.p7m) but XAdES is also accepted. The signing trait is at signature.rs:22-27:

#[async_trait::async_trait]
pub trait Signer: Send + Sync {
async fn sign(&self, xml_data: &[u8]) -> Result<Vec<u8>, SignatureError>;
}

We ship a NoOpSigner (signature.rs:30-38) that returns the input unchanged — usable for sandbox where SDI accepts unsigned XML, and as the trait stub for tests. Production deployments must swap in either a QTSP-backed signer (DocuSign / Aruba PEC / Namirial) or a local PKCS#12-backed signer.

DC — QTSP electronic seal

Per adapter.rs:306-322:

fn required_certificate_id(&self, config: &serde_json::Value) -> Option<String> {
let system = config.get("system").and_then(|v| v.as_str()).unwrap_or(SYSTEM_SDI);
if system == SYSTEM_DC {
config.get("seal_certificate_id").and_then(|v| v.as_str())...
} else {
None
}
}

DC submissions must be sealed with a sigillo elettronico qualificato issued by an Italian QTSP (Aruba, Namirial, Infocert, etc.) — this is seal_certificate_id in Config. The sealed bytes go on the wire as application/octet-stream. AdE verifies the seal before accepting the file; an unsealed DC submission is rejected with error 0004 ("digital seal verification failed", errors.rs:14).

This is the single most important deployment constraint for DC — it forces EmbeddedLocal mode. See §6.

Why credentials and seals live in different places

  • SDI: api_credential_id in Config (config.rs:33) → loads channel + token from AuthStore.
  • DC: seal_certificate_id in Config (config.rs:58) → loads a PKCS#12 / HSM handle from the cert store.

There is no shared identity in SDI/DC; they are operated by different AdE teams with different onboarding paths.


4. Payload shape

SDI — FatturaElettronica

Top-level structure built in rust/adapters/italy/src/fatturapa.rs:14-44:

FatturaElettronica (versione=FPR12 | FPA12)
├── FatturaElettronicaHeader
│ ├── DatiTrasmissione
│ │ ├── IdTrasmittente (IdPaese + IdCodice)
│ │ ├── ProgressivoInvio # 10-char id we mint per submission
│ │ ├── FormatoTrasmissione # FPA12 | FPR12
│ │ └── CodiceDestinatario # 7-char SDI mailbox or "0000000" / "XXXXXXX"
│ ├── CedentePrestatore (supplier)
│ │ ├── DatiAnagrafici (IdFiscaleIVA, CodiceFiscale, Anagrafica, RegimeFiscale)
│ │ └── Sede (address)
│ └── CessionarioCommittente (customer)
│ ├── DatiAnagrafici
│ └── Sede
└── FatturaElettronicaBody
├── DatiGenerali
│ └── DatiGeneraliDocumento
│ ├── TipoDocumento # TD01 / TD04 / TD17–TD27 / etc.
│ ├── Divisa # ISO 4217
│ ├── Data # YYYY-MM-DD
│ ├── Numero
│ └── ImportoTotaleDocumento
├── DatiBeniServizi
│ ├── DettaglioLinee[] # one per line item
│ └── DatiRiepilogo[] # one per VAT rate (taxable / tax / nature / exigibility)
└── DatiPagamento (optional)
└── DettaglioPagamento (ModalitaPagamento, ImportoPagamento, IBAN, ...)

The CodiceDestinatario defaults follow this rule (fatturapa.rs:79-87):

  • Provided by the customer's recipient code: use as-is (7 chars).
  • Foreign customer (country != "IT"): XXXXXXX.
  • Italian customer with no destination code: 0000000.

DC — DocumentoCommerciale

Top-level structure built in rust/adapters/italy/src/dc.rs:11-33:

DocumentoCommerciale (versione="1.0")
├── DatiConfigurazioneDC
│ ├── FormatoVersione # "1.0"
│ └── Matricola # PEM matricola, NNNN-NNNNNN
├── CedentePrestatore
│ ├── IdFiscaleIVA (IdPaese + IdCodice)
│ ├── CodiceFiscale
│ ├── Denominazione
│ └── Sede (Indirizzo, CAP, Comune, Provincia)
└── Documento
├── DataOraDocumento # ISO 8601 dateTime
├── NumeroProgressivo # NNNN-NNNN, monotonic per matricola
├── ElementiContabili[] # line items (description, qty, unitPrice, totalPrice, vatRate, type=vendita|reso)
├── Riepilogo[] # VAT summary per rate
├── Vendita | ResoAnnullo # totals + cash/electronic/discount/ticket breakdown
├── DatiLotteria (optional) # lottery code or instant-lottery QR
└── CodiceFiscaleCliente (optional)

Document type:

  • Vendita (sale): standard receipt with Vendita totals block.
  • ResoAnnullo (return / void): includes TipologiaRA = R (return) | A (annullo) and mandatory reference to the original document (matricola, date, progressive number).

Field formatting (both regimes)

The adapter normalizes minor units into the wire decimals it expects (adapter.rs:499-519):

fn format_cents(cents: i64) -> String {
format!("{}.{:02}", cents / 100, (cents % 100).unsigned_abs())
}
fn format_decimal(value: i64, decimals: usize) -> String { /* qty in thousandths */ }
fn format_rate(basis_points: i64) -> String {
format!("{}.{:02}", basis_points / 100, (basis_points % 100).unsigned_abs())
}

So LineItem.unit_price = 10000 (cents) → "100.00", quantity = 2000 (thousandths) → "2.000", tax_rate = 2200 (basis points) → "22.00".

Document-type mapping

Adapter mapping (adapter.rs:521-526):

fn map_sdi_doc_type(tx_type: &str) -> String {
match tx_type {
"refund" | "return" | "credit_note" => "TD04".to_string(),
_ => "TD01".to_string(),
}
}

So consumer-side tx_type=sale → SDI TD01, refund/return/credit_noteTD04. The wider TD17–TD27 set (cross-border, reverse charge, asset transfers, deferred invoices) is documented in apps/docs-internal/docs/guides/italy.md:60-85 but currently requires hand-rolled tx_type extension — not something an arbitrary upstream caller can produce automatically. For autofatture / cross-border we extend the mapping per merchant onboarding.


5. Ordering and sequencing

SDI: IdentificativoSdI (per submission)

SDI returns one IdentificativoSdI per accepted submission, regardless of whether the call carried one invoice or N invoices in a single lotto (lot). Our adapter sends one invoice per call — process_sdi at adapter.rs:96-143 builds and submits a single FatturapaRequest, so the returned identificativoSdI is the per-invoice tracking ID (adapter.rs:127-133):

Ok(AdapterResult {
status: STATUS_PENDING.to_string(),
fiscal_id: resp.identificativo_sdi,
receipt_data: xml_bytes,
content_type: "application/xml".to_string(),
})

The ProgressivoInvio field on the wire is the issuer's tracking number (10 chars, we mint from tx.idadapter.rs:376); the SDI ID is what AdE returns.

Idempotency: SDI deduplicates on (IdTrasmittente.IdCodice, ProgressivoInvio). A retry with the same ProgressivoInvio returns the original IdentificativoSdI rather than minting a new one — but only within a 5-day window and only if the prior call reached AdE. Resubmitting after a timeout is therefore safe; resubmitting weeks later under the same ProgressivoInvio yields error 00200 ("duplicate invoice", errors.rs:80).

DC: idPresaInCarico + NumeroProgressivo

Two ordering keys exist:

  1. idPresaInCarico — UUID returned per submission (dc_client.rs:43-46), opaque tracker.
  2. NumeroProgressivo — the printed progressive number, format NNNN-NNNN, monotonic per PEM matricola (config.rs:54).

The matricola is the Punto di Emissione (emission point) serial. Each cash register gets one matricola at first-use registration with AdE; all that register's documents share it. Within a matricola, the progressive resets at midnight and increments by 1 per receipt.

Adapter rule: the NumeroProgressivo is taken from tx.id verbatim (adapter.rs:481). Upstream is responsible for the monotonic property; the adapter does not mint or validate the sequence. If upstream sends a duplicate, AdE returns error 0010 ("progressive number already used for this PEM", errors.rs:18) and the document is rejected.

Retries and idempotency

Both regimes:

  • Network failure / 5xx: safe to retry — the same payload bytes produce the same dedup key.
  • 4xx with a permanent code (schema, signature, missing matricola): fix and resubmit with a new progressive number; the original number is "burned".
  • Timeout after submission: retry — if the prior call reached the authority, we get back the same tracking ID.

6. Submission window and deployment mode

Submission windows

From adapter.rs:226-231:

fn submission_window(&self) -> SubmissionWindow {
match self.system.as_str() {
SYSTEM_DC => SubmissionWindow::Immediate,
_ => SubmissionWindow::Days(12),
}
}
SystemWindowSource
SDI12 days from issuanceDPR 633/1972 Art. 21 — invoice must be issued and submitted within 12 days of the operazione
DCImmediate at point of saleD.Lgs. 1/2024 — receipt must be transmitted at the moment of issuance (with daily azzeramento / closure transmitted within the same calendar day)

The DC daily-closure (chiusura giornaliera) is a separate per-day file that aggregates the day's receipts and is transmitted at random within 00:00–22:00 local time, deadline 12 days. That file is generated downstream of the per-receipt flow and isn't yet in the adapter — it's tracked separately.

Deployment mode

From adapter.rs:233-238:

fn supported_deployment_modes(&self) -> &[DeploymentMode] {
match self.system.as_str() {
SYSTEM_DC => &[DeploymentMode::EmbeddedLocal],
_ => &[DeploymentMode::Cloud, DeploymentMode::EmbeddedLocal],
}
}
  • SDI is Cloud-OK. Channel credentials and token can live in our KMS; the QES signing can be brokered to a remote QTSP.
  • DC is Embedded-only. The QTSP electronic seal that AdE verifies on every submission must be applied at point-of-sale, per AdE provvedimento — a Cloud-side seal application breaks the "device emits sealed file" mental model and has been rejected in past audits. There may be a path forward for HSM-backed Cloud DC later, but as of 2026-04 we do not deploy DC in Cloud mode.

7. Sandbox access

Two distinct onboarding flows:

SDI sandbox

  • Endpoint: https://api.fatturapa.gov.it/servizi/fatturapa/v1-test.
  • Onboarding: register a canale di trasmissione via AdE's Servizi Telematici. Requires an Italian VAT number and digital identity (SPID / CIE / CNS).
  • Sandbox accepts unsigned XML (the NoOpSigner works); production enforces CAdES-BES.
  • See apps/docs-internal/docs/countries/italy-sandbox.md for the full step-by-step.

DC sandbox

  • Endpoint: https://api.corrispettivi.agenziaentrate.gov.it/v1-test.
  • Onboarding: separate AdE Fatture e Corrispettivi portal flow. Register a software identity (CAU code, 4 alnum chars — config.rs:50) and a test PEM matricola (NNNN-NNNNNNconfig.rs:54, 151-158).
  • Sandbox does verify a test seal certificate. Aruba and Namirial issue free test seal certs against AdE's test trust roots; capture the PKCS#12 + password and reference via seal_certificate_id.

Note: the architecture doc at docs/architecture/italy-documento-commerciale.md covers the full DC onboarding sequence including PEM activation.


8. Error model

Two-stage error semantics

Both regimes return an immediate (synchronous) submission response with a tracking ID, then drive the actual outcome asynchronously through poll-or-notification. The poll module classifies states (poll.rs:25-110):

pub const SDI_POLL_POLICY: PollPolicy = PollPolicy::new(
5 * 60, // initial_delay: 5 min
60 * 60, // interval: 1 hour
14 * 24 * 60 * 60, // dead_letter: 14 days
);
pub const DC_POLL_POLICY: PollPolicy = PollPolicy::new(
10, // initial_delay: 10s
60, // interval: 1 min
60 * 60, // dead_letter: 1 hour
);

Polling cadence reflects the asymmetric finalization speeds: SDI can take days to deliver to a recipient PEC; DC finalizes in seconds-to-minutes.

SDI states

Defined at sdi_client.rs:51-54:

StateMapping (poll.rs:44-79)Meaning
IN_ELABORAZIONEStillPendingAdE is processing the submission
CONSEGNATAAcceptedDelivered to recipient PEC — happy path
NON_CONSEGNATAAccepted (not Rejected)AdE accepted but couldn't reach the recipient. The issuer's tax position is fine. Recipient retrieves via "PUT-AT-DISPOSAL" channel.
SCARTATARejectedSchema / signature / business validation failed; invoice is not legally issued
anything elseStillPending (with warning log)future-proofing for AdE adding new states

The NON_CONSEGNATAAccepted mapping is a deliberate design choice. Many naive implementations report it as failure, which is wrong — the issuer's invoice has been registered, the only thing missing is recipient delivery, which AdE makes available through a secondary channel.

DC states

Defined at dc_client.rs:13-15:

StateMappingMeaning
IN_ELABORAZIONEStillPendingAdE is processing
PRONTAAcceptedOutcome ready and OK
NON_DISPONIBILEStillPending (warning)File not in the "ready-to-serve" cache; transient or hard-fail — the dead-letter window decides

SDI notifications (RC / NS / MC)

Once CONSEGNATA / NON_CONSEGNATA / SCARTATA is reached, AdE emits an outcome notification XML on the issuer's PEC (apps/docs-internal/docs/guides/italy.md:127-131):

CodeStatus
RC (RicevutaConsegna)delivered
NS (NotificaScarto)rejected — XML enumerates <errori>
MC (NotificaMancataConsegna)delivery_error — recipient unreachable, eventually goes to put-at-disposal

The adapter can build these notification XMLs as well via FatturapaBuilder::build_notification_xml (fatturapa.rs:37-44) — used in tests / mocks.

Error code catalogues

errors.rs ships static maps for both SSW (DC, codes 0001–0901) and SDI (codes 00001–00404). The classifier translates the Italian description to English so logs and dashboards stay readable.

Selected SSW codes (errors.rs:8-59):

CodeClassTranslation
0001formatfile format not recognized
0002sizefile exceeds maximum allowed size (1 MB)
0004sealdigital seal verification failed
0007matricolaPEM matricola not registered or not active
0009duplicateduplicate submission detected
0010duplicateprogressive number already used for this PEM
0107timingDataOraDocumento is in the future
0200arithmeticRiepilogo totals do not match ElementiContabili
0400timingtransmission outside allowed window (00:00-22:00)
0401timingdocument date exceeds 12-day transmission deadline
0502raterate limit exceeded, retry after cooldown
0900serverinternal processing error, please retry

Selected SDI codes (errors.rs:73-86):

CodeTranslation
00001file not conforming to FatturaPA schema
00002transmitter IdCodice not found in registry
00100CodiceDestinatario not valid or not active
00200duplicate invoice (same number/date/sender)
00301buyer IdFiscaleIVA not found in registry
00400digital signature verification failed
00403signing certificate expired
00404signing certificate revoked

9. Implementation notes

Crate layout

rust/adapters/italy/
├── Cargo.toml # crate "italy", optional `http` feature gates reqwest
└── src/
├── lib.rs # re-exports Adapter, DcBuilder, FatturapaBuilder
├── adapter.rs # Adapter struct + CountryAdapter impl + system routing
├── config.rs # Config (system="sdi"|"dc"), validation per system
├── auth.rs # Credentials, AuthStore, FileAuthStore (channel creds for SDI)
├── signature.rs # Signer trait, NoOpSigner; production signers live elsewhere
├── client.rs # AdEHttpClient (shared reqwest wrapper, error taxonomy)
├── sdi_client.rs # SdiClient trait + HttpSdiClient (FatturaPA REST)
├── dc_client.rs # DcClient trait + HttpDcClient (SSW REST)
├── fatturapa.rs # FatturapaBuilder — FatturaPA XML + notification XML emitters
├── dc.rs # DcBuilder — DocumentoCommerciale XML emitter
├── poll.rs # PollableAdapter impl, classify_sdi_stato / classify_dc_stato
├── errors.rs # SSW + SDI error code translations
└── types.rs # FatturapaRequest, DcRequest, all wire-shape DTOs

Trait fit

CountryAdapter::prepare() is a pure XML-build:

  • parse_sealed_config() extracts the validated Config from loc.country_config (adapter.rs:332-337).
  • Routes on config.effective_system() to either DcBuilder::build_dc_xml or FatturapaBuilder::build_invoice_xml.
  • Returns PreparedRecord with submission_payload = xml.into_bytes(), content_type = "application/xml". The signature/seal is not applied at prepare() — that happens at submit time (see below).

CountryAdapter::submit() is a no-op (adapter.rs:289-297):

fn submit(&self, _record: &PreparedRecord) -> Result<SubmitResult, CoreAdapterError> {
// Actual async SDI/DC submission is handled by the retry worker.
Ok(SubmitResult { authority_id: None, authority_response: None })
}

The actual HTTP submission is a separate worker that consumes the PreparedRecord.submission_payload, signs/seals, posts to the appropriate client, and reconciles the tracking ID back into the record. This split exists because:

  1. Submission is async and error-prone — we want to retry with backoff without re-running prepare().
  2. Signing requires KMS / HSM access; we don't want the prepare hot path to depend on that.
  3. The poll loop (PollableAdapter impl at poll.rs:120-152) is the third leg of the same pipeline.

Idiosyncrasies and gotchas

  1. Adapter struct holds both clients as Option<Box<dyn ...>> (adapter.rs:55-62). When the matching client is None, the adapter returns STATUS_PENDING_SUBMISSION and the worker is expected to fill the gap. This makes prepare() testable without network.
  2. fiscal_id is provisional at prepare time: format!("IT-{}", tx.id) (adapter.rs:276). The real SDI ID / DC idPresaInCarico only arrives post-submit. Downstream consumers should treat fiscal_id as a stable internal handle and look at fiscal_id_upstream (which the worker fills) for the authority's ID.
  3. progressive_number for the SDI transmitter is tx.id truncated to 10 chars (adapter.rs:376). If tx.id is non-alnum or shorter, that may produce a ProgressivoInvio AdE rejects. We've not seen a regression from this in practice but it's a sharp edge.
  4. DC matricola has a strict format: NNNN-NNNNNN (4 + 6 alphanumeric, config.rs:151-158). A bad matricola at config registration is rejected; a matricola that's valid format but not registered with AdE is rejected at submission with 0007 ("PEM matricola not registered or not active").
  5. SDI deduplicates on IdTrasmittente + ProgressivoInvio, not on invoice number. Two invoices with the same Numero and different ProgressivoInvio are both accepted by SDI. The "duplicate invoice" error (00200) only fires on (supplier, year, invoice_number) collision — a separate check.
  6. Sandbox accepts unsigned FatturaPA, production does not. Don't wire the NoOpSigner to a Cloud production deployment.
  7. NON_CONSEGNATA is success. If you read the error logs, this looks like a failure — it is not.

10. References

Authority documentation

Adapter source

  • rust/adapters/italy/Cargo.toml — crate manifest, http feature.
  • rust/adapters/italy/src/adapter.rsAdapter, system routing, CountryAdapter impl.
  • rust/adapters/italy/src/config.rsConfig schema, validators, SYSTEM_SDI / SYSTEM_DC constants.
  • rust/adapters/italy/src/auth.rs — channel credentials store.
  • rust/adapters/italy/src/signature.rsSigner trait, NoOpSigner.
  • rust/adapters/italy/src/sdi_client.rs — REST surface for SDI.
  • rust/adapters/italy/src/dc_client.rs — REST surface for SSW DC.
  • rust/adapters/italy/src/fatturapa.rs — FatturaPA XML emitter + notification XML emitter.
  • rust/adapters/italy/src/dc.rs — DC XML emitter.
  • rust/adapters/italy/src/poll.rsPollableAdapter, classify_sdi_stato / classify_dc_stato.
  • rust/adapters/italy/src/errors.rs — SSW + SDI error code translations.

Internal docs

  • docs/architecture/italy-documento-commerciale.md — deep DC design doc (XSD, lottery, daily closure).
  • apps/docs-internal/docs/architecture/italy-documento-commerciale.md — published rendering of the same.
  • apps/docs-internal/docs/guides/italy.md — integration guide (legacy; should migrate to external).
  • apps/docs-internal/docs/countries/italy-sandbox.md — onboarding steps for both sandboxes.
  • apps/docs/docs/guides/italy.md — current customer-facing presentation.