Docs
9

Webhook

Registrazione di un indirizzo, firma e consegna.

  • Nella sandboxfirma
  • Nella sandboxconsegna
  • Nella sandboxregistrazione

In parole semplici

Un webhook permette a eurinvoice di inviare ogni cambio di stato a un indirizzo web gestito dal cliente, così il sistema del cliente non deve interrogare il servizio. L’indirizzo si registra tramite l’API, ogni consegna è firmata e le consegne non riuscite vengono ritentate. Gli sviluppatori del cliente realizzano la parte ricevente.

Un webhook invia ogni evento di stato a un URL gestito in proprio. Il corpo è l’evento di stato, invariato (vedere stati). L’URL si registra tramite l’API (vedere sotto), ogni consegna è firmata e una consegna non riuscita viene ritentata.

Firma

Nella sandboxfirma

Ogni consegna riporta queste intestazioni.

IntestazioneValore
X-Eurinvoice-Signaturet=<unix seconds>,v1=<signature>
X-Eurinvoice-Event-IdL’event_id dell’evento.
X-Eurinvoice-Delivery-Attempt1 per la prima consegna, 2 per il primo nuovo tentativo, e così via.
Content-Typeapplication/json

v1 è un HMAC-SHA256, scritto come 64 caratteri esadecimali minuscoli. La chiave è il segreto del webhook, come stringa UTF-8. Il messaggio è t, un punto e il corpo grezzo. Rifiutare una firma il cui t dista più di 300 secondi dal proprio orologio, in entrambe le direzioni, e confrontare in tempo costante.

Dopo una rotazione del segreto l’intestazione riporta due valori v1 per 24 ore: uno firmato con il nuovo segreto, poi uno con il vecchio. Accettare la consegna se un qualsiasi v1 corrisponde al proprio segreto. Chi legge solo il primo, con un ricevitore che è già passato al nuovo segreto, rifiuta ogni consegna di quel giorno.

Avvertenza

Controllare il corpo esattamente come è arrivato. Una copia analizzata e serializzata di nuovo non corrisponderà.

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Verify {
    /**
     * True when one of the header's v1 values matches the raw body and the header is at most five minutes old.
     * For 24 hours after a secret rotation the header carries two v1 values, so check them all.
     * Pass the body exactly as it arrived.
     */
    static boolean verify(String secret, String header, byte[] rawBody, long nowSeconds) throws Exception {
        String t = null;
        java.util.List<String> signatures = new java.util.ArrayList<>();
        for (String part : header == null ? new String[0] : header.split(",")) {
            String[] pair = part.trim().split("=", 2);
            if (pair.length == 2 && pair[0].equals("t")) t = pair[1];
            if (pair.length == 2 && pair[0].equals("v1")) signatures.add(pair[1]);
        }
        if (t == null) return false;
        long seconds;
        try {
            seconds = Long.parseLong(t);
        } catch (NumberFormatException e) {
            return false;
        }
        if (Math.abs(nowSeconds - seconds) > 300) return false;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        mac.update((seconds + ".").getBytes(StandardCharsets.UTF_8));
        byte[] expected = mac.doFinal(rawBody);
        boolean ok = false;
        for (String signature : signatures) {
            if (signature.matches("[0-9a-f]{64}") && MessageDigest.isEqual(expected, HexFormat.of().parseHex(signature))) ok = true;
        }
        return ok;
    }

    // Check it against the sample on this page: java Verify.java
    public static void main(String[] args) throws Exception {
        String secret = "sample-secret";
        byte[] body = "{\"event_id\":\"evt_12345678\",\"sequence\":1}".getBytes(StandardCharsets.UTF_8);
        String header = "t=1790000000,v1=bd9a6a2d85d7e3706ff8209156fdc43b8291094a4c7af458ed31e7ae9174449e";
        System.out.println(verify(secret, header, body, 1790000100L)); // true
        System.out.println(verify(secret, header, body, 1790000400L)); // false: older than five minutes
        byte[] changed = "{\"event_id\":\"evt_12345678\",\"sequence\":2}".getBytes(StandardCharsets.UTF_8);
        System.out.println(verify(secret, header, changed, 1790000100L)); // false: body changed
    }
}
Si eseguono così come sono: java Verify.java, node verify.mjs. L’intestazione di esempio è stata generata dal codice di firma del servizio stesso.

L’esempio usa il segreto sample-secret, il corpo {"event_id":"evt_12345678","sequence":1} e l’intestazione t=1790000000,v1=bd9a6a2d85d7e3706ff8209156fdc43b8291094a4c7af458ed31e7ae9174449e. Controllata al tempo 1790000100 è valida. Al tempo 1790000400 è troppo vecchia, e con "sequence":2 il corpo non corrisponde più.

Entrambi gli snippet concordano con il controllo del servizio stesso su 28 casi. Tra questi ci sono un’intestazione vecchia di 301 secondi, un corpo modificato, un segreto errato, una parte mancante, un corpo con testo non ASCII e l’intestazione con due firme di una rotazione.

Cosa fa un ricevitore

  1. Esporre l’URL su HTTPS. La consegna avviene solo verso indirizzi pubblici.
  2. Verificare la firma e la sua età prima di leggere il corpo.
  3. Rispondere con un 2xx entro 10 secondi. Qualsiasi altra risposta, o l’assenza di risposta in tempo, conta come errore e la consegna viene ritentata.
  4. Conservare l’event_id di ogni evento gestito, e ignorare un evento già visto. Un nuovo tentativo può consegnare di nuovo lo stesso evento.
receiver.mjs
import http from 'node:http';
import { verify } from './verify.mjs';

const seen = new Set(); // keep these in a database in real use

http
  .createServer(async (req, res) => {
    const chunks = [];
    for await (const chunk of req) chunks.push(chunk);
    const body = Buffer.concat(chunks);
    if (!verify(process.env.WEBHOOK_SECRET, req.headers['x-eurinvoice-signature'], body)) {
      res.writeHead(401).end();
      return;
    }
    const event = JSON.parse(body);
    if (!seen.has(event.event_id)) {
      seen.add(event.event_id);
      // hand the event to your own queue here, then answer quickly
    }
    res.writeHead(204).end(); // any 2xx within 10 seconds counts as delivered
  })
  .listen(3000);
Un ricevitore Node. È stato eseguito su una porta locale e ha risposto 204 a un evento firmato e 401 a un corpo modificato, a una firma scaduta e a una richiesta senza firma.

Consegna

Nella sandboxconsegna

Una consegna non riuscita viene ritentata dopo 1 minuto, poi dopo 5 minuti, poi dopo 30 minuti, poi ogni ora fino a 24 ore dopo il primo errore. Dopodiché passa in dead letter. L’evento archiviato non viene mai modificato. Ogni tentativo viene firmato di nuovo con l’ora corrente, quindi un nuovo tentativo non viene rifiutato come troppo vecchio.

Registrazione

Nella sandboxregistrazione

La chiave di un cliente con lo scope admin registra l’indirizzo del proprio cliente. La registrazione si fa con tre chiamate:

ChiamataCosa fa
PUT /webhookImposta l’url e crea il segreto, che viene restituito una sola volta. Inviare rotate_secret: true per creare un nuovo segreto; per 24 ore ogni consegna riporta una firma per il nuovo segreto e una per il vecchio, così si può passare all’altro senza perdere eventi. contact_email indica chi avvisare quando le consegne si interrompono.
GET /webhookMostra le impostazioni: url, active, quando il segreto è stato ruotato l’ultima volta, e l’orario e lo stato HTTP dell’ultima consegna. Il segreto non viene mai mostrato di nuovo.
POST /webhook/testInvia all’indirizzo un evento di test firmato. Non riguarda alcuna fattura.

L’indirizzo deve essere https, e ogni indirizzo a cui il suo host si risolve deve essere pubblico. Gli indirizzi di loopback, privati, link-local, multicast e dei metadati del cloud vengono rifiutati, sia quando lo si imposta sia prima di ogni consegna, e i reindirizzamenti non vengono seguiti. Vengono consegnati solo gli eventi scritti dopo il primo PUT.

Suggerimento

Leggere GET /invoices/{id}/events per mettersi in pari dopo un’interruzione (vedere leggere una fattura). Restituisce ogni evento della fattura, quindi una consegna mancata non fa perdere nulla.

Non ancora disponibili

  • Intestazioni personalizzate.
  • TLS reciproco.
  • Un registro delle consegne.
  • Un indirizzo di origine fisso.

Indicarci quale di questi serve al progetto pilota.

In questa pagina