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
- Gå til Konfiguration > Webhooks på din Frihet-konto
- Klik på Opret webhook
- Indtast et beskrivende navn, destinations-URL'en, og vælg de begivenheder, du vil modtage
- Valgfrit, definer en hemmelig nøgle til HMAC-verifikation (anbefales)
- 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
| Felt | Type | Påkrævet | Beskrivelse |
|---|---|---|---|
url | string | Ja | Destinations-URL (HTTPS obligatorisk i produktion) |
events | string[] | Ja | Liste over begivenhedstyper, der skal modtages |
secret | string | Nej | Hemmelig nøgle til HMAC-SHA256 signatur (anbefales) |
active | boolean | Nej | Webhook-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
localhostog127.0.0.1under 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)
| Begivenhed | Beskrivelse |
|---|---|
invoice.created | En ny faktura er oprettet |
invoice.updated | En eksisterende faktura er ændret |
invoice.paid | En faktura er markeret som betalt |
invoice.overdue | En faktura har overskredet sin forfaldsdato |
Udgifter (2 begivenheder)
| Begivenhed | Beskrivelse |
|---|---|
expense.created | En ny udgift er registreret |
expense.updated | En eksisterende udgift er ændret |
Tilbud (4 begivenheder)
| Begivenhed | Beskrivelse |
|---|---|
quote.created | Et nyt tilbud er oprettet |
quote.updated | Et eksisterende tilbud er ændret |
quote.accepted | En kunde har accepteret et tilbud (status ændret til accepted) |
quote.rejected | En kunde har afvist et tilbud (status ændret til rejected) |
Kunder (2 begivenheder)
| Begivenhed | Beskrivelse |
|---|---|
client.created | En ny kunde er oprettet |
client.updated | En kundes data er ændret |
Produkter (2 begivenheder)
| Begivenhed | Beskrivelse |
|---|---|
product.created | Et nyt produkt eller en ny service er oprettet |
product.updated | Et 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:
| Header | Beskrivelse | Eksempel |
|---|---|---|
Content-Type | Indholdstype | application/json |
X-Frihet-Event | Begivenhedstype | invoice.paid |
X-Frihet-Delivery-Id | Unikt leverings-id | d4e5f6a7b8c9 |
X-Frihet-Timestamp | ISO 8601 tidsstempel | 2026-02-12T14:30:00.000Z |
X-Frihet-Signature | HMAC-SHA256 signatur af payload'et | sha256=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:
- Tag den rå anmodnings-body (raw body)
- Beregn HMAC-SHA256 ved at bruge den hemmelige nøgle som nøgle
- Sammenlign resultatet med værdien af headeren
X-Frihet-Signature(udensha256=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øg | Forsinkelse | Akkumuleret tid |
|---|---|---|
| 1 (initialt) | øjeblikkeligt | 0s |
| 2 (første genforsøg) | 2 sekunder | 2s |
| 3 (andet genforsøg) | 4 sekunder | 6s |
- 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:
- Gå til Indstillinger > Udviklere > Webhooks
- Klik på den webhook, du vil inspicere
- Å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,retryingellerfailed
Almindelige problemer
| Symptom | Sandsynlig årsag | Løsning |
|---|---|---|
| Alle leveringer mislykkes med timeout | Endpoint tager mere end 30 sekunder at svare | Accepter begivenheden med et øjeblikkeligt 200 og behandl asynkront |
| SSL/TLS-fejl | Udløbet certifikat eller ufuldstændig kæde | Forny certifikatet og verificer kæden med openssl s_client |
| Systematisk 403-statuskode | Firewall blokerer indgående POST-anmodninger | Tillad trafik fra Google Cloud IP'er (us-central1) |
| 502/503-statuskode | Server nede eller under vedligeholdelse | Gennemgå 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
- Verificer, at webhook-URL'en er korrekt og tilgængelig fra internettet
- Tjek, at webhook'en er i aktiv status i panelet
- Gennemgå leveringsloggene for at se, om der er fejl
- 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
- Verificer, at du bruger den rå anmodnings-body (før JSON-parsing)
- Bekræft, at den hemmelige nøgle i din kode matcher den, der er konfigureret i Frihet-panelet
- Du må ikke ændre eller omformatere body'en, før du verificerer signaturen
- Tjek, at dit framework ikke automatisk parser body'en, før du kan få adgang til den rå body
Genforsøg modtages ikke
- Genforsøg behandles hvert 5. minut. Hvis dit endpoint var nede kortvarigt, er forsøgene muligvis allerede udtømt
- Gennemgå leveringshistorikken for at bekræfte status for hvert forsøg
- 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.