Met uitgaande webhooks stuur je FixControl-events naar je eigen systemen: een notifier, een statuspagina, een intern dashboard. Jij kiest de events en geeft een URL op; FixControl ondertekent elke levering en probeert het een paar keer opnieuw als je endpoint hem niet accepteert.
Voordat je begint
- Rol — je hebt de rol admin of manager in de workspace nodig. Andere rollen kunnen webhooks niet openen of wijzigen.
- Een endpoint — een URL die
POST-requests met een JSON-body accepteert. Gebruik HTTPS in productie. De URL moet vanaf internet bereikbaar zijn: FixControl levert niet af op adressen in privé-netwerkbereiken, en volgt geen redirects.
Een webhook aanmaken
- Open Instellingen → Kanalen. Open in de sectie Webhooks & API de kaart Uitgaande webhooks (of ga direct naar /settings/integrations/webhooks).
- Klik Webhook toevoegen.
- Vul in:
- Naam — voor je eigen administratie, bijvoorbeeld het systeem dat hem ontvangt. - Endpoint-URL — https://.... - Events — alle events staan aangevinkt als het formulier opent. Vink uit wat je niet nodig hebt. Je moet er minstens één houden: zonder event kan het formulier niet worden opgeslagen ("Kies minstens één event-type").
- Klik Webhook aanmaken.
- Het signing-secret (begint met
ws_) wordt één keer getoond. Kopieer het naar je secret store voordat je op Ik heb hem opgeslagen klikt. FixControl kan het daarna niet meer tonen.
Daarna staat de webhook in de lijst met zijn naam, URL, events (all of een aantal zoals 3/7), de eerste tekens van het secret en zijn status in de kolom Actief.
Events
Elke levering heeft een event-veld. Dit zijn alle events die er nu zijn:
| Event | Wordt verstuurd wanneer |
|---|---|
issue.created | Er een nieuwe issue is aangemaakt (intake, handmatig of via de API). |
issue.updated | Een issue wordt bijgewerkt. Dit gebeurt bij elke wijziging, ook bij een aangepaste titel of tekst, niet alleen bij een statuswissel. |
patch.ready | Een patch is goedgekeurd en klaar om toe te passen. |
patch.approved | Een goedgekeurde patch is toegepast; de wijziging is doorgevoerd. |
patch.failed | Een patch is afgewezen, of er zijn wijzigingen gevraagd. |
pr.created | Er is een pull of merge request geopend voor een patch. |
integration.error | Het openen van een pull of merge request voor een patch is mislukt (bijvoorbeeld een authenticatieprobleem bij de Git-provider). |
Omdat issue.updated bij elke wijziging wordt verstuurd, kan een endpoint dat erop geabonneerd is in korte tijd meerdere leveringen voor één issue krijgen. Abonneer je alleen op wat je nodig hebt.
Vorm van de payload
Elke levering heeft dezelfde envelope:
{
"event": "patch.approved",
"deliveryId": "whd_lzx3k9q2_8f3k2j1a",
"timestamp": 1746267262,
"data": {
"patchId": "ptch_01HV...",
"workspaceId": "ws_01HU...",
"version": 2,
"status": "applied",
"previousStatus": "approved",
"decidedBy": "usr_...",
"issueKey": "BIZ-29"
}
}event— de naam van het event.deliveryId— een unieke id per levering, in de vormwhd_<tijd>_<willekeurig>. Nieuwe pogingen voor dezelfde levering houden dezelfde id, dus gebruik hem om duplicaten te negeren.timestamp— Unix-seconden op het moment dat deze poging werd verstuurd.data— de eigen velden van het event. Bij issue-events zijn dat de issue-key (zoalsBIZ-29), het type, de titel, de prioriteit en de status; bij patch-events de patch-id, versie, status en issue-key; bijpr.createdde URL, het nummer en de branch van de pull request. De precieze velden verschillen per event, dus lees de velden die je nodig hebt en negeer wat je niet herkent.
De signature verifiëren
Elke levering wordt ondertekend met HMAC-SHA256 en het secret van je webhook. Elk request heeft deze headers:
X-FixControl-Signature: t=<unix-seconden>,v1=<hex>— de signature.X-FixControl-Event— de naam van het event.X-FixControl-Delivery— dezelfde waarde alsdeliveryId.X-FixControl-Webhook— de id van de webhook waar deze levering bij hoort (geen secret).
De signature is de hex-gecodeerde HMAC-SHA256 van de string <t>.<ruwe body>: de t-waarde uit de header, een punt, en dan de request-body precies zoals ontvangen.
Zo verifieer je:
- Lees de ruwe request-body als bytes. Parse en serialiseer de JSON niet eerst opnieuw; dan veranderen de bytes.
- Splits de header op
,en lees de waardentenv1. - Wijs het request af als
tmeer dan 5 minuten van je huidige tijd afwijkt. - Bereken
HMAC-SHA256(secret, "<t>.<ruwe body>")als hex. - Vergelijk die met
v1met een constant-time vergelijking.
Voorbeeld (Node.js, Express):
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const SECRET = process.env.FIXCONTROL_WEBHOOK_SECRET; // ws_...
function verify(rawBody, header, secret) {
if (!header) return false;
let t = null;
let v1 = null;
for (const part of header.split(",")) {
const [k, v] = part.trim().split("=", 2);
if (k === "t") t = Number(v);
if (k === "v1") v1 = v;
}
if (!Number.isFinite(t) || !v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody.toString("utf8")}`)
.digest("hex");
if (expected.length !== v1.length) return false;
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
}
app.post("/fixcontrol-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.get("X-FixControl-Signature"), SECRET)) {
return res.status(401).end();
}
const delivery = JSON.parse(req.body.toString("utf8"));
// Ontdubbel op delivery.deliveryId en verwerk daarna delivery.event.
res.status(204).end();
});Voorbeeld (Python):
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1 or not t.isdigit():
return False
if abs(int(time.time()) - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Wijs elk request af waarvan de verificatie mislukt. Vertrouw een webhook nooit alleen op basis van de URL; een URL is geen secret.
Nieuwe pogingen
FixControl telt een levering als geaccepteerd als je endpoint binnen 10 seconden met een 2xx-status antwoordt. Al het andere (een andere status, een redirect, een time-out, een verbindingsfout) is een mislukte poging. Een levering krijgt in totaal 3 pogingen:
poging 1 direct
poging 2 ongeveer 1 seconde later
poging 3 ongeveer 5 seconden laterZolang er pogingen over zijn, staat de levering op Opnieuw proberen. Na de derde mislukte poging staat hij op Opgegeven en wordt hij niet meer geprobeerd. Er is geen knop om een levering opnieuw af te spelen: heb je een levering gemist, lees dan de actuele stand via de publieke API, of verstuur een test om je endpoint te controleren.
Leveringen controleren
Elke rij in de lijst met webhooks heeft deze acties:
- Test versturen — stuurt een
webhook.test-levering naar je endpoint, ondertekend zoals een echte, en opent het leveringslog. Dedatavan een testlevering bevat"kind": "test". Een uitgeschakelde webhook verstuurt geen tests; schakel hem eerst in. - Leveringen bekijken — opent Recente leveringen voor die webhook onder de lijst: event, status (In wachtrij, Opnieuw proberen, Geslaagd, Opgegeven), responscode, aantal pogingen, de fout, de tijd en de request-ID (dezelfde waarde als
deliveryId). Met de verversknop werk je het bij. - Bewerken — wijzig de naam, URL of events. Het secret blijft hetzelfde.
- Verwijderen — verwijdert de webhook na een bevestiging.
Pauzeren, verwijderen en het secret vervangen
- Pauzeren zonder verwijderen — klik Uitschakelen in de kolom Actief. Een uitgeschakelde webhook krijgt geen events meer tot je weer op Inschakelen klikt.
- Verwijderen — verwijdert de webhook definitief. Leveringen die nog opnieuw geprobeerd werden, eindigen als Opgegeven met de fout
webhook_deleted. Het leveringslog van die webhook wordt niet meer in de app getoond. - Nieuw secret — een secret kun je niet roteren of opnieuw tonen. Is het gelekt of kwijt, maak dan een nieuwe webhook aan met dezelfde URL en events, zet je endpoint over op het nieuwe secret en verwijder daarna de oude webhook.
Als het niet werkt
| Wat je ziet | Wat je doet |
|---|---|
| Webhook aanmaken blijft grijs | Vul een naam en URL in en houd minstens één event aangevinkt. |
| "Kan webhook niet opslaan." | Controleer of de URL een volledig http://- of https://-adres is. |
Leveringen staan op Opgegeven met http_4xx of http_5xx | Je endpoint weigerde of crashte. Kijk in de logs; een 401 betekent meestal dat de signature-controle mislukte (verkeerd secret, of de body werd geparst vóór het verifiëren). |
Fout begint met redirect_blocked_ | Je endpoint antwoordde met een redirect. Gebruik direct de uiteindelijke URL. |
Fout begint met url_blocked_ | De URL verwijst naar een privé- of intern adres. Gebruik een adres dat vanaf internet bereikbaar is. |
| "Test-levering kon niet worden ingepland." | De webhook staat mogelijk uit. Schakel hem in en probeer het opnieuw. |
Zie ook
- Publieke API — beheer webhooks vanuit je eigen tools en lees issues en patches.
- Admin-handleiding voor webhooks — inkomende webhooks van Slack, GitHub en andere integraties.
Veelgestelde vragen
Mijn endpoint zit achter authenticatie. Hoe laat ik FixControl door? FixControl stuurt geen eigen authenticatieheaders (Basic, bearer) mee. Verifieer in plaats daarvan de HMAC-signature. Moet het request een privé-netwerk bereiken, zet er dan een publieke proxy voor die de signature controleert en het request naar binnen doorstuurt.
Kan één webhook leveringen voor meerdere workspaces krijgen? Nee. Een webhook ontvangt alleen events van de workspace waarin hij is aangemaakt.
In welk formaat is de signature? Hex-gecodeerde HMAC-SHA256 over <timestamp>.<ruwe request-body>, verstuurd als t=<unix-seconden>,v1=<hex> in één header. Dat is dezelfde vorm die Stripe gebruikt, dus een verifier in Stripe-stijl kun je direct overnemen.