Hvilke hændelser findes?
Partner-API'et har én hændelse: operation.completed. Den kan oprettes ved en vellykket produktiv journalpostering, produktoprettelse eller fakturakladde. Body er {id, type, operation, operation_id, created_at}, hvor type altid er operation.completed. Den indeholder ingen regnskabsdata. Den indeholder ikke kundeinput, banktransaktioner, saldo, IBAN, providerpayload eller en vilkårlig intern domænerække.
Der udsendes ikke automatisk en hændelse for alle ændringer i Hours. Ændringer fra UI, import, bankafstemning eller andre integrationer er ikke en del af denne kontrakt. En partnermodtager skal derfor ikke bruge operation.completed som dokumentation for fuldstændig synkronisering af kundens regnskab.
Registrér destinationen før review
Webhookadressen er en del af appprofilen og dermed af den konkrete reviewrevision. Den skal være præcis HTTPS på port 443 og på en offentlig destination. Wildcards, credentials i URL, fragmenter og private værter er ikke tilladt. Afsenderen understøtter offentlig IPv4; et domæne med IPv6-adresser afvises konservativt.
Efter godkendelse vælger en tofaktorbekræftet ejer eller administrator Konfigurér webhook. Serveren genererer en hemmelig signeringsværdi og viser den én gang. Den genvises ikke senere. Den serverlagrede værdi krypteres med AES-256-GCM under en versionsmærket nøgle; app-id indgår som additional authenticated data, så ciphertext ikke kan flyttes mellem apps uden afvisning.
En profilændring kræver nyt review og kan tilbagekalde gammel adgang. Ret derfor ikke destinationen midt i drift uden en plan for skiftet. En redirect fra gammel til ny modtager følges ikke som en genvej; den nye adresse skal registreres og vurderes.
Body og eksempel
{
"id": "00000000-0000-4000-8000-000000000a00",
"type": "operation.completed",
"operation": "invoices.create",
"operation_id": "00000000-0000-4000-8000-000000000900",
"created_at": "2026-09-30T08:00:00Z"
}Brug operation_id sammen med operations:read til at hente kvitteringen.
Signaturen og de rå bytes
Afsenderen medsender headeren hours-signature i formatet t=<unixsekunder>,v1=<hexsignatur>. Signaturen er HMAC-SHA256 med signeringshemmeligheden over "<t>.<body>", altså timestamp, punktum og den præcise JSON-body. Headeren hours-delivery er leverings-id'et.
Verificér signaturen på de rå requestbytes, før JSON fortolkes, og sammenlign med en timing-sikker funktion. En parser, der omformaterer JSON, ændrer byteindholdet og kan ødelægge en korrekt signatur. Tillad højst fem minutters tidsafvigelse; modtagerens ur skal derfor være synkroniseret.
// Eksempel på modtagersiden. rawBody er præcis den modtagne UTF-8-body.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function validSignature(rawBody, header, secret, now = Date.now()) {
const m = /^t=(\d{1,12}),v1=([a-f0-9]{64})$/.exec(header ?? '');
if (!m || Math.abs(Math.floor(now / 1000) - Number(m[1])) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${m[1]}.${rawBody}`, 'utf8').digest();
const received = Buffer.from(m[2], 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}En korrekt signatur beviser afsenderens kendskab til signeringshemmeligheden, ikke at modtagerens efterfølgende forretningslogik er korrekt. Kontrollér også hændelsestype, format og din egen referencehåndtering.
Dubletter, kø og genforsøg
Gem leverings-id'et fra hours-delivery i en tabel med unik constraint hos modtageren. Når en allerede behandlet event modtages igen, kvitteres uden at gentage den faglige effekt. Et timeout efter modtagelse kan føre til genlevering, selv om den første behandling lykkedes.
Hændelsen gemmes i en outbox sammen med den vellykkede domæneoperation. En særskilt worker tager en kort lease og forsøger levering. Højst otte automatiske forsøg udføres med stigende ventetid. Et 2xx-svar regnes som leveret. Alle andre svar og transportfejl prøves igen, indtil forsøgene er brugt. Svarer modtageren 410, stopper genforsøgene med det samme.
En successtatus fra modtageren skal først sendes, når modtagelsen er durabelt accepteret. Lang behandling bør køres i modtagerens egen kø efter en sikker optagelse. Ellers risikerer et 200-svar før lagring at miste hændelsen, mens et langt åbent request skaber unødige dubletter.
Netværksgrænsen på Hours-siden
Afsenderen resolver destinationen, afviser private, lokale og reserverede adresser og binder HTTPS-forbindelsen til en kontrolleret adresse. TLS valideres stadig mod det oprindelige værtsnavn. Kun HTTPS på port 443 accepteres. Redirects følges ikke, og der videresendes ingen kundecookie eller Supabase-credential.
Et almindeligt DNS-opslag efterfulgt af et frit fetch() er ikke den samme garanti, fordi navnet kan resolves igen mellem kontrol og forbindelse. Derfor bruger webhookafsenderen sin egen forbindelseshåndtering med socketbinding, TLS og destinationsafvisning.
Hver levering har en samlet frist på fem sekunder, og svarets body læses ikke. Afsenderen gemmer HTTP-status og en begrænset fejlkategori, ikke modtagerens fulde svarbody. Det reducerer risikoen for at få tokens eller personoplysninger tilbage i Hours' driftslog.
Suspension, nøgleskift og drift
Før en levering leases, kontrollerer køen apprevision, grant, operationsscope og ejeradgang. Suspension eller tilbagekaldelse annullerer ventende leveringer. En HTTP-request, der allerede er sendt, kan ikke trækkes tilbage. Modtageren skal derfor også stoppe behandling for forbindelser, som den selv ved er lukket.
Krypteringsnøglesamlingen konfigureres server-side og versionsstyres uden at lægge de hemmelige værdier i Git. Gamle dekrypteringsnøgler bevares, indtil alle relevante webhooksecrets er roteret. En nøglefejl giver en synlig leveringsfejl og udløser ikke en ny tilfældig hemmelighed ved hvert genforsøg.
Worker-ruten beskyttes af en særskilt, lang jobhemmelighed. Drift skal konfigurere kald til den samt alarmer for gamle pending-rækker, gentagne afvisninger, udløbne leases og opbrugte forsøg. Manuelt retry findes i konsollen; det ændrer ikke dataadgangsreglerne.