Docs
9

Webhooki

Rejestracja adresu, podpisywanie i dostarczanie.

  • W sandboxiepodpisywanie
  • W sandboxiedostarczanie
  • W sandboxierejestracja

Prostymi słowami

Webhook pozwala eurinvoice wysyłać każdą zmianę statusu na adres internetowy, który obsługuje klient, więc system klienta nie musi o nią pytać. Adres rejestruje się przez API, każde dostarczenie jest podpisane, a nieudane dostarczenia są ponawiane. Stronę odbierającą budują programiści klienta.

Webhook wysyła każde zdarzenie statusu na adres URL, który Państwo obsługują. Treść to zdarzenie statusu bez zmian (zob. statusy). Adres URL rejestruje się przez API (zob. niżej), każde dostarczenie jest podpisane, a nieudane dostarczenie jest ponawiane.

Podpis

W sandboxiepodpisywanie

Każda dostarczana wiadomość zawiera te nagłówki.

NagłówekWartość
X-Eurinvoice-Signaturet=<unix seconds>,v1=<signature>
X-Eurinvoice-Event-Idevent_id zdarzenia.
X-Eurinvoice-Delivery-Attempt1 dla pierwszego dostarczenia, 2 dla pierwszego ponowienia i tak dalej.
Content-Typeapplication/json

v1 to HMAC-SHA256 zapisany jako 64 małe znaki szesnastkowe. Kluczem jest sekret webhooka jako ciąg UTF-8. Wiadomość to t, kropka i surowa treść. Należy odrzucić podpis, którego t różni się od wskazania Państwa zegara o więcej niż 300 sekund w dowolną stronę, a porównanie wykonywać w stałym czasie.

Po rotacji sekretu nagłówek przez 24 godziny zawiera dwie wartości v1: jedną podpisaną nowym sekretem, a potem jedną starym. Dostarczenie należy przyjąć, jeśli którakolwiek wartość v1 zgadza się z Państwa sekretem. Jeśli odbiorca odczyta tylko pierwszą, a przeszedł już na nowy sekret, przez ten dzień odrzuci każde dostarczenie.

Ostrzeżenie

Treść należy sprawdzać dokładnie w takiej postaci, w jakiej dotarła. Kopia sparsowana i ponownie zserializowana nie będzie zgodna.

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
    }
}
Do uruchomienia bez zmian: java Verify.java, node verify.mjs. Przykładowy nagłówek wygenerował własny kod podpisujący usługi.

Przykład to sekret sample-secret, treść {"event_id":"evt_12345678","sequence":1} i nagłówek t=1790000000,v1=bd9a6a2d85d7e3706ff8209156fdc43b8291094a4c7af458ed31e7ae9174449e. Sprawdzony w chwili 1790000100 jest prawidłowy. W chwili 1790000400 jest za stary, a z "sequence":2 treść przestaje się zgadzać.

Oba fragmenty kodu dają ten sam wynik co własna kontrola usługi w 28 przypadkach. Obejmują one nagłówek sprzed 301 sekund, zmienioną treść, błędny sekret, brakującą część, treść z tekstem spoza ASCII i nagłówek z dwoma podpisami po rotacji.

Zadania odbiorcy

  1. Udostępnić adres URL przez HTTPS. Dostarczanie odbywa się tylko na adresy publiczne.
  2. Sprawdzić podpis i jego wiek przed odczytaniem treści.
  3. Odpowiedzieć kodem 2xx w ciągu 10 sekund. Każda inna odpowiedź lub brak odpowiedzi w tym czasie to niepowodzenie, po którym dostarczenie jest ponawiane.
  4. Zapisywać event_id każdego obsłużonego zdarzenia i pomijać zdarzenia już widziane. Ponowienie może dostarczyć to samo zdarzenie jeszcze raz.
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);
Odbiornik w Node. Uruchomiony na porcie lokalnym odpowiedział 204 na podpisane zdarzenie oraz 401 na zmienioną treść, przeterminowany podpis i brak podpisu.

Dostarczanie

W sandboxiedostarczanie

Dostarczenie, które się nie powiedzie, jest ponawiane po 1 minucie, potem po 5 minutach, potem po 30 minutach, a następnie co godzinę, aż do 24 godzin od pierwszego niepowodzenia. Potem trafia na listę dead letter. Zapisane zdarzenie nigdy się nie zmienia. Każda próba jest podpisywana ponownie z bieżącym czasem, więc ponowienie nie zostaje odrzucone jako za stare.

Rejestracja

W sandboxierejestracja

Klucz klienta z zakresem admin rejestruje własny adres klienta. Rejestracja to trzy wywołania:

WywołanieCo robi
PUT /webhookUstawia url i tworzy sekret, który jest zwracany tylko raz. Wysłanie rotate_secret: true tworzy nowy sekret; przez 24 godziny każde dostarczenie zawiera podpis dla nowego sekretu i dla starego, więc można przełączyć się bez utraty zdarzeń. contact_email wskazuje, kogo powiadomić, gdy dostarczenia przestają się udawać.
GET /webhookPokazuje ustawienia: url, active, kiedy sekret był ostatnio rotowany oraz czas i status HTTP ostatniego dostarczenia. Sekret nigdy nie jest pokazywany ponownie.
POST /webhook/testWysyła na adres podpisane zdarzenie testowe. Nie dotyczy żadnej faktury.

Adres musi być https, a każdy adres, na który wskazuje jego host, musi być publiczny. Adresy pętli zwrotnej, prywatne, link-local, multicast i metadanych chmury są odrzucane, przy ustawianiu i ponownie przed każdym dostarczeniem, a przekierowania nie są śledzone. Dostarczane są tylko zdarzenia zapisane po pierwszym PUT.

Wskazówka

Aby nadrobić zaległości po przerwie w działaniu, należy odczytać GET /invoices/{id}/events (zob. odczyt faktury). Zwraca każde zdarzenie faktury, więc w razie nieodebranego dostarczenia nic nie ginie.

Jeszcze niedostępne

  • Własne nagłówki.
  • Wzajemne uwierzytelnianie TLS.
  • Dziennik dostarczeń.
  • Stały adres źródłowy.

Prosimy dać nam znać, której z tych funkcji potrzebuje Państwa pilotaż.

Na tej stronie