Vai al contenuto

API fattura elettronica in Java

Invia fatture elettroniche allo SdI e ricevi fatture e notifiche dei fornitori dal tuo codice Java, con esempi completi da copiare. Ambiente: Java 11+ e Spring Boot 3.

Prima di iniziare

  1. Crea un account e conferma l'e-mail: hai 30 giorni e 20 fatture di prova.
  2. Usa le stesse credenziali del portale per il login alle API (POST https://api.fattura-elettronica.cloud/api/login) e conserva il token.
  3. Per ricevere fatture e notifiche sul tuo server, imposta gli indirizzi dei tuoi endpoint nel profilo del portale e attiva l'inoltro.

Inviare una fattura elettronica in Java

L'esempio usa solo java.net.http.HttpClient (Java 11+), costruendo a mano il multipart. In un progetto Spring puoi usare WebClient o RestClient con MultipartBodyBuilder. La risposta contiene inoltro.id_sdi, l'identificativo con cui arriveranno le notifiche della fattura.

// Java 11+, solo librerie standard
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class InvioFattura {

    static final String API = "https://api.fattura-elettronica.cloud/api";
    static final HttpClient HTTP = HttpClient.newHttpClient();

    public static void main(String[] args) throws Exception {
        // 1. Login: il token vale a lungo, salvalo e rifai il login solo se ricevi 401
        HttpRequest login = HttpRequest.newBuilder(URI.create(API + "/login"))
                .header("Accept", "application/json")
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                        "{\"email\":\"tua@email.it\",\"password\":\"LaTuaPassword\"}"))
                .build();
        String token = campo(HTTP.send(login, HttpResponse.BodyHandlers.ofString()).body(), "access_token");
        if (token == null) throw new IllegalStateException("Login non riuscito");

        // 2. Invio della fattura XML allo SdI (multipart/form-data, campo fileFattura)
        Path file = Path.of("IT01234567890_00001.xml");
        String confine = "----fe" + UUID.randomUUID();
        byte[] inizio = ("--" + confine + "\r\n"
                + "Content-Disposition: form-data; name=\"fileFattura\"; filename=\"" + file.getFileName() + "\"\r\n"
                + "Content-Type: application/xml\r\n\r\n").getBytes(StandardCharsets.UTF_8);
        byte[] fine = ("\r\n--" + confine + "--\r\n").getBytes(StandardCharsets.UTF_8);

        HttpRequest invio = HttpRequest.newBuilder(URI.create(API + "/inoltro"))
                .header("Accept", "application/json")
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "multipart/form-data; boundary=" + confine)
                .POST(HttpRequest.BodyPublishers.ofByteArrays(List.of(inizio, Files.readAllBytes(file), fine)))
                .build();
        HttpResponse<String> risposta = HTTP.send(invio, HttpResponse.BodyHandlers.ofString());

        if (risposta.statusCode() == 200) {
            System.out.println("Inviata, identificativo SdI: " + campo(risposta.body(), "id_sdi"));
        } else {
            System.out.println("Errore " + risposta.statusCode() + ": " + campo(risposta.body(), "messaggio"));
        }
    }

    // Per brevità legge un campo del JSON con una regex: nel tuo progetto usa Jackson o Gson
    static String campo(String json, String nome) {
        Matcher m = Pattern.compile("\"" + nome + "\"\\s*:\\s*(?:\"([^\"]*)\"|([^,}\\s]+))").matcher(json);
        return m.find() ? (m.group(1) != null ? m.group(1) : m.group(2)) : null;
    }
}

Codici di risposta: 200 inviata, 400 file non valido, 401 token scaduto, 403 pacchetto esaurito o e-mail non confermata, 422 rifiutata dallo SdI. Tutti i dettagli nella documentazione completa.

Ricevere le fatture elettroniche in Java

Quando arriva una fattura per il tuo codice destinatario la mandiamo al tuo endpoint con una POST form-urlencoded: strxml (XML della fattura), xmlF (file originale), xmlM (metadati SdI), tutti in base64, e formato. Con Spring Boot leggi il corpo dall'InputStream della richiesta prima che qualsiasi filtro chieda i parametri del form, altrimenti il corpo risulta già consumato e la firma non si può verificare.

// Spring Boot 3 (Java 17+) - URL da indicare in "Endpoint ricezione fatture"
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@RestController
public class FattureSdiController {

    // pagina Integrazione del portale; vuoto se non usi la firma
    static final String SEGRETO = System.getenv().getOrDefault("FE_SEGRETO", "");

    @PostMapping("/ricevi-fatture")
    public ResponseEntity<String> fattura(HttpServletRequest request) throws Exception {
        byte[] corpo = request.getInputStream().readAllBytes(); // il corpo così com'è arrivato, serve per la firma
        if (!firmaValida(request, corpo)) return ResponseEntity.status(403).body("firma non valida");

        Map<String, String> dati = leggiForm(new String(corpo, StandardCharsets.UTF_8));
        byte[] xmlFattura = Base64.getDecoder().decode(dati.getOrDefault("strxml", "")); // XML, già estratto dal .p7m
        String metadati = new String(Base64.getDecoder().decode(dati.getOrDefault("xmlM", "")), StandardCharsets.UTF_8);
        Matcher id = Pattern.compile("<IdentificativoSdI>(\\d+)<").matcher(metadati);
        if (!id.find() || xmlFattura.length == 0) return ResponseEntity.badRequest().body("dati mancanti");

        // Stesso IdentificativoSdI = stessa fattura: se arriva di nuovo sovrascrivi, non duplicare
        Path cartella = Files.createDirectories(Path.of("fatture_ricevute"));
        Files.write(cartella.resolve(id.group(1) + ".xml"), xmlFattura);

        return ResponseEntity.ok("OK"); // esattamente OK: con qualsiasi altra risposta viene rimandata
    }

    // X-FE-Firma = HMAC-SHA256 di "timestamp\ncorpo" con il tuo segreto
    static boolean firmaValida(HttpServletRequest req, byte[] corpo) throws Exception {
        if (SEGRETO.isEmpty()) return true;
        String ts = Objects.toString(req.getHeader("X-FE-Timestamp"), "");
        String firma = Objects.toString(req.getHeader("X-FE-Firma"), "");
        if (!ts.matches("\\d+") || Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(ts)) > 300) return false;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(SEGRETO.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        mac.update((ts + "\n").getBytes(StandardCharsets.UTF_8));
        String attesa = HexFormat.of().formatHex(mac.doFinal(corpo));
        return MessageDigest.isEqual(attesa.getBytes(StandardCharsets.UTF_8), firma.getBytes(StandardCharsets.UTF_8));
    }

    static Map<String, String> leggiForm(String corpo) {
        Map<String, String> dati = new HashMap<>();
        for (String coppia : corpo.split("&")) {
            String[] kv = coppia.split("=", 2);
            if (kv.length == 2) {
                dati.put(URLDecoder.decode(kv[0], StandardCharsets.UTF_8), URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
            }
        }
        return dati;
    }
}

Rispondi OK con HTTP 200 solo dopo aver salvato la fattura. Se il tuo server non risponde riproviamo per circa 4 giorni: la stessa fattura può arrivare più volte, riconoscila da IdentificativoSdI.

Ricevere le notifiche SdI in Java

Ogni notifica dello SdI (consegna, scarto, mancata consegna, esiti della PA) arriva al tuo endpoint notifiche con i campi tipoNotifica e esitojson. Il significato di ogni notifica è spiegato nella guida alle notifiche SdI.

// Spring Boot 3 - nello stesso controller dell'endpoint fatture (firmaValida e leggiForm sono quelle)
// URL da indicare in "Endpoint ricezione notifiche"
static final Map<String, String> TIPI = Map.of("1", "RC", "2", "MC", "3", "NS", "4", "NE", "5", "DT", "6", "AT");
static final Map<String, String> STATI = Map.of(
        "RC", "consegnata",
        "MC", "mancata consegna",      // disponibile nel cassetto fiscale del cliente
        "NS", "scartata",              // i motivi sono in ListaErrori
        "NE", "esito committente",     // solo PA: EsitoCommittente.Esito = EC01 accettata, EC02 rifiutata
        "DT", "decorrenza termini",    // solo PA: nessuna risposta entro 15 giorni
        "AT", "attestazione trasmissione");

private final ObjectMapper json = new ObjectMapper(); // Jackson, già incluso in Spring Boot

@PostMapping("/ricevi-notifiche")
public ResponseEntity<String> notifica(HttpServletRequest request) throws Exception {
    byte[] corpo = request.getInputStream().readAllBytes();
    if (!firmaValida(request, corpo)) return ResponseEntity.status(403).body("firma non valida");

    Map<String, String> dati = leggiForm(new String(corpo, StandardCharsets.UTF_8));
    String tipo = TIPI.get(dati.getOrDefault("tipoNotifica", ""));
    JsonNode notifica = json.readTree(dati.getOrDefault("esitojson", "null")); // la notifica SdI in JSON
    if (tipo == null || notifica.isNull()) return ResponseEntity.badRequest().body("dati mancanti");

    // IdentificativoSdI è lo stesso restituito dall'API all'invio (inoltro.id_sdi).
    // La stessa notifica può arrivare due volte: l'aggiornamento deve poter essere ripetuto.
    fatture.aggiornaStato(notifica.path("IdentificativoSdI").asText(), STATI.get(tipo), notifica.toString());

    return ResponseEntity.ok("OK"); // esattamente OK: con qualsiasi altra risposta viene rimandata
}

Prossimi passi

Inizia con Java

Prova gratis 30 giorni con 20 fatture, poi da 5 € al mese.