Hop til hovedindhold

Webhooks

Frihet webhooks gør det muligt at modtage HTTP-notifikationer i realtid, når der opstår begivenheder på din konto. I stedet for at polle API'en, skal du konfigurere en URL, og Frihet sender en POST med begivenhedsdataene, så snart den opstår.

Konfiguration

Du kan administrere webhooks fra Frihet-panelet (Konfiguration > Webhooks) eller programmatisk via REST API'en.

Fra panelet

  1. Gå til Konfiguration > Webhooks på din Frihet-konto
  2. Klik på Opret webhook
  3. Indtast et beskrivende navn, destinations-URL'en, og vælg de begivenheder, du vil modtage
  4. Valgfrit, definer en hemmelig nøgle til HMAC-verifikation (anbefales)
  5. Gem webhook'en

Webhooks REST API

Administrer webhooks programmatisk med de komplette CRUD-endpoints.

Vis webhooks

GET /v1/webhooks
curl https://api.frihet.io/v1/webhooks \
-H "X-API-Key: fri_tu-clave-aqui"

Svar (200):

{
"data": [
{
"id": "wh_abc123",
"url": "https://mi-app.com/hook",
"events": ["invoice.paid", "invoice.created"],
"active": true,
"createdAt": "2026-02-01T10:00:00.000Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}

Hent webhook

GET /v1/webhooks/:id
curl https://api.frihet.io/v1/webhooks/wh_abc123 \
-H "X-API-Key: fri_tu-clave-aqui"

Opret webhook

POST /v1/webhooks
FeltTypePåkrævetBeskrivelse
urlstringJaDestinations-URL (HTTPS obligatorisk i produktion)
eventsstring[]JaListe over begivenhedstyper, der skal modtages
secretstringNejHemmelig nøgle til HMAC-SHA256 signatur (anbefales)
activebooleanNejWebhook-status (standard: true)
curl -X POST https://api.frihet.io/v1/webhooks \
-H "X-API-Key: fri_tu-clave-aqui" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mi-app.com/hook",
"events": ["invoice.paid", "expense.created"],
"secret": "mi-secreto-hmac"
}'

Svar (201):

{
"id": "wh_def456",
"url": "https://mi-app.com/hook",
"events": ["invoice.paid", "expense.created"],
"active": true,
"createdAt": "2026-03-18T10:00:00.000Z"
}

Opdater webhook

PATCH /v1/webhooks/:id

Du behøver kun at sende de felter, du vil ændre.

curl -X PATCH https://api.frihet.io/v1/webhooks/wh_def456 \
-H "X-API-Key: fri_tu-clave-aqui" \
-H "Content-Type: application/json" \
-d '{
"events": ["invoice.paid", "invoice.created", "expense.created"],
"active": false
}'

Svar (200): Opdateret webhook-objekt.

Slet webhook

DELETE /v1/webhooks/:id
curl -X DELETE https://api.frihet.io/v1/webhooks/wh_def456 \
-H "X-API-Key: fri_tu-clave-aqui"

Svar: 204 No Content


URL-krav:

  • Skal bruge HTTPS (HTTP er kun tilladt for localhost og 127.0.0.1 under udvikling)
  • Private IP'er (10.x, 172.16-31.x, 192.168.x) er ikke tilladt for at undgå SSRF-angreb
  • Skal svare med en 2xx-statuskode på under 30 sekunder

Grænser:

  • Maksimalt 20 webhooks pr. konto
  • Maksimal payload på 100 KB pr. levering

Begivenhedstyper

Frihet udsender 14 begivenhedstyper, grupperet efter ressource.

Fakturaer (4 begivenheder)

BegivenhedBeskrivelse
invoice.createdEn ny faktura er oprettet
invoice.updatedEn eksisterende faktura er ændret
invoice.paidEn faktura er markeret som betalt
invoice.overdueEn faktura har overskredet sin forfaldsdato

Udgifter (2 begivenheder)

BegivenhedBeskrivelse
expense.createdEn ny udgift er registreret
expense.updatedEn eksisterende udgift er ændret

Tilbud (4 begivenheder)

BegivenhedBeskrivelse
quote.createdEt nyt tilbud er oprettet
quote.updatedEt eksisterende tilbud er ændret
quote.acceptedEn kunde har accepteret et tilbud (status ændret til accepted)
quote.rejectedEn kunde har afvist et tilbud (status ændret til rejected)

Kunder (2 begivenheder)

BegivenhedBeskrivelse
client.createdEn ny kunde er oprettet
client.updatedEn kundes data er ændret

Produkter (2 begivenheder)

BegivenhedBeskrivelse
product.createdEt nyt produkt eller en ny service er oprettet
product.updatedEt eksisterende produkt eller en eksisterende service er ændret

Payload-struktur

Hver webhook-levering er en POST-anmodning med JSON-body. Strukturen er den samme for alle begivenhedstyper:

{
"event": "invoice.paid",
"timestamp": "2026-02-12T14:30:00.000Z",
"data": {
"id": "inv_abc123",
"clientName": "Acme S.L.",
"items": [
{ "description": "Consultoria", "quantity": 10, "unitPrice": 75 }
],
"total": 750,
"status": "paid",
"paidAt": "2026-02-12T14:29:58.000Z",
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-02-12T14:30:00.000Z"
}
}

Feltet data indeholder ressourcens fulde tilstand på tidspunktet for begivenheden.


Leveringsheaders

Hver webhook-anmodning inkluderer følgende headers:

HeaderBeskrivelseEksempel
Content-TypeIndholdstypeapplication/json
X-Frihet-EventBegivenhedstypeinvoice.paid
X-Frihet-Delivery-IdUnikt leverings-idd4e5f6a7b8c9
X-Frihet-TimestampISO 8601 tidsstempel2026-02-12T14:30:00.000Z
X-Frihet-SignatureHMAC-SHA256 signatur af payload'etsha256=a1b2c3d4e5f6...

Headeren X-Frihet-Signature inkluderes kun, hvis du har konfigureret en hemmelig nøgle i webhook'en.


Signaturverifikation

Hvis du konfigurerer en hemmelig nøgle, når du opretter webhook'en, signerer Frihet hvert payload med HMAC-SHA256. Verifikationsprocessen består af:

  1. Tag den rå anmodnings-body (raw body)
  2. Beregn HMAC-SHA256 ved at bruge den hemmelige nøgle som nøgle
  3. Sammenlign resultatet med værdien af headeren X-Frihet-Signature (uden sha256= præfikset)

Med SDK'en (anbefales)

import { Webhooks } from '@frihet/sdk';

app.post('/webhook/frihet', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = Webhooks.verifySignature(
req.body,
req.headers['x-frihet-signature'],
process.env.FRIHET_WEBHOOK_SECRET,
);

if (!isValid) return res.status(401).send('Firma invalida');

const event = JSON.parse(req.body.toString());
console.log(req.headers['x-frihet-event'], event.data);
res.sendStatus(200);
});

Manuelt eksempel i Node.js

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');

const receivedSignature = signature.replace('sha256=', '');

return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}

// Brug i en Express-server
app.post('/webhook/frihet', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-frihet-signature'];
const secret = process.env.FRIHET_WEBHOOK_SECRET;

if (!signature || !verifyWebhookSignature(req.body.toString(), signature, secret)) {
return res.status(401).send('Ugyldig signatur');
}

const event = JSON.parse(req.body.toString());
const eventType = req.headers['x-frihet-event'];

console.log(`Begivenhed modtaget: ${eventType}`, event.data);

// Behandl begivenheden i henhold til dens type
switch (eventType) {
case 'invoice.paid':
// Opdater dit regnskabssystem
break;
case 'expense.created':
// Underret finansteamet
break;
}

res.status(200).send('OK');
});

Eksempel i Python

import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = 'tu-secreto-aqui'

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()

received = signature.replace('sha256=', '')

return hmac.compare_digest(expected, received)

@app.route('/webhook/frihet', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Frihet-Signature', '')
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
abort(401)

event = request.get_json()
event_type = request.headers.get('X-Frihet-Event')

print(f'Begivenhed modtaget: {event_type}')

if event_type == 'invoice.paid':
# Opdater dit regnskabssystem
pass
elif event_type == 'quote.accepted':
# Konverter tilbud til faktura
pass

return 'OK', 200

Vigtigt: Brug altid en tidskonstant sammenligning (timingSafeEqual i Node.js, compare_digest i Python) for at undgå timing-angreb.


Genforsøgspolitik

Hvis en webhook-levering mislykkes (ikke-2xx svarstatuskode eller timeout), genforsøger Frihet automatisk med eksponentiel backoff:

ForsøgForsinkelseAkkumuleret tid
1 (initialt)øjeblikkeligt0s
2 (første genforsøg)2 sekunder2s
3 (andet genforsøg)4 sekunder6s
  • Maksimalt 3 forsøg pr. levering (1 initialt + 2 genforsøg)
  • Maksimal forsinkelse på 30 sekunder mellem genforsøg
  • Timeout på 30 sekunder pr. anmodning
  • Hvis alle 3 forsøg mislykkes, markeres leveringen som failed

Genforsøg behandles via et job, der kører hvert 5. minut, hvilket garanterer pålidelighed, selvom hovedprocessen genstartes.

Du kan konsultere leveringshistorikken for hver webhook fra Frihet-panelet, inklusive svarstatuskoden, svar-body (de første 1000 tegn) og fejl for hvert forsøg.


Test

Fra Frihet-panelet kan du sende en testbegivenhed til enhver konfigureret webhook. Test-payload'et har følgende struktur:

{
"event": "webhook.test",
"timestamp": "2026-02-12T14:30:00.000Z",
"data": {
"message": "This is a test webhook from Frihet ERP"
}
}

Dette giver dig mulighed for at verificere, at dit endpoint er tilgængeligt, at signaturen valideres korrekt, og at dit system behandler begivenhederne uden fejl.


Bedste praksis

Svar hurtigt

Dit endpoint skal svare med en 200-statuskode så hurtigt som muligt. Hvis du har brug for at udføre tung behandling (sende e-mails, opdatere eksterne databaser osv.), skal du acceptere begivenheden og behandle den asynkront i en jobkø.

Håndter idempotens

Det er muligt, at den samme begivenhed leveres mere end én gang (for eksempel, hvis din server svarede med en timeout, men behandlede begivenheden). Brug feltet X-Frihet-Delivery-Id som en idempotens-nøgle for at undgå dubletter.

Verificer altid signaturen

Stol aldrig på en webhook uden at verificere X-Frihet-Signature headeren. Enhver aktør med adgang til din URL kunne sende falske payloads.

Brug HTTPS

I produktion skal dit endpoint være beskyttet med HTTPS. Frihet afviser HTTP-URL'er (undtagen under lokal udvikling).

Overvåg fejl

Gennemgå regelmæssigt leveringsloggene i Frihet-panelet. Hvis du ser gentagne mislykkede leveringer, skal du verificere, at dit endpoint er tilgængeligt og svarer på under 30 sekunder.

Filtrer begivenhederne

Abonner kun på de begivenheder, du har brug for. Hver webhook kan lytte til en eller flere begivenhedstyper. Jo færre unødvendige begivenheder du behandler, jo mindre belastning er der på din server.


Debugging af leverancer

Frihet logger resultatet af hver webhook-levering. Du kan konsultere det fra panelet:

  1. Gå til Indstillinger > Udviklere > Webhooks
  2. Klik på den webhook, du vil inspicere
  3. Åbn fanen Leverancer

Hver post viser:

  • HTTP-svarkode returneret af dit endpoint
  • Svartid i millisekunder
  • Svar-body (de første 1000 tegn)
  • Dato og tid for hvert forsøg (inklusive genforsøg)
  • Endelig status: delivered, retrying eller failed

Almindelige problemer

SymptomSandsynlig årsagLøsning
Alle leveringer mislykkes med timeoutEndpoint tager mere end 30 sekunder at svareAccepter begivenheden med et øjeblikkeligt 200 og behandl asynkront
SSL/TLS-fejlUdløbet certifikat eller ufuldstændig kædeForny certifikatet og verificer kæden med openssl s_client
Systematisk 403-statuskodeFirewall blokerer indgående POST-anmodningerTillad trafik fra Google Cloud IP'er (us-central1)
502/503-statuskodeServer nede eller under vedligeholdelseGennemgå din servers logs og verificer, at processen er aktiv

Testknap

På detaljeskærmen for hver webhook sender knappen Send test en syntetisk webhook.test begivenhed til din URL. Brug denne knap til at verificere:

  • At URL'en er tilgængelig fra internettet
  • At HMAC-signaturen valideres korrekt
  • At din server svarer med en 2xx-statuskode

Testresultatet vises med det samme i leveringsloggen.


Fejlfinding

Jeg modtager ikke webhooks

  1. Verificer, at webhook-URL'en er korrekt og tilgængelig fra internettet
  2. Tjek, at webhook'en er i aktiv status i panelet
  3. Gennemgå leveringsloggene for at se, om der er fejl
  4. Hvis du bruger en firewall, skal du sørge for, at indgående POST-anmodninger fra Google Cloud IP'er (us-central1) er tilladt

Signaturer stemmer ikke overens

  1. Verificer, at du bruger den rå anmodnings-body (før JSON-parsing)
  2. Bekræft, at den hemmelige nøgle i din kode matcher den, der er konfigureret i Frihet-panelet
  3. Du må ikke ændre eller omformatere body'en, før du verificerer signaturen
  4. Tjek, at dit framework ikke automatisk parser body'en, før du kan få adgang til den rå body

Genforsøg modtages ikke

  1. Genforsøg behandles hvert 5. minut. Hvis dit endpoint var nede kortvarigt, er forsøgene muligvis allerede udtømt
  2. Gennemgå leveringshistorikken for at bekræfte status for hvert forsøg
  3. Hvis alle 3 forsøg mislykkedes, vil leveringen ikke blive genforsøgt automatisk

Payload er for stor

Hvis den ressource, der er knyttet til begivenheden, er meget stor (mange linjer i en faktura, omfattende felter), kan payload'et overskride grænsen på 100 KB, og leveringen vil blive afvist. Forenkle ressourcens data, eller kontakt support, hvis du har brug for en højere grænse.