Come funziona
Il servizio è collegato direttamente al Sistema di Interscambio dell'Agenzia delle Entrate con il canale SDICoop. Il tuo gestionale parla con noi, noi parliamo con lo SdI:
- Invio: il gestionale manda il file XML della fattura con
POST https://api.fattura-elettronica.cloud/api/inoltroe riceve subito l'identificativo SdI. - Notifiche: quando lo SdI risponde (consegna, scarto, mancata consegna...), inoltriamo la notifica al tuo endpoint notifiche.
- Ricezione: le fatture dei fornitori indirizzate al tuo codice destinatario arrivano al tuo endpoint fatture, oltre che nel portale.
Gli endpoint sono semplici URL del tuo server che rispondono OK: li imposti nel profilo del portale. Ogni richiesta può essere firmata con un segreto che conosci solo tu.
Indirizzo base delle API: https://api.fattura-elettronica.cloud/api
Manda sempre l'header Accept: application/json, così anche gli errori arrivano in JSON.
Autenticazione
Le API usano le stesse credenziali del portale (l'e-mail deve essere confermata). Fai il login una volta, conserva il token e usalo in ogni richiesta con l'header Authorization: Bearer <token>. Il token vale a lungo: rifai il login solo se una chiamata risponde 401.
| Metodo | Indirizzo | Parametri | Risposta |
|---|---|---|---|
POST | /api/login | email, password | access_token e dati dell'utente; senza access_token le credenziali non sono valide |
Inviare una fattura
POST /api/inoltro in multipart/form-data, con il file XML nel campo fileFattura. Il nome del file deve rispettare le regole dello SdI, ad esempio IT01234567890_00001.xml (vedi formato XML e nome file).
<?php
// PHP 7.4+ con l'estensione curl
const API = 'https://api.fattura-elettronica.cloud/api';
function chiamaApi(string $percorso, array $campi = [], ?string $token = null): array
{
$ch = curl_init(API . $percorso);
$header = ['Accept: application/json'];
if ($token) {
$header[] = 'Authorization: Bearer ' . $token;
}
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $header,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
if ($campi) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $campi); // array = multipart/form-data (POST)
}
$risposta = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$http, json_decode((string) $risposta, true)];
}
// 1. Login: il token vale a lungo, salvalo e rifai il login solo se ricevi 401
[$http, $login] = chiamaApi('/login', ['email' => 'tua@email.it', 'password' => 'LaTuaPassword']);
$token = $login['access_token'] ?? null;
if (!$token) {
exit('Login non riuscito: ' . ($login['message'] ?? "HTTP $http"));
}
// 2. Invio della fattura XML allo SdI
$file = new CURLFile(__DIR__ . '/IT01234567890_00001.xml', 'application/xml');
[$http, $esito] = chiamaApi('/inoltro', ['fileFattura' => $file], $token);
if ($http === 200 && ($esito['esito'] ?? '') === 'ok') {
echo 'Inviata, identificativo SdI: ' . $esito['inoltro']['id_sdi'] . PHP_EOL;
} else {
echo "Errore $http: " . ($esito['messaggio'] ?? 'risposta non valida') . PHP_EOL;
}
// 3. Elenco delle fatture inviate (esito: 0 in attesa, 1 consegnata, 2 mancata consegna, 3 scartata, ...)
[$http, $elenco] = chiamaApi('/inoltro', [], $token);
foreach ($elenco['inoltri'] ?? [] as $inoltro) {
echo $inoltro['id_sdi'] . ' ' . $inoltro['esito'] . PHP_EOL;
}
# Python 3.8+ (pip install requests)
import requests
API = "https://api.fattura-elettronica.cloud/api"
HEADERS = {"Accept": "application/json"}
# 1. Login: il token vale a lungo, salvalo e rifai il login solo se ricevi 401
r = requests.post(f"{API}/login", headers=HEADERS, timeout=30,
data={"email": "tua@email.it", "password": "LaTuaPassword"})
token = r.json().get("access_token")
if not token:
raise SystemExit(f"Login non riuscito: {r.json().get('message')}")
auth = {**HEADERS, "Authorization": f"Bearer {token}"}
# 2. Invio della fattura XML allo SdI
nome = "IT01234567890_00001.xml"
with open(nome, "rb") as f:
r = requests.post(f"{API}/inoltro", headers=auth, timeout=90,
files={"fileFattura": (nome, f, "application/xml")})
dati = r.json()
if r.status_code == 200 and dati.get("esito") == "ok":
print("Inviata, identificativo SdI:", dati["inoltro"]["id_sdi"])
else:
print(f"Errore {r.status_code}: {dati.get('messaggio')}")
# 3. Elenco delle fatture inviate (esito: 0 in attesa, 1 consegnata, 2 mancata consegna, 3 scartata, ...)
for inoltro in requests.get(f"{API}/inoltro", headers=auth, timeout=30).json()["inoltri"]:
print(inoltro["id_sdi"], inoltro["esito"])
<?php
// Laravel 8+ - .env: FE_API_TOKEN=... (ottienilo una volta con FatturaElettronica::token())
// config/services.php: 'fattura_elettronica' => ['token' => env('FE_API_TOKEN')],
namespace App\Services;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class FatturaElettronica
{
private const API = 'https://api.fattura-elettronica.cloud/api';
/** Login: restituisce il token da salvare in FE_API_TOKEN */
public static function token(string $email, string $password): string
{
$risposta = Http::acceptJson()->post(self::API . '/login', compact('email', 'password'));
if (!$risposta->json('access_token')) {
throw new RuntimeException($risposta->json('message', 'Login non riuscito'));
}
return $risposta->json('access_token');
}
/** Invia l'XML allo SdI e restituisce l'identificativo SdI */
public static function invia(string $percorsoXml): string
{
$risposta = Http::acceptJson()
->withToken(config('services.fattura_elettronica.token'))
->timeout(90)
->attach('fileFattura', file_get_contents($percorsoXml), basename($percorsoXml))
->post(self::API . '/inoltro');
if ($risposta->failed() || $risposta->json('esito') !== 'ok') {
throw new RuntimeException("Invio non riuscito ({$risposta->status()}): " . $risposta->json('messaggio'));
}
return (string) $risposta->json('inoltro.id_sdi');
}
/** Fatture inviate con il loro esito (0 in attesa, 1 consegnata, 2 mancata consegna, 3 scartata, ...) */
public static function inviate(): array
{
return Http::acceptJson()
->withToken(config('services.fattura_elettronica.token'))
->get(self::API . '/inoltro')
->json('inoltri', []);
}
}
// Uso:
// $idSdi = FatturaElettronica::invia(storage_path('app/fatture/IT01234567890_00001.xml'));
// Node.js 18+ (fetch, FormData e Blob sono già inclusi) - file invio.mjs
import { readFile } from "node:fs/promises";
const API = "https://api.fattura-elettronica.cloud/api";
// 1. Login: il token vale a lungo, salvalo e rifai il login solo se ricevi 401
const login = await fetch(`${API}/login`, {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({ email: "tua@email.it", password: "LaTuaPassword" }),
}).then((r) => r.json());
if (!login.access_token) throw new Error(`Login non riuscito: ${login.message}`);
const auth = { Accept: "application/json", Authorization: `Bearer ${login.access_token}` };
// 2. Invio della fattura XML allo SdI
const nome = "IT01234567890_00001.xml";
const form = new FormData();
form.append("fileFattura", new Blob([await readFile(nome)], { type: "application/xml" }), nome);
const risposta = await fetch(`${API}/inoltro`, { method: "POST", headers: auth, body: form });
const esito = await risposta.json();
if (risposta.ok && esito.esito === "ok") {
console.log("Inviata, identificativo SdI:", esito.inoltro.id_sdi);
} else {
console.error(`Errore ${risposta.status}: ${esito.messaggio}`);
}
// 3. Elenco delle fatture inviate (esito: 0 in attesa, 1 consegnata, 2 mancata consegna, 3 scartata, ...)
const { inoltri } = await fetch(`${API}/inoltro`, { headers: auth }).then((r) => r.json());
for (const inoltro of inoltri) console.log(inoltro.id_sdi, inoltro.esito);
// 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;
}
}
// .NET 6+ (HttpClient e System.Text.Json già inclusi) - Program.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
const string Api = "https://api.fattura-elettronica.cloud/api";
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// 1. Login: il token vale a lungo, salvalo e rifai il login solo se ricevi 401
var login = await http.PostAsJsonAsync($"{Api}/login", new { email = "tua@email.it", password = "LaTuaPassword" });
var datiLogin = await login.Content.ReadFromJsonAsync<JsonElement>();
if (!datiLogin.TryGetProperty("access_token", out var token))
throw new Exception("Login non riuscito");
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.GetString());
// 2. Invio della fattura XML allo SdI
var percorso = "IT01234567890_00001.xml";
using var form = new MultipartFormDataContent();
var file = new ByteArrayContent(await File.ReadAllBytesAsync(percorso));
file.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
form.Add(file, "fileFattura", Path.GetFileName(percorso));
var risposta = await http.PostAsync($"{Api}/inoltro", form);
var esito = await risposta.Content.ReadFromJsonAsync<JsonElement>();
if (risposta.IsSuccessStatusCode && esito.GetProperty("esito").GetString() == "ok")
Console.WriteLine($"Inviata, identificativo SdI: {esito.GetProperty("inoltro").GetProperty("id_sdi")}");
else
Console.WriteLine($"Errore {(int)risposta.StatusCode}: {esito.GetProperty("messaggio").GetString()}");
// 3. Elenco delle fatture inviate (esito: 0 in attesa, 1 consegnata, 2 mancata consegna, 3 scartata, ...)
var elenco = await http.GetFromJsonAsync<JsonElement>($"{Api}/inoltro");
foreach (var inoltro in elenco.GetProperty("inoltri").EnumerateArray())
Console.WriteLine($"{inoltro.GetProperty("id_sdi")} {inoltro.GetProperty("esito")}");
# 1. Login: ricevi il token (vale a lungo: salvalo e rifai il login solo se ricevi 401)
curl -s https://api.fattura-elettronica.cloud/api/login \
-H "Accept: application/json" \
-d "email=tua@email.it" -d "password=LaTuaPassword"
# 2. Invia la fattura XML allo SdI
curl -s https://api.fattura-elettronica.cloud/api/inoltro \
-H "Accept: application/json" \
-H "Authorization: Bearer IL_TUO_TOKEN" \
-F "fileFattura=@IT01234567890_00001.xml"
# 3. Elenco delle fatture inviate con il loro esito
curl -s https://api.fattura-elettronica.cloud/api/inoltro \
-H "Accept: application/json" \
-H "Authorization: Bearer IL_TUO_TOKEN"
Se lo SdI accetta il file la risposta è 200:
{
"esito": "ok",
"messaggio": "Invio effettuato",
"inoltro": {
"id": 1532,
"id_sdi": "8754310921",
"piva_mittente": "IT01234567890",
"piva_destinatario": "IT09876543210",
"data_ora_ricezione_sdi": "2026-11-01 10:15:32",
"data_ora_ricezione_esito": null,
"esito": 0
}
}
Conserva id_sdi: è l'identificativo con cui arriveranno tutte le notifiche di quella fattura. L'accettazione dello SdI vuol dire che il file è stato ricevuto; l'esito vero (consegna o scarto) arriva dopo, con le notifiche.
Stato delle fatture inviate
| Metodo | Indirizzo | Risposta |
|---|---|---|
GET | /api/inoltro | inoltri: tutte le tue fatture inviate, dalla più recente |
GET | /api/inoltro/{id} | inoltro: una fattura (404 se non è tua) |
Il campo esito vale:
| Valore | Significato |
|---|---|
0 | In attesa della risposta dello SdI |
1 | RC – ricevuta di consegna: la fattura è stata consegnata |
2 | MC – mancata consegna: la fattura è nel cassetto fiscale del cliente |
3 | NS – notifica di scarto: la fattura non è valida e va corretta e reinviata |
4 | NE – esito del committente (PA): accettata o rifiutata |
5 | DT – decorrenza termini (PA): nessuna risposta entro 15 giorni |
6 | AT – attestazione di avvenuta trasmissione con impossibilità di recapito (PA) |
Il dettaglio di ogni notifica è nella guida alle notifiche SdI. Invece di interrogare queste API, ti conviene ricevere le notifiche sul tuo endpoint.
Ricevere le fatture sul tuo server
Le fatture indirizzate al tuo codice destinatario sono sempre visibili nel portale. Se imposti l'endpoint ricezione fatture e attivi l'inoltro, te le mandiamo anche con una POST application/x-www-form-urlencoded con questi campi:
| Campo | Contenuto |
|---|---|
strxml | XML della fattura in base64, già estratto se il fornitore l'ha firmata (.p7m) |
xmlF | Il file così come è arrivato dallo SdI (.xml o .xml.p7m), in base64 |
xmlM | Il file metadati dello SdI in base64, con IdentificativoSdI e NomeFile |
formato | FPR12 (tra privati) o FPA12 (verso la PA) |
Il tuo server deve rispondere HTTP 200 con il testo OK. Con qualsiasi altra risposta, o se non risponde, riproviamo in automatico con attese crescenti da 5 minuti a 6 ore, per circa 4 giorni. Per questo la stessa fattura può arrivare più volte: usa IdentificativoSdI per non duplicarla.
<?php
// ricevi-fatture.php - URL da indicare in "Endpoint ricezione fatture" (es. https://tuosito.it/ricevi-fatture.php)
const SEGRETO = 'IL_TUO_SEGRETO'; // pagina Integrazione del portale; lascia '' se non usi la firma
$corpo = file_get_contents('php://input');
// Verifica della firma: X-FE-Firma = HMAC-SHA256 di "timestamp\ncorpo" con il tuo segreto
if (SEGRETO !== '') {
$timestamp = $_SERVER['HTTP_X_FE_TIMESTAMP'] ?? '';
$attesa = hash_hmac('sha256', $timestamp . "\n" . $corpo, SEGRETO);
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300
|| !hash_equals($attesa, $_SERVER['HTTP_X_FE_FIRMA'] ?? '')) {
http_response_code(403);
exit('firma non valida');
}
}
parse_str($corpo, $dati);
$xmlFattura = base64_decode($dati['strxml'] ?? ''); // XML della fattura, già estratto dal .p7m
$fileOriginale = base64_decode($dati['xmlF'] ?? ''); // file così come è arrivato dallo SdI (.xml o .xml.p7m)
$metadati = simplexml_load_string(base64_decode($dati['xmlM'] ?? '')); // file metadati dello SdI
$formato = $dati['formato'] ?? ''; // FPR12 tra privati, FPA12 verso la PA
$idSdi = $metadati ? (string) $metadati->IdentificativoSdI : '';
if ($idSdi === '' || $xmlFattura === '') {
http_response_code(400);
exit('dati mancanti');
}
// Salva la fattura fuori dalla cartella pubblica. Stesso IdentificativoSdI = stessa fattura:
// se arriva di nuovo (ad esempio dopo un tuo errore) sovrascrivi, non duplicare.
$cartella = __DIR__ . '/../fatture_ricevute';
if (!is_dir($cartella)) {
mkdir($cartella, 0750, true);
}
file_put_contents("$cartella/$idSdi.xml", $xmlFattura);
file_put_contents("$cartella/{$idSdi}_" . basename((string) $metadati->NomeFile), $fileOriginale);
// Leggi i dati che ti servono direttamente dall'XML
$fattura = simplexml_load_string($xmlFattura);
$fornitore = (string) $fattura->FatturaElettronicaHeader->CedentePrestatore->DatiAnagrafici->Anagrafica->Denominazione;
$numero = (string) $fattura->FatturaElettronicaBody->DatiGenerali->DatiGeneraliDocumento->Numero;
echo 'OK'; // rispondi esattamente OK: con qualsiasi altra risposta la fattura viene rimandata più tardi
# Python 3.8+ con Flask (pip install flask) - URL da indicare in "Endpoint ricezione fatture"
import base64, hashlib, hmac, os, time
import xml.etree.ElementTree as ET
from urllib.parse import parse_qs
from flask import Flask, request
SEGRETO = os.environ.get("FE_SEGRETO", "") # pagina Integrazione del portale; vuoto se non usi la firma
app = Flask(__name__)
def firma_valida(corpo: bytes) -> bool:
"""X-FE-Firma = HMAC-SHA256 di "timestamp\\ncorpo" con il tuo segreto"""
if not SEGRETO:
return True
ts = request.headers.get("X-FE-Timestamp", "")
attesa = hmac.new(SEGRETO.encode(), ts.encode() + b"\n" + corpo, hashlib.sha256).hexdigest()
return (ts.isdigit() and abs(time.time() - int(ts)) <= 300
and hmac.compare_digest(attesa, request.headers.get("X-FE-Firma", "")))
@app.post("/ricevi-fatture")
def ricevi_fatture():
corpo = request.get_data() # il corpo così com'è arrivato, serve per la firma
if not firma_valida(corpo):
return "firma non valida", 403
dati = {k: v[0] for k, v in parse_qs(corpo.decode()).items()}
xml_fattura = base64.b64decode(dati.get("strxml", "")) # XML della fattura, già estratto dal .p7m
file_originale = base64.b64decode(dati.get("xmlF", "")) # file arrivato dallo SdI (.xml o .xml.p7m)
metadati = ET.fromstring(base64.b64decode(dati.get("xmlM", "")))
id_sdi = metadati.findtext("{*}IdentificativoSdI")
if not id_sdi or not xml_fattura:
return "dati mancanti", 400
# Stesso IdentificativoSdI = stessa fattura: se arriva di nuovo sovrascrivi, non duplicare
os.makedirs("fatture_ricevute", exist_ok=True)
with open(f"fatture_ricevute/{id_sdi}.xml", "wb") as f:
f.write(xml_fattura)
return "OK" # rispondi esattamente OK: con qualsiasi altra risposta la fattura viene rimandata
<?php
// ---- app/Http/Middleware/VerificaFirmaFE.php ----
// .env: FE_SEGRETO=... (pagina Integrazione del portale)
// config/services.php: 'fattura_elettronica' => ['segreto' => env('FE_SEGRETO')],
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VerificaFirmaFE
{
public function handle(Request $request, Closure $next)
{
$segreto = config('services.fattura_elettronica.segreto');
if ($segreto) {
// X-FE-Firma = HMAC-SHA256 di "timestamp\ncorpo" con il tuo segreto
$ts = (string) $request->header('X-FE-Timestamp');
$attesa = hash_hmac('sha256', $ts . "\n" . $request->getContent(), $segreto);
abort_unless(ctype_digit($ts) && abs(time() - (int) $ts) <= 300
&& hash_equals($attesa, (string) $request->header('X-FE-Firma')), 403, 'firma non valida');
}
return $next($request);
}
}
// ---- routes/api.php (le route api non hanno il controllo CSRF) ----
// URL da indicare in "Endpoint ricezione fatture": https://tuosito.it/api/fe/fatture
Route::post('/fe/fatture', [FattureSdiController::class, 'fattura'])->middleware(VerificaFirmaFE::class);
// ---- app/Http/Controllers/FattureSdiController.php ----
public function fattura(Request $request)
{
$xml = base64_decode($request->input('strxml')); // XML della fattura, già estratto dal .p7m
$metadati = simplexml_load_string(base64_decode($request->input('xmlM')));
abort_if(!$metadati || $xml === '', 400, 'dati mancanti');
// Stesso IdentificativoSdI = stessa fattura: se arriva di nuovo non la duplichi
FatturaPassiva::firstOrCreate(['id_sdi' => (string) $metadati->IdentificativoSdI], [
'nome_file' => (string) $metadati->NomeFile,
'formato' => $request->input('formato'), // FPR12 tra privati, FPA12 verso la PA
'xml' => $xml,
'originale' => $request->input('xmlF'), // base64 del file arrivato dallo SdI (.xml o .p7m)
]);
return response('OK'); // esattamente OK: con qualsiasi altra risposta la fattura viene rimandata
}
// Node.js 18+ con Express (npm install express) - URL da indicare in "Endpoint ricezione fatture"
import express from "express";
import crypto from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
const SEGRETO = process.env.FE_SEGRETO ?? ""; // pagina Integrazione del portale; vuoto se non usi la firma
const app = express();
// X-FE-Firma = HMAC-SHA256 di "timestamp\ncorpo" con il tuo segreto
function firmaValida(req, corpo) {
if (!SEGRETO) return true;
const ts = req.get("X-FE-Timestamp") ?? "";
const firma = Buffer.from(req.get("X-FE-Firma") ?? "");
const attesa = Buffer.from(crypto.createHmac("sha256", SEGRETO).update(`${ts}\n`).update(corpo).digest("hex"));
return /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) <= 300
&& firma.length === attesa.length && crypto.timingSafeEqual(firma, attesa);
}
// express.raw: serve il corpo così com'è arrivato per verificare la firma
app.post("/ricevi-fatture", express.raw({ type: "*/*", limit: "50mb" }), async (req, res) => {
if (!firmaValida(req, req.body)) return res.status(403).send("firma non valida");
const dati = new URLSearchParams(req.body.toString());
const xmlFattura = Buffer.from(dati.get("strxml") ?? "", "base64"); // XML, già estratto dal .p7m
const metadati = Buffer.from(dati.get("xmlM") ?? "", "base64").toString();
const idSdi = metadati.match(/<IdentificativoSdI>(\d+)</)?.[1];
if (!idSdi || !xmlFattura.length) return res.status(400).send("dati mancanti");
// Stesso IdentificativoSdI = stessa fattura: se arriva di nuovo sovrascrivi, non duplicare
await mkdir("fatture_ricevute", { recursive: true });
await writeFile(`fatture_ricevute/${idSdi}.xml`, xmlFattura);
res.type("text/plain").send("OK"); // esattamente OK: con qualsiasi altra risposta viene rimandata
});
app.listen(3000);
// 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;
}
}
// ASP.NET Core 6+ (minimal API) - Program.cs - URL da indicare in "Endpoint ricezione fatture"
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.WebUtilities;
var segreto = Environment.GetEnvironmentVariable("FE_SEGRETO") ?? ""; // pagina Integrazione; vuoto = senza firma
var app = WebApplication.CreateBuilder(args).Build();
app.MapPost("/ricevi-fatture", async (HttpRequest request) =>
{
using var lettore = new StreamReader(request.Body, Encoding.UTF8);
var corpo = await lettore.ReadToEndAsync(); // il corpo così com'è arrivato, serve per la firma
if (!FirmaValida(request, corpo, segreto)) return Results.StatusCode(403);
var dati = QueryHelpers.ParseQuery(corpo);
var xmlFattura = Convert.FromBase64String(dati["strxml"].ToString()); // XML, già estratto dal .p7m
var metadati = Encoding.UTF8.GetString(Convert.FromBase64String(dati["xmlM"].ToString()));
var idSdi = Regex.Match(metadati, @"<IdentificativoSdI>(\d+)<").Groups[1].Value;
if (idSdi == "" || xmlFattura.Length == 0) return Results.BadRequest("dati mancanti");
// Stesso IdentificativoSdI = stessa fattura: se arriva di nuovo sovrascrivi, non duplicare
Directory.CreateDirectory("fatture_ricevute");
await File.WriteAllBytesAsync(Path.Combine("fatture_ricevute", $"{idSdi}.xml"), xmlFattura);
return Results.Text("OK"); // esattamente OK: con qualsiasi altra risposta viene rimandata
});
app.Run();
// X-FE-Firma = HMAC-SHA256 di "timestamp\ncorpo" con il tuo segreto
static bool FirmaValida(HttpRequest req, string corpo, string segreto)
{
if (segreto == "") return true;
var ts = req.Headers["X-FE-Timestamp"].ToString();
if (!long.TryParse(ts, out var t) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(segreto));
var attesa = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(ts + "\n" + corpo))).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(attesa),
Encoding.ASCII.GetBytes(req.Headers["X-FE-Firma"].ToString()));
}
Ricevere le notifiche dello SdI
Con l'endpoint ricezione notifiche ti inoltriamo ogni notifica appena arriva dallo SdI, nell'ordine in cui è arrivata, con una POST application/x-www-form-urlencoded:
| Campo | Contenuto |
|---|---|
tipoNotifica | 1 RC, 2 MC, 3 NS, 4 NE, 5 DT, 6 AT (gli stessi valori di esito) |
esitojson | La notifica dello SdI convertita in JSON: IdentificativoSdI, NomeFile, DataOraRicezione e i campi specifici (per uno scarto, ListaErrori) |
Anche qui va risposto OK, con gli stessi tentativi automatici. Una notifica non viene mai consegnata prima di quelle precedenti della stessa fattura.
<?php
// ricevi-notifiche.php - URL da indicare in "Endpoint ricezione notifiche"
const SEGRETO = 'IL_TUO_SEGRETO'; // pagina Integrazione del portale; lascia '' se non usi la firma
$corpo = file_get_contents('php://input');
if (SEGRETO !== '') { // stessa verifica della firma dell'endpoint fatture
$ts = $_SERVER['HTTP_X_FE_TIMESTAMP'] ?? '';
$attesa = hash_hmac('sha256', $ts . "\n" . $corpo, SEGRETO);
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300 || !hash_equals($attesa, $_SERVER['HTTP_X_FE_FIRMA'] ?? '')) {
http_response_code(403);
exit('firma non valida');
}
}
parse_str($corpo, $dati);
$tipi = [1 => 'RC', 2 => 'MC', 3 => 'NS', 4 => 'NE', 5 => 'DT', 6 => 'AT'];
$tipo = $tipi[(int) ($dati['tipoNotifica'] ?? 0)] ?? null;
$notifica = json_decode($dati['esitojson'] ?? '', true); // la notifica dello SdI convertita in JSON
if (!$tipo || !$notifica) {
http_response_code(400);
exit('dati mancanti');
}
$stati = [
'RC' => 'consegnata',
'MC' => 'mancata consegna: disponibile nel cassetto fiscale del cliente',
'NS' => 'scartata', // i motivi sono in $notifica['ListaErrori']
'NE' => 'esito del committente', // solo PA: EsitoCommittente.Esito = EC01 accettata, EC02 rifiutata
'DT' => 'decorrenza termini', // solo PA: nessuna risposta entro 15 giorni
'AT' => 'attestazione di avvenuta trasmissione',
];
// IdentificativoSdI è lo stesso restituito dall'API al momento dell'invio (inoltro.id_sdi).
// La stessa notifica può arrivare due volte: l'aggiornamento deve poter essere ripetuto.
$pdo = new PDO('mysql:host=localhost;dbname=gestionale;charset=utf8mb4', 'utente', 'password');
$pdo->prepare('UPDATE fatture SET stato_sdi = ?, notifica_sdi = ? WHERE id_sdi = ?')
->execute([$stati[$tipo], $dati['esitojson'], $notifica['IdentificativoSdI']]);
echo 'OK'; // rispondi esattamente OK: con qualsiasi altra risposta la notifica viene rimandata
# Flask - URL da indicare in "Endpoint ricezione notifiche" (firma_valida è la stessa dell'endpoint fatture)
import json
from urllib.parse import parse_qs
TIPI = {1: "RC", 2: "MC", 3: "NS", 4: "NE", 5: "DT", 6: "AT"}
STATI = {
"RC": "consegnata",
"MC": "mancata consegna: disponibile nel cassetto fiscale del cliente",
"NS": "scartata", # i motivi sono in notifica["ListaErrori"]
"NE": "esito del committente", # solo PA: EsitoCommittente.Esito = EC01 accettata, EC02 rifiutata
"DT": "decorrenza termini", # solo PA: nessuna risposta entro 15 giorni
"AT": "attestazione di avvenuta trasmissione",
}
@app.post("/ricevi-notifiche")
def ricevi_notifiche():
corpo = request.get_data()
if not firma_valida(corpo):
return "firma non valida", 403
dati = {k: v[0] for k, v in parse_qs(corpo.decode()).items()}
tipo = TIPI.get(int(dati.get("tipoNotifica", 0)))
notifica = json.loads(dati.get("esitojson", "null")) # la notifica dello SdI convertita in JSON
if not tipo or not notifica:
return "dati mancanti", 400
# IdentificativoSdI è lo stesso restituito dall'API all'invio (inoltro.id_sdi).
# La stessa notifica può arrivare due volte: l'aggiornamento deve poter essere ripetuto.
aggiorna_fattura(id_sdi=notifica["IdentificativoSdI"], stato=STATI[tipo], dettaglio=notifica)
return "OK" # rispondi esattamente OK: con qualsiasi altra risposta la notifica viene rimandata
<?php
// ---- routes/api.php (stesso middleware VerificaFirmaFE dell'endpoint fatture) ----
// URL da indicare in "Endpoint ricezione notifiche": https://tuosito.it/api/fe/notifiche
Route::post('/fe/notifiche', [FattureSdiController::class, 'notifica'])->middleware(VerificaFirmaFE::class);
// ---- app/Http/Controllers/FattureSdiController.php ----
public function notifica(Request $request)
{
$tipo = [1 => 'RC', 2 => 'MC', 3 => 'NS', 4 => 'NE', 5 => 'DT', 6 => 'AT'][(int) $request->input('tipoNotifica')] ?? null;
$notifica = json_decode((string) $request->input('esitojson'), true); // la notifica SdI convertita in JSON
abort_if(!$tipo || !$notifica, 400, 'dati mancanti');
$stato = [
'RC' => 'consegnata',
'MC' => 'mancata consegna', // disponibile nel cassetto fiscale del cliente
'NS' => 'scartata', // i motivi sono in $notifica['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',
][$tipo];
// IdentificativoSdI è lo stesso restituito dall'API all'invio (inoltro.id_sdi).
// La stessa notifica può arrivare due volte: update ripetibile senza effetti collaterali.
Fattura::where('id_sdi', $notifica['IdentificativoSdI'])
->update(['stato_sdi' => $stato, 'notifica_sdi' => $request->input('esitojson')]);
return response('OK'); // esattamente OK: con qualsiasi altra risposta la notifica viene rimandata
}
// Express - URL da indicare in "Endpoint ricezione notifiche" (firmaValida è la stessa dell'endpoint fatture)
const TIPI = { 1: "RC", 2: "MC", 3: "NS", 4: "NE", 5: "DT", 6: "AT" };
const STATI = {
RC: "consegnata",
MC: "mancata consegna: disponibile nel cassetto fiscale del cliente",
NS: "scartata", // i motivi sono in notifica.ListaErrori
NE: "esito del committente", // solo PA: EsitoCommittente.Esito = EC01 accettata, EC02 rifiutata
DT: "decorrenza termini", // solo PA: nessuna risposta entro 15 giorni
AT: "attestazione di avvenuta trasmissione",
};
app.post("/ricevi-notifiche", express.raw({ type: "*/*", limit: "5mb" }), async (req, res) => {
if (!firmaValida(req, req.body)) return res.status(403).send("firma non valida");
const dati = new URLSearchParams(req.body.toString());
const tipo = TIPI[dati.get("tipoNotifica")];
const notifica = JSON.parse(dati.get("esitojson") ?? "null"); // la notifica SdI convertita in JSON
if (!tipo || !notifica) return res.status(400).send("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.
await aggiornaFattura(notifica.IdentificativoSdI, STATI[tipo], notifica);
res.type("text/plain").send("OK"); // esattamente OK: con qualsiasi altra risposta viene rimandata
});
// 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
}
// ASP.NET Core - nello stesso Program.cs dell'endpoint fatture (FirmaValida è quella)
// URL da indicare in "Endpoint ricezione notifiche"
var tipi = new Dictionary<string, string> { ["1"] = "RC", ["2"] = "MC", ["3"] = "NS", ["4"] = "NE", ["5"] = "DT", ["6"] = "AT" };
var stati = new Dictionary<string, string>
{
["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",
};
app.MapPost("/ricevi-notifiche", async (HttpRequest request) =>
{
using var lettore = new StreamReader(request.Body, Encoding.UTF8);
var corpo = await lettore.ReadToEndAsync();
if (!FirmaValida(request, corpo, segreto)) return Results.StatusCode(403);
var dati = QueryHelpers.ParseQuery(corpo);
if (!tipi.TryGetValue(dati["tipoNotifica"].ToString(), out var tipo)) return Results.BadRequest("dati mancanti");
using var notifica = JsonDocument.Parse(dati["esitojson"].ToString()); // la notifica SdI in JSON
var idSdi = notifica.RootElement.GetProperty("IdentificativoSdI").ToString();
// IdentificativoSdI è lo stesso restituito dall'API all'invio (inoltro.id_sdi).
// La stessa notifica può arrivare due volte: l'aggiornamento deve poter essere ripetuto.
await AggiornaFattura(idSdi, stati[tipo], dati["esitojson"].ToString());
return Results.Text("OK"); // esattamente OK: con qualsiasi altra risposta viene rimandata
});
Firma delle richieste
Per essere sicuro che fatture e notifiche arrivino davvero da noi, genera il tuo segreto personale nella pagina Integrazione del portale. Da quel momento ogni richiesta ai tuoi endpoint porta due header:
X-FE-Timestamp: l'ora dell'invio, in secondi Unix;X-FE-Firma: HMAC-SHA256, in esadecimale minuscolo, ditimestamp + "\n" + corpo della richiesta, calcolato con il tuo segreto.
Calcola la firma sul corpo così com'è arrivato, prima di decodificarlo, confrontala in tempo costante e scarta le richieste con un timestamp più vecchio di 5 minuti. Gli esempi qui sopra lo fanno già.
Errori e limiti
| Codice | Quando |
|---|---|
400 | File mancante, non XML o non leggibile (campo messaggio) |
401 | Token mancante o non più valido: rifai il login |
403 | E-mail non confermata, nessun pacchetto attivo o fatture del mese esaurite |
422 | Lo SdI ha rifiutato subito il file (ad esempio file vuoto): il motivo è in messaggio |
429 | Troppi tentativi di login in un minuto |
503 | Invii momentaneamente sospesi per manutenzione: riprova più tardi |
Il file inviato con le API deve essere XML e pesare al massimo 2 MB; le fatture firmate (.p7m) si caricano dal portale. Il numero di fatture che puoi inviare dipende dal pacchetto; la ricezione è sempre illimitata.
Guide per linguaggio
Le stesse istruzioni, con il codice completo e i dettagli di ogni ambiente:
API fattura elettronica in PHP
PHP 7.4+ con curl
API fattura elettronica in Python
Python 3.8+ con requests e Flask
API fattura elettronica in Laravel
Laravel 8+ con il client HTTP
API fattura elettronica in Node.js
Node.js 18+ con fetch ed Express
API fattura elettronica in Java
Java 11+ e Spring Boot 3
API fattura elettronica in C# / .NET
.NET 6+ e ASP.NET Core
Prova le API gratis
30 giorni e 20 fatture incluse, poi pacchetti da 5 € al mese.