Docs
9

Webhooky

Registrácia adresy, podpisovanie a doručovanie.

  • V sandboxepodpisovanie
  • V sandboxedoručovanie
  • V sandboxeregistrácia

Jednoducho povedané

Webhook umožňuje eurinvoice posielať každú zmenu stavu na webovú adresu, ktorú prevádzkuje klient, takže systém klienta sa nemusí pýtať. Adresu zaregistrujete cez API, každé doručenie je podpísané a neúspešné doručenia sa opakujú. Prijímaciu stranu si budujú vývojári klienta.

Webhook posiela každú stavovú udalosť na URL, ktorú prevádzkujete. Telo je stavová udalosť bez zmeny (pozri stavy). URL zaregistrujete cez API (pozri nižšie), každé doručenie je podpísané a neúspešné doručenie sa opakuje.

Podpis

V sandboxepodpisovanie

Každé doručenie obsahuje tieto hlavičky.

HlavičkaHodnota
X-Eurinvoice-Signaturet=<unix seconds>,v1=<signature>
X-Eurinvoice-Event-Idevent_id udalosti.
X-Eurinvoice-Delivery-Attempt1 pri prvom doručení, 2 pri prvom opakovanom pokuse a tak ďalej.
Content-Typeapplication/json

v1 je HMAC-SHA256 zapísaný ako 64 hexadecimálnych znakov malými písmenami. Kľúčom je tajný kľúč webhooku ako reťazec UTF-8. Správa je t, bodka a nespracované telo. Odmietnite podpis, ktorého t sa od vašich hodín líši o viac ako 300 sekúnd v ktoromkoľvek smere, a porovnávajte v konštantnom čase.

Po rotácii tajného kľúča obsahuje hlavička 24 hodín dve hodnoty v1: jednu podpísanú novým tajným kľúčom, potom jednu starým. Doručenie prijmite, ak sa niektorá hodnota v1 zhoduje s vaším tajným kľúčom. Ak prečítate iba prvú, príjemca, ktorý už prešiel na nový tajný kľúč, odmietne v ten deň každé doručenie.

Varovanie

Kontrolujte telo presne tak, ako prišlo. Kópia po parsovaní a opätovnej serializácii sa nebude zhodovať.

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
    }
}
Spustíte ich bez úprav: java Verify.java, node verify.mjs. Vzorovú hlavičku vytvoril vlastný podpisový kód služby.

Vzorku tvorí tajný kľúč sample-secret, telo {"event_id":"evt_12345678","sequence":1} a hlavička t=1790000000,v1=bd9a6a2d85d7e3706ff8209156fdc43b8291094a4c7af458ed31e7ae9174449e. Pri kontrole v čase 1790000100 je platná. V čase 1790000400 je príliš stará a s "sequence":2 sa telo už nezhoduje.

Obe ukážky kódu sa zhodujú s vlastnou kontrolou služby v 28 prípadoch. Patria medzi ne hlavička stará 301 sekúnd, zmenené telo, nesprávny tajný kľúč, chýbajúca časť, telo s textom mimo ASCII a hlavička s dvoma podpismi po rotácii.

Čo robí príjemca

  1. Poskytujte URL cez HTTPS. Doručovanie ide len na verejné adresy.
  2. Pred čítaním tela overte podpis a jeho vek.
  3. Odpovedzte 2xx do 10 sekúnd. Čokoľvek iné alebo žiadna odpoveď včas je zlyhanie a pokus sa zopakuje.
  4. Uchovávajte event_id každej spracovanej udalosti a udalosť, ktorú ste už videli, ignorujte. Opakovaný pokus môže doručiť tú istú udalosť znova.
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);
Prijímač v Node. Bežal na lokálnom porte a na podpísanú udalosť odpovedal 204, na zmenené telo, zastaraný podpis a chýbajúci podpis 401.

Doručovanie

V sandboxedoručovanie

Neúspešné doručenie sa zopakuje po 1 minúte, potom po 5 minútach, potom po 30 minútach a potom každú hodinu až do 24 hodín od prvého zlyhania. Potom sa presunie do dead-letter. Uložená udalosť sa nikdy nemení. Každý pokus sa znova podpíše s aktuálnym časom, takže opakovaný pokus sa neodmietne ako príliš starý.

Registrácia

V sandboxeregistrácia

Kľúč klienta s oprávnením admin zaregistruje adresu vlastného klienta. Registrácia sú tri volania:

VolanieČo robí
PUT /webhookNastaví url a vytvorí tajný kľúč, ktorý sa vráti raz. Pošlite rotate_secret: true, aby ste vytvorili nový tajný kľúč; 24 hodín obsahuje každé doručenie podpis pre nový tajný kľúč a jeden pre starý, takže môžete prejsť bez straty udalostí. contact_email určuje, koho informovať, keď doručenia zlyhávajú natrvalo.
GET /webhookZobrazí nastavenia: url, active, kedy sa tajný kľúč naposledy rotoval a čas a stav HTTP posledného doručenia. Tajný kľúč sa už nikdy nezobrazí.
POST /webhook/testPošle na adresu podpísanú testovaciu udalosť. Nehlási žiadnu faktúru.

Adresa musí byť https a každá adresa, na ktorú sa jej hostiteľ preloží, musí byť verejná. Adresy loopback, súkromné, link-local, multicast a metadátové adresy cloudu sa odmietnu, pri nastavení aj znova pred každým doručením, a presmerovania sa nesledujú. Doručujú sa len udalosti zapísané po prvom PUT.

Tip

Na dobehnutie zmien po výpadku čítajte GET /invoices/{id}/events (pozri čítanie faktúry). Vráti všetky udalosti faktúry, takže zmeškané doručenie nič nestratí.

Zatiaľ sa neponúka

  • Vlastné hlavičky.
  • Vzájomné TLS.
  • Log doručení.
  • Pevná zdrojová adresa.

Napíšte nám, ktorú z týchto možností potrebuje váš pilotný projekt.

Na tejto stránke