Docs
9

Webhook-uri

Înregistrarea unei adrese, semnare și livrare.

  • În sandboxsemnarea
  • În sandboxlivrarea
  • În sandboxînregistrarea

În cuvinte simple

Un webhook îi permite lui eurinvoice să trimită fiecare schimbare de stare la o adresă web administrată de client, astfel încât sistemul clientului să nu fie nevoit să întrebe. Înregistrați adresa prin API, fiecare livrare este semnată, iar livrările eșuate sunt reîncercate. Dezvoltatorii clientului construiesc partea care primește mesajele.

Un webhook trimite fiecare eveniment de stare la un URL administrat de dumneavoastră. Corpul este evenimentul de stare, nemodificat (consultați stările). Înregistrați URL-ul prin API (vedeți mai jos), fiecare livrare este semnată, iar o livrare eșuată este reîncercată.

Semnătura

În sandboxsemnarea

Fiecare livrare poartă aceste antete.

AntetValoare
X-Eurinvoice-Signaturet=<unix seconds>,v1=<signature>
X-Eurinvoice-Event-IdValoarea event_id a evenimentului.
X-Eurinvoice-Delivery-Attempt1 pentru prima livrare, 2 pentru prima reîncercare și așa mai departe.
Content-Typeapplication/json

v1 este un HMAC-SHA256, scris ca 64 de caractere hexazecimale cu litere mici. Cheia este secretul webhook-ului, ca șir UTF-8. Mesajul este t, un punct și corpul brut. Refuzați o semnătură al cărei t diferă cu mai mult de 300 de secunde de ceasul dumneavoastră, în oricare direcție, și comparați în timp constant.

După rotirea unui secret, antetul conține două valori v1 timp de 24 de ore: una semnată cu noul secret, apoi una cu cel vechi. Acceptați livrarea dacă oricare v1 se potrivește cu secretul dumneavoastră. Dacă citiți doar prima valoare, un receptor care a trecut deja la noul secret refuză toate livrările din acea zi.

Atenție

Verificați corpul exact așa cum a sosit. O copie parsată și serializată din nou nu se va potrivi.

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
    }
}
Rulați-le așa cum sunt: java Verify.java, node verify.mjs. Antetul exemplu a fost generat de codul propriu de semnare al serviciului.

Exemplul folosește secretul sample-secret, corpul {"event_id":"evt_12345678","sequence":1} și antetul t=1790000000,v1=bd9a6a2d85d7e3706ff8209156fdc43b8291094a4c7af458ed31e7ae9174449e. Verificat la 1790000100, este valid. La 1790000400 este prea vechi, iar cu "sequence":2 corpul nu se mai potrivește.

Ambele fragmente de cod dau același rezultat ca verificarea proprie a serviciului în 28 de cazuri. Printre ele: un antet vechi de 301 secunde, un corp modificat, un secret greșit, o parte lipsă, un corp cu text non-ASCII și antetul cu două semnături de la o rotire.

Ce face receptorul

  1. Serviți URL-ul prin HTTPS. Livrarea se face doar către adrese publice.
  2. Verificați semnătura și vechimea ei înainte de a citi corpul.
  3. Răspundeți cu un 2xx în cel mult 10 secunde. Orice alt răspuns sau lipsa unui răspuns la timp este un eșec și se reîncearcă.
  4. Păstrați valoarea event_id a fiecărui eveniment pe care îl procesați și ignorați un eveniment deja văzut. O reîncercare poate livra din nou același eveniment.
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 receptor Node. A fost rulat pe un port local și a răspuns 204 la un eveniment semnat și 401 la un corp modificat, la o semnătură expirată și la lipsa semnăturii.

Livrarea

În sandboxlivrarea

O livrare eșuată este încercată din nou după 1 minut, apoi după 5 minute, apoi după 30 de minute, apoi în fiecare oră, până la 24 de ore după primul eșec. După aceea, livrarea trece în dead-letter. Evenimentul stocat nu se schimbă niciodată. Fiecare încercare este semnată din nou cu ora curentă, așa că o reîncercare nu este refuzată ca prea veche.

Înregistrarea

În sandboxînregistrarea

Cheia unui client cu domeniul admin înregistrează adresa propriului client. Înregistrarea înseamnă trei apeluri:

ApelCe face
PUT /webhookSetează url și creează secretul, care este returnat o singură dată. Trimiteți rotate_secret: true pentru a crea un secret nou; timp de 24 de ore, fiecare livrare poartă o semnătură pentru noul secret și una pentru cel vechi, astfel încât puteți trece la cel nou fără să pierdeți evenimente. contact_email spune pe cine anunțăm când livrările eșuează definitiv.
GET /webhookArată setările: url, active, când a fost rotit ultima dată secretul, precum și ora și starea HTTP ale ultimei livrări. Secretul nu mai este afișat niciodată.
POST /webhook/testTrimite la adresă un eveniment de test semnat. Nu raportează nicio factură.

Adresa trebuie să fie https, iar fiecare adresă la care se rezolvă gazda ei trebuie să fie publică. Adresele de loopback, private, link-local, multicast și cele de metadate cloud sunt refuzate, atât când o setați, cât și înainte de fiecare livrare, iar redirecționările nu sunt urmate. Sunt livrate doar evenimentele scrise după primul PUT.

Sfat

Citiți GET /invoices/{id}/events pentru a recupera evenimentele după o întrerupere (consultați citirea unei facturi). Apelul returnează toate evenimentele facturii, așa că, dacă o livrare este ratată, nu se pierde nimic.

Ce nu oferim încă

  • Antete personalizate.
  • TLS mutual.
  • Un jurnal al livrărilor.
  • O adresă sursă fixă.

Spuneți-ne de care dintre ele are nevoie proiectul dumneavoastră pilot.

Pe această pagină