Upute za integraciju webhooka

English version

Ove upute objašnjavaju kako primati webhooke iz sustava SUPER. Namijenjene su developerima koji izrađuju prijemni endpoint na strani klijenta.

1. Što je webhook

Webhook je HTTP POST zahtjev koji SUPER šalje na vaš URL kada se nešto dogodi. Ne morate periodički ispitivati (pollati) naš API. Kada se dogodi događaj, mi pozivamo vaš endpoint.

Endpoint kreirate u SUPER web aplikaciji. Zadajete:

Kod kreiranja endpointa prikazujemo vam tajni ključ (secret) samo jednom. Ključ počinje s whsec_. Kopirajte ga i pohranite na sigurno mjesto. Ne možete ga ponovno vidjeti. Ako ga izgubite, rotirajte ključ u aplikaciji i dobit ćete novi.

2. Događaj invoice.received

Trenutno je dostupan jedan događaj:

Webhook ne sadrži cijeli račun. Javlja vam da je račun stigao i daje vam njegov invoiceGuid. Zatim pozovete GetInvoice u našem API-ju s tim invoiceGuid i pročitate cijeli račun.

3. Zaglavlja zahtjeva (headeri)

Svaki webhook POST ima ova zaglavlja:

ZaglavljeZnačenje
X-Super-Webhook-IdJedinstveni id ove isporuke. Ista vrijednost kao id u tijelu. Koristite ga za otkrivanje duplikata.
X-Super-Webhook-EventTip događaja, npr. invoice.received.
X-Super-Webhook-TimestampVrijeme kada smo kreirali zahtjev, kao Unix timestamp u sekundama (UTC).
X-Super-Webhook-SignaturePotpis zahtjeva. Koristite ga za provjeru da zahtjev zaista dolazi od nas.

Tijelo je uvijek JSON. Content-Type je application/json.

4. JSON tijelo

{
  "id": "0f8c2a1e-4b7d-4a9e-9c3f-2b1d5e6a7c88",
  "type": "invoice.received",
  "createdUtc": "2026-07-03T09:15:42.1234567Z",
  "companyGuid": "8159a7b9-0770-4d27-afe3-e6eb0466d857",
  "data": {
    "invoiceGuid": "63c1964d-0d83-4f8d-b01d-57c94ba453fc"
  }
}

Nazivi polja su camelCase.

PoljeZnačenje
idJedinstveni id ove isporuke. Isti kao zaglavlje X-Super-Webhook-Id.
typeTip događaja, npr. invoice.received.
createdUtcKada smo izgradili payload (UTC).
companyGuidGUID vaše tvrtke na koju se događaj odnosi.
dataPodaci događaja. Za invoice.received sadrži invoiceGuid.
data.invoiceGuidGUID novog računa. Pozovite GetInvoice s ovom vrijednošću i pročitajte cijeli račun.

Testna slanja

Kada u aplikaciji pritisnete "Pošalji testni događaj", šaljemo zahtjev istog oblika, ali je data objekt drugačiji:

{
  "id": "...",
  "type": "invoice.received",
  "createdUtc": "...",
  "companyGuid": "...",
  "data": {
    "test": true
  }
}

Dakle, pravi događaj ima data.invoiceGuid. Testno slanje ima data.test = true.

5. Provjera potpisa

Uvijek provjerite potpis prije nego što povjerujete zahtjevu. Time dokazujete da zahtjev dolazi od nas i da nije mijenjan.

Kako mi gradimo potpis:

  1. Uzmemo timestamp iz zaglavlja X-Super-Webhook-Timestamp (Unix sekunde).
  2. Izgradimo string "{timestamp}.{body}". body je točno onaj sirovi (raw) JSON koji smo poslali.
  3. Izračunamo HMAC-SHA256 nad tim stringom, koristeći vaš whsec_ ključ kao tajnu.
  4. Rezultat zapišemo kao mala heksadecimalna slova, s prefiksom sha256=.

Za provjeru napravite iste korake sa sirovim tijelom koje ste primili, pa usporedite svoj rezultat sa zaglavljem X-Super-Webhook-Signature.

Dva važna pravila:

C# primjer:

using System.Globalization;
using System.Security.Cryptography;
using System.Text;

public static bool IsValidWebhook( string secret, string timestampHeader, string signatureHeader, string rawBody )
{
    // 1) Odbij stare zahtjeve (zaštita od replaya).
    if ( !long.TryParse( timestampHeader, NumberStyles.Integer, CultureInfo.InvariantCulture, out var unixSeconds ) )
    {
        return false;
    }

    var sentAt = DateTimeOffset.FromUnixTimeSeconds( unixSeconds );
    if ( DateTimeOffset.UtcNow - sentAt > TimeSpan.FromMinutes( 5 ) )
    {
        return false;
    }

    // 2) Ponovno izračunaj potpis nad "{timestamp}.{body}".
    var payload = $"{timestampHeader}.{rawBody}";
    using var hmac = new HMACSHA256( Encoding.UTF8.GetBytes( secret ) );
    var hash = hmac.ComputeHash( Encoding.UTF8.GetBytes( payload ) );
    var expected = "sha256=" + Convert.ToHexString( hash ).ToLowerInvariant( );

    // 3) Usporedba u konstantnom vremenu.
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes( expected ),
        Encoding.UTF8.GetBytes( signatureHeader ) );
}
Pročitajte sirovo tijelo kao tekst, točno kako je stiglo. Nemojte parsirati pa ponovno serijalizirati JSON prije provjere potpisa. Ponovna serijalizacija može promijeniti bajtove i provjera će pasti.

6. Kako odgovoriti

Vratite 2xx status kod što brže možete (200, 201, 202 i 204 svi vrijede). Time nam javljate da je isporuka uspjela.

Teški posao odradite kasnije. Spremite događaj, odgovorite 2xx, pa ga obradite u pozadini. Naš zahtjev istječe nakon 10 sekundi.

Ako vratite bilo koji drugi status kod, ili je vaš endpoint spor ili nedostupan, isporuku smatramo neuspjelom i ponavljamo je.

7. Pravila ponavljanja (retry)

Ako isporuka ne uspije, ponavljamo je. Nakon svakog pokušaja čekamo dulje:

Nakon neuspjelog pokušajaČekamo
11 minutu
25 minuta
330 minuta
42 sata
56 sati

Nakon 6 neuspjelih pokušaja isporuka prelazi u status FailedFinal (trajno neuspješno). Tu isporuku više ne ponavljamo automatski.

Ako jedan endpoint 5 puta zaredom završi u FailedFinal, gasimo taj endpoint. Zapišemo razlog na endpoint i pošaljemo email vašoj tvrtki. Dok je endpoint ugašen, ne šaljemo mu nove webhooke. Ponovno ga uključite u aplikaciji; time se resetira i brojač grešaka.

Uspješna isporuka resetira brojač na nulu.

8. Obrada duplikata (idempotentnost)

Isti webhook id može stići više puta. To se može dogoditi nakon ponavljanja, ili kada netko u aplikaciji pritisne "Pošalji ponovno". Ručno ponovno slanje koristi isti id; ne stvara novi.

Zato svaki id obradite samo jednom. Prije obrade provjerite jeste li taj id već obradili. Ako jeste, odgovorite 2xx i ne radite ništa drugo. Tako vaša strana ostaje ispravna i kada webhook stigne dvaput.