Docs
9

Webhookovi

Registracija adrese, potpisivanje i isporuka.

  • U sandboxupotpisivanje
  • U sandboxuisporuka
  • U sandboxuregistracija

Jednostavnim riječima

Webhook omogućuje usluzi eurinvoice da svaku promjenu statusa pošalje na web-adresu kojom upravlja klijent, pa klijentov sustav ne mora sam slati upite. Adresu registrirate putem API-ja, svaka je isporuka potpisana, a neuspjele isporuke ponavljaju se. Stranu koja prima poruke izrađuju klijentovi programeri.

Webhook šalje svaki statusni događaj na URL kojim upravljate. Tijelo je nepromijenjeni statusni događaj (pogledajte statuse). URL registrirate putem API-ja (pogledajte u nastavku), svaka je isporuka potpisana, a neuspjela isporuka ponavlja se.

Potpis

U sandboxupotpisivanje

Svaka isporuka nosi ova zaglavlja.

ZaglavljeVrijednost
X-Eurinvoice-Signaturet=<unix seconds>,v1=<signature>
X-Eurinvoice-Event-Idevent_id događaja.
X-Eurinvoice-Delivery-Attempt1 za prvu isporuku, 2 za prvi ponovni pokušaj i tako dalje.
Content-Typeapplication/json

v1 je HMAC-SHA256, zapisan kao 64 heksadecimalna znaka malim slovima. Ključ je tajna webhooka, kao UTF-8 niz znakova. Poruka je t, točka i neobrađeno tijelo. Odbijte potpis čiji t odstupa od vašeg sata više od 300 sekundi, u bilo kojem smjeru, i uspoređujte u konstantnom vremenu.

Nakon rotacije tajne zaglavlje 24 sata nosi dvije vrijednosti v1: jednu potpisanu novom tajnom, a zatim jednu starom. Prihvatite isporuku ako se bilo koja vrijednost v1 podudara s vašom tajnom. Ako čitate samo prvu, primatelj koji je već prešao na novu tajnu odbijat će svaku isporuku tog dana.

Upozorenje

Provjeravajte tijelo točno onakvo kakvo je stiglo. Parsirana i ponovno serijalizirana kopija neće se podudarati.

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
    }
}
Pokrenite ih takve kakvi jesu: java Verify.java, node verify.mjs. Ogledno zaglavlje izradio je vlastiti kod usluge za potpisivanje.

Primjer čine tajna sample-secret, tijelo {"event_id":"evt_12345678","sequence":1} i zaglavlje t=1790000000,v1=bd9a6a2d85d7e3706ff8209156fdc43b8291094a4c7af458ed31e7ae9174449e. Provjereno u trenutku 1790000100, valjano je. U trenutku 1790000400 prestaro je, a sa "sequence":2 tijelo se više ne podudara.

Oba isječka slažu se s vlastitom provjerom usluge u 28 slučajeva. Među njima su zaglavlje staro 301 sekundu, izmijenjeno tijelo, pogrešna tajna, dio koji nedostaje, tijelo s tekstom koji nije ASCII i zaglavlje s dva potpisa nakon rotacije.

Što radi primatelj

  1. Poslužujte URL preko HTTPS-a. Isporuka ide samo na javne adrese.
  2. Provjerite potpis i njegovu starost prije čitanja tijela.
  3. Odgovorite s 2xx u roku od 10 sekundi. Sve ostalo, kao i izostanak odgovora na vrijeme, smatra se neuspjehom i ponovno se pokušava.
  4. Spremite event_id svakog događaja koji obradite i zanemarite onaj koji ste već vidjeli. Ponovni pokušaj može ponovno isporučiti isti događaj.
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);
Primatelj za Node. Pokrenut je na lokalnom portu i odgovorio je 204 na potpisani događaj te 401 na izmijenjeno tijelo, zastarjeli potpis i izostanak potpisa.

Isporuka

U sandboxuisporuka

Isporuka koja ne uspije ponovno se pokušava nakon 1 minute, zatim nakon 5 minuta, zatim nakon 30 minuta, a zatim svaki sat do 24 sata nakon prvog neuspjeha. Nakon toga premješta se među neisporučive (dead letter). Spremljeni događaj nikad se ne mijenja. Svaki se pokušaj ponovno potpisuje s trenutačnim vremenom, pa se ponovni pokušaj ne odbija kao prestar.

Registracija

U sandboxuregistracija

Ključ klijenta s opsegom admin registrira klijentovu vlastitu adresu. Registracija su tri poziva:

PozivŠto radi
PUT /webhookPostavlja url i izrađuje tajnu, koja se vraća jednom. Pošaljite rotate_secret: true za novu tajnu; 24 sata svaka isporuka nosi potpis za novu tajnu i jedan za staru, pa možete prijeći bez gubitka događaja. contact_email kaže koga obavijestiti kad isporuke odumru.
GET /webhookPrikazuje postavke: url, active, kada je tajna zadnji put rotirana te vrijeme i HTTP status zadnje isporuke. Tajna se nikad više ne prikazuje.
POST /webhook/testŠalje potpisani testni događaj na adresu. Ne prijavljuje nijedan račun.

Adresa mora biti https, a svaka adresa na koju se njezin host razrješava mora biti javna. Adrese loopback, privatne, link-local, multicast i adrese metapodataka oblaka odbijaju se, i pri postavljanju i ponovno prije svake isporuke, a preusmjeravanja se ne slijede. Isporučuju se samo događaji zapisani nakon prvog PUT.

Savjet

Čitajte GET /invoices/{id}/events da nadoknadite propušteno nakon prekida rada (pogledajte čitanje računa). Vraća svaki događaj za račun, pa propuštena isporuka ne znači gubitak.

Još se ne nudi

  • Prilagođena zaglavlja.
  • Uzajamni TLS.
  • Zapisnik isporuka.
  • Fiksna izvorišna adresa.

Recite nam što od toga treba vašem pilotu.

Na ovoj stranici