DEV Community

getregdata for Apify

Posted on

Italian e-invoice routing has three branches. Most integrations implement two.

Every invoice between Italian businesses passes through SDI (Sistema di Interscambio), the state clearing system. Over 2 billion invoices a year. An invoice SDI rejects is legally never issued. And the field that routes it, the codice destinatario, is filled from data the Agenzia delle Entrate's own guide describes as "comunicato dal cliente": communicated by the customer.

So the one field your invoice delivery depends on comes from an email somebody sent your AP team a year ago, or a form field somebody filled in once. No Italian company is obliged to publish it anywhere.

I build registry-data tools for a living, and this year one of them, a bulk lookup that resolves Italian companies' certified email (PEC) and SDI code from the public registry layer, became one of the most heavily used things we run. The usage pattern was always the same: somebody feeding in their whole supplier master, in one batch, before an e-invoicing rollout or a cleanup.

This post is the implementation half of that story: the three routing branches (most code I have seen implements two), the two different nulls that look identical and route differently, the sole-trader field that is worse than missing if you fill it wrong, and working code with its real output and real cost.

The rules, from the source

Two legal facts set up everything else, and the details are load-bearing, so read them in the original if you work on this.

First, the routing rules. The Agenzia delle Entrate's guide to preparing an electronic invoice gives exactly three ways to fill the routing fields:

  • insert the 7-character code "comunicato dal cliente", or
  • insert 0000000 and fill the PEC Destinatario field with the PEC address "comunicato dal cliente", or
  • insert 0000000 alone, when the customer is a consumer or a VAT-registered business that has communicated neither a code nor a PEC. In this case the invoice is delivered to the recipient's reserved area on the Agenzia's portal, and the supplier must give the customer a paper or digital copy ("rilasciare al suo cliente una copia informatica o analogica della fattura elettronica").

The third case is the branch that gets forgotten, and it is not exotic. It is the default for any business counterparty that never told you anything. If your routing logic has two branches, it has a bug. The "neither" case exists, it has its own delivery semantics, and it puts a follow-up duty on you: send the customer a copy yourself.

Second, the PEC duty. Article 37 of D.L. 76/2020 obliges every Italian company and sole trader to register a certified email address (PEC, a mailbox where delivery has the legal effect of registered mail) with the business register. Miss the duty and the Chamber of Commerce assigns you one and fines you. Unlike the SDI code, the PEC is public: free to look up, one company at a time.

The two routing inputs have opposite natures. The SDI code is a private handshake nobody must publish. The PEC is a statutory register entry everybody must have. Treat them accordingly in your integration: collect and verify the code, re-derive the PEC whenever you want.

The lookup, and why the input is one mixed list

The tool takes one array called identifiers, and it accepts three different things in the same list: a Partita IVA (VAT number), a Codice Fiscale, or a company name.

The shape came from watching real supplier masters. The only field you can usually trust in an ERP vendor record is the Partita IVA, because the buyer's own tax treatment depends on it being right. Real masters are dirty, though: some rows have only a Codice Fiscale, some only a name typed by a human. Forcing the caller to pre-classify their own mess just moves the mess one file to the left. So: one list, the resolver figures out what each entry is, one result row per identifier either way.

Here is the whole batch job. I ran this exact script while writing this post:

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

// P.IVA, Codice Fiscale or company names - one list, mixed on purpose
const identifiers = ['00159560366', '01654010345', '00905811006', 'Illycaffe'];

const run = await client.actor('regdata/italy-pec-lookup').call({ identifiers });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

function route(rec) {
  if (rec.codiceDestinatarioSDI) {
    return { channel: 'SDI_CODE', value: rec.codiceDestinatarioSDI };
  }
  if (rec.detailLevel === 'partial') {
    // Reduced registry page: the SDI code was UNAVAILABLE, not absent.
    // Do not conclude anything - re-check or ask the customer.
    return { channel: 'UNKNOWN_RECHECK', value: null };
  }
  if (rec.pec) {
    // Full detail and still no code: the firm publishes none.
    // 0000000 + PEC is the documented routing, not a workaround.
    return { channel: 'ZERO_PLUS_PEC', value: rec.pec };
  }
  // Full detail, no code, no PEC: the third branch. 0000000 alone,
  // the invoice lands in the recipient's reserved area, and you owe
  // the customer a copy.
  return { channel: 'ZERO_RESERVED_AREA_SEND_COPY', value: null };
}

for (const rec of items) {
  console.log(rec.query, rec.denominazione, rec.detailLevel,
    rec.codiceDestinatarioSDI, '->', route(rec).channel);
}
Enter fullscreen mode Exit fullscreen mode

The real output of that run, four lookups in about 30 seconds:

00159560366  FERRARI-SOCIETA' PER AZIONI ESERCIZIO FABBRICHE
             AUTOMOBILI E CORSE O SEMPLICEMENTE: FERRARI S.P.A.
             detail=full  sdi=null     -> ZERO_PLUS_PEC (ferrari@pec.ferrari.com)
01654010345  BARILLA G. E R. FRATELLI - SOCIETA' PER AZIONI
             detail=full  sdi=null     -> ZERO_PLUS_PEC (barilla@legalmail.it)
00905811006  ENI S.P.A.
             detail=full  sdi=7L12QWU  -> SDI_CODE (7L12QWU)
Illycaffe    ILLYCAFFE S.P.A.
             detail=full  sdi=JX8OYTO  -> SDI_CODE (JX8OYTO)
Enter fullscreen mode Exit fullscreen mode

Ferrari and Barilla publish no SDI code on this registry layer. Two of the most famous companies in the country, and the correct routing for both is 0000000 plus their PEC. A pipeline that treats a missing code as an error state declares half of Italian industry broken, while the Agenzia's rules say the PEC route is ordinary first-class routing.

Also worth noticing: Illycaffe went in as a bare name, no VAT number, and resolved to the legal entity with its code. The dirty-supplier-master case, working.

The two nulls (this is the part that bites)

codiceDestinatarioSDI: null means one of two completely different things, and the record tells you which through a field called detailLevel.

When detailLevel is full and the code is null, the registry page was complete and the firm simply publishes no code. A confirmed absence. Route with 0000000 plus PEC, confidently.

When detailLevel is partial and the code is null, the registry served a reduced page for that company, so the code was unavailable rather than absent. An unknown. The only honest handling is to re-check later or ask the counterparty.

We made detailLevel a first-class output field because the two cases are indistinguishable in the value itself. A batch job that collapses them will do one of two bad things: route real code-holders through the anonymous 0000000 channel, so their invoice lands somewhere their ERP is not watching, or flag half the file "unknown" and drown the AP team in manual checks that a full record had already answered.

The transferable design rule: when a source can fail to show you a field, "we saw the whole record and it is not there" and "we did not see the whole record" must be different values in your output. A single null is a lie about one of them.

The sole-trader trap: null beats a plausible wrong value

Italian sole traders (imprese individuali) have a subtlety that punishes helpful code. For a company, the Codice Fiscale on the registry layer is a tax code that often equals the Partita IVA. For a sole trader, the real Codice Fiscale is the owner's 16-character personal code, and the public layer we resolve from does not publish it.

The tempting implementation fills the CF field with the Partita IVA anyway, because for most rows that is what it would have been. For sole traders you have now produced wrong data with the right shape, which is strictly worse than a gap, because everything downstream (deduplication, tax-code validation, cross-register joins) will consume it without complaint. The correct output is null, documented as such.

Missing data pauses a pipeline. Plausible wrong data flows through it.

The name-reconciliation step nobody budgets for

Ferrari's legal name in the output above reads:

FERRARI-SOCIETA' PER AZIONI ESERCIZIO FABBRICHE AUTOMOBILI E CORSE O SEMPLICEMENTE: FERRARI S.P.A.

One company. It is "Ferrari SpA" in your ERP, "FERRARI S.P.A." in the registry header, and possibly a trading name on the invoice PDF. When you resolve a supplier master in bulk, the last step is reconciling what the registry returned against what your ERP says, and it is a fuzzy-language problem rather than a string problem. A regular expression that survives "O SEMPLICEMENTE:" has not met the next province's notary yet. In our own pipeline a small language model does this step: compare the pair, return same | different | unsure, and send only the disagreements to a human.

What a full pass costs

The pricing is public pay-per-event: about $0.01 per resolved contact record plus a small run start fee. The four-lookup demonstration above cost about five cents. A one-time pass over a 500-supplier master lands around six dollars and finishes over coffee, against an AP team resolving the same file by hand, one free registry search at a time.

The re-run is the part people skip. Registered offices, PEC addresses and published codes change, and your file does not. A quarterly re-pass at these prices is a rounding error next to one invoice that spent three weeks in the wrong channel.

Wrap-up

  • Italian invoice routing has three branches, per the Agenzia's own guide. Implement all three; the "customer told us nothing" branch has its own delivery semantics and its own copy-duty on you.
  • The SDI code is "comunicato dal cliente", private by design. The PEC is a statutory register entry, re-derivable by design. Build the two fields differently.
  • Distinguish confirmed-absent from unavailable. Two kinds of null, two different actions.
  • For sole traders, return null rather than a plausible wrong tax code.
  • Reconcile names with something that understands language, and only escalate the disagreements.

The batch lookup used above is italy-pec-lookup, and the fuller registry record (REA, ATECO, share capital, employees) comes from italy-registro-imprese-scraper. The routing rules are on the Agenzia delle Entrate's guide, and the PEC obligation on registroimprese.it.

If you have implemented SDI routing and hit a case these three branches do not cover, I would genuinely like to hear about it in the comments.

Top comments (0)