Wanneer gaat een webhook af
Een webhook gaat af op het moment dat de stap Webhook in een automatisering aan de beurt is: bij een schema, een handmatige start, een nieuwe rij of een drempel, afhankelijk van hoe je de automatisering hebt ingericht. APIboard doet een POST of PUT naar het adres dat je in de stap hebt ingesteld, met een JSON-bericht in de body.
Staat de stap buiten een Voor elke rij, dan krijg je één bericht met alle rijen van dat moment (maximaal 100). Staat de stap binnen een Voor elke rij, dan krijg je één bericht per rij, met alleen die rij.
Het bericht (payload versie 1)
{
"id": "dlv_0b1f6c0e-5d0e-4e0a-9c53-2f6f0f3a7a11",
"type": "automation.run",
"api_versie": 1,
"created_at": "2026-10-23T14:05:00Z",
"test": false,
"organisatie": { "id": "…", "naam": "Voorbeeld BV" },
"automatisering": { "id": "…", "naam": "Grote facturen doorsturen", "versie_id": "…" },
"run": { "id": "…", "gestart_op": "2026-10-23T14:05:00Z", "gestart_door": "data" },
"trigger": {
"soort": "new_row",
"tabel": "facturen",
"koppelingen": [{ "id": "…", "naam": "Boekhouding", "connector": "moneybird" }]
},
"rijen": [{ "factuurnummer": "2026-0042", "bedrag": 12100.5 }],
"rijen_afgekapt": false
}
Velden die niet van toepassing zijn ontbreken, ze staan dus nooit als null in het bericht.
| Veld | Betekenis |
|---|---|
id | Het leveringsnummer, begint met dlv_. Blijft gelijk bij elke nieuwe poging van dezelfde levering. |
type | Altijd automation.run. |
api_versie | Versie van de opbouw van dit bericht, nu 1. |
created_at | Tijdstip waarop de levering is aangemaakt (UTC). Blijft gelijk bij nieuwe pogingen. |
test | true bij Test versturen, anders false. |
organisatie | id en naam van je organisatie. |
automatisering | id, naam en versie_id van de gepubliceerde versie die de run uitvoert. |
run | id en gestart_op van de run, en gestart_door: schema, handmatig, automatisering of data. |
trigger | Wat de run startte: soort (bijvoorbeeld new_row, change, threshold of schedule), tabel, koppelingen (lijst met id, naam en connector), en bij een drempel waarde (opgemaakte tekst) en waarde_getal (het getal zelf). |
rijen | Buiten Voor elke rij: de rijen op het moment van de stap, maximaal 100. |
rijen_afgekapt | true als er rijen zijn weggelaten omdat het bericht anders boven 100 rijen of 256 KB uitkomt. Er wordt van achteren afgekapt. |
rij | Binnen Voor elke rij: de huidige rij. rijen ontbreekt dan. |
rij_index | Binnen Voor elke rij: het volgnummer van de rij. |
Headers
Elk verzoek bevat deze headers:
| Header | Inhoud |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | APIboard-Webhooks/1 |
X-APIboard-Delivery | Het leveringsnummer (dlv_…), gelijk aan id in het bericht. |
X-APIboard-Signature | De handtekening, zie hieronder. |
Eigen headers die je in de stap instelt komen daar bovenop, bijvoorbeeld Authorization. Namen die APIboard zelf gebruikt (zoals Content-Type, Host en alles dat met X-APIboard- begint) kun je niet instellen.
De handtekening controleren
Controleer bij elk bericht de handtekening voordat je het verwerkt. Zo weet je dat het bericht van APIboard komt en onderweg niet is aangepast. De header heeft deze vorm:
X-APIboard-Signature: t=1700000000,v1=38877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789
tis het tijdstip van versturen in Unix-seconden.v1is de HMAC-SHA256 in hexadecimale vorm over de tekst<t>.<rauwe body>, met je webhookgeheim als sleutel.- Gebruik het geheim precies zoals het in APIboard staat, inclusief het voorvoegsel
whsec_, als UTF-8-tekst. - Reken over de rauwe body zoals je die ontvangt, niet over een opnieuw geserialiseerde versie. Zodra je de JSON eerst parset en weer omzet, kan de handtekening afwijken.
- Vergelijk met een constant-time functie, niet met
==. - Weiger berichten waarvan
tmeer dan 5 minuten van je eigen klok afwijkt. Dat beschermt tegen het opnieuw afspelen van een onderschept bericht.
Je vindt het geheim in de stap Webhook (Tonen) en kunt het daar ook vernieuwen. Een testvector om je code mee te controleren: met geheim whsec_test, t gelijk aan 1700000000 en body {"a":1} is de handtekening t=1700000000,v1=38877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789.
import crypto from "node:crypto";
// rauweBody: de ongewijzigde body als string of Buffer
export function controleerHandtekening(geheim, rauweBody, header, nu = Date.now()) {
const delen = Object.fromEntries(header.split(",").map((d) => d.split("=")));
const t = Number(delen.t);
if (!Number.isFinite(t) || Math.abs(nu / 1000 - t) > 300) return false;
const verwacht = crypto
.createHmac("sha256", geheim)
.update(t + "." + rauweBody)
.digest();
const ontvangen = Buffer.from(delen.v1 || "", "hex");
return ontvangen.length === verwacht.length && crypto.timingSafeEqual(ontvangen, verwacht);
} import hashlib
import hmac
import time
def controleer_handtekening(geheim: str, ruwe_body: bytes, header: str, nu: float | None = None) -> bool:
# ruwe_body: de ongewijzigde body als bytes
delen = dict(d.split("=", 1) for d in header.split(","))
try:
t = int(delen["t"])
except (KeyError, ValueError):
return False
if abs((nu if nu is not None else time.time()) - t) > 300:
return False
verwacht = hmac.new(
geheim.encode("utf-8"), str(t).encode() + b"." + ruwe_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(verwacht, delen.get("v1", "")) <?php
// $rauweBody: de ongewijzigde body, bijvoorbeeld file_get_contents('php://input')
function controleerHandtekening(string $geheim, string $rauweBody, string $header, ?int $nu = null): bool
{
$delen = [];
foreach (explode(',', $header) as $deel) {
[$sleutel, $waarde] = array_pad(explode('=', $deel, 2), 2, '');
$delen[$sleutel] = $waarde;
}
if (!isset($delen['t']) || !ctype_digit($delen['t'])) {
return false;
}
$t = (int) $delen['t'];
if (abs(($nu ?? time()) - $t) > 300) {
return false;
}
$verwacht = hash_hmac('sha256', $t . '.' . $rauweBody, $geheim);
return hash_equals($verwacht, $delen['v1'] ?? '');
} Dubbele leveringen herkennen
Een levering kan meer dan één keer bij je aankomen: bijvoorbeeld als jouw systeem wel verwerkte maar de bevestiging te laat terugkwam, of als APIboard na een storing opnieuw verstuurt. Elke nieuwe poging van dezelfde levering heeft dezelfde X-APIboard-Delivery-header en hetzelfde id in het bericht. De inhoud kan bij een nieuwe poging licht afwijken (bijvoorbeeld een gewijzigde organisatienaam). Bewaar de leveringsnummers die je hebt verwerkt en sla een nummer dat je al kent over, met een 2xx-antwoord.
Wat telt als gelukt en als fout
Antwoord met een statuscode van 200 tot en met 299 zodra je het bericht hebt ontvangen. Doe zwaar werk liever daarna, want APIboard wacht maximaal 10 seconden op je antwoord.
| Uitkomst | Behandeling |
|---|---|
| 2xx | Gelukt. |
| 5xx, 408 en 429, time-out, geen verbinding of adres niet gevonden | Tijdelijke fout: APIboard probeert het opnieuw. |
| Andere 4xx, doorverwijzing (3xx), adres niet toegestaan of ongeldig adres | Blijvende fout: geen nieuwe poging. |
Opnieuw proberen
Buiten een Voor elke rij probeert APIboard een levering maximaal 5 keer. Tussen de pogingen zit steeds meer tijd:
| Poging | Wanneer |
|---|---|
| 1 | Direct |
| 2 | 1 minuut na poging 1 |
| 3 | 5 minuten na poging 2 |
| 4 | 30 minuten na poging 3 |
| 5 | 2 uur na poging 4 |
Stuurt je systeem een Retry-After-header (in seconden of als HTTP-datum) die langer is dan het schema, dan wacht APIboard zo lang, tot maximaal 2 uur. In de runs-lijst van de automatisering zie je zolang het loopt bijvoorbeeld “Webhook opnieuw om 14:05 (poging 2 van 5)”. Mislukt ook poging 5, dan krijgt de stap de status fout, de run staat op “Met fouten” en de gebruikelijke foutmail gaat uit.
Binnen een Voor elke rij wacht APIboard niet: daar zijn het maximaal 3 pogingen direct na elkaar, met 1 en 4 seconden ertussen. Mislukt een rij na 3 pogingen, dan is de stap voor die rij mislukt en gaat de run verder met de volgende rij. Zo houdt één rij die steeds faalt niet de hele run uren op.
Eisen aan het adres
- Het adres begint met
https://en bevat geen gebruikersnaam of wachtwoord. Maximaal 2048 tekens. - De naam moet naar een openbaar adres wijzen. Adressen binnen een bedrijfsnetwerk of op dezelfde machine (zoals
localhost,127.0.0.1of169.254.169.254) worden geweigerd. Wijst een naam naar zowel een openbaar als een intern adres, dan wordt het ook geweigerd. - Doorverwijzingen worden niet gevolgd. Geef het uiteindelijke adres op.
- Je systeem moet binnen 10 seconden antwoorden.
- Een bericht is maximaal 256 KB groot. Bij meerdere rijen worden rijen weggelaten tot het past; past al één rij (in een Voor elke rij) niet binnen 256 KB, dan mislukt de stap voor die rij en wordt er niets verstuurd.
- Je kunt maximaal 10 eigen headers instellen; de naam mag maximaal 64 tekens zijn. Een headerwaarde mag alleen Latin-1-tekens bevatten (gewone letters, cijfers en leestekens en tekens als é en ü, maar geen emoji, euroteken of regeleinden) en is maximaal 4096 tekens lang.
Bij een fout bewaart APIboard maximaal 500 tekens van je antwoord, om te kunnen laten zien wat er misging. Headerwaarden, het geheim en het pad van je adres worden daarin vervangen door •••. Het volledige adres en de headerwaarden staan nooit in de automatisering, de runs of de logboeken, alleen een weergave als https://voorbeeld.nl/….
Test versturen
In de stap Webhook kun je met Test versturen één voorbeeldbericht sturen naar je eigen adres. Het bericht heeft "test": true en een voorbeeldrij, wordt ondertekend zoals een echte levering en wordt één keer geprobeerd. Je ziet direct de statuscode, de duur en bij een fout het gemaskeerde begin van het antwoord. Sla eerst je wijzigingen op: de test gebruikt wat is opgeslagen. Per organisatie kun je maximaal 50 keer per dag testen. Kijk in je ontvanger naar test om testberichten niet als echte gegevens te verwerken.
Het geheim vernieuwen
Het webhookgeheim vernieuw je in de stap Webhook. Het nieuwe geheim gaat pas gelden zodra je de automatisering publiceert: tot dan ondertekent APIboard nog met het oude geheim. Pogingen van runs die al wachten om het opnieuw te proberen gebruiken het geheim van de versie waarmee die run is gestart, dus ook na het vernieuwen kan een wachtende levering nog met het oude geheim aankomen. Laat je ontvanger daarom kort beide geheimen accepteren, tot er niets meer wacht.
Het bewerken van het adres, de headers en het geheim kan alleen een Admin of Member.
Was dit artikel behulpzaam?