Webhooks

Bijgewerkt op 26 oktober 2026 · Gepubliceerd op 26 oktober 2026

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.

VeldBetekenis
idHet leveringsnummer, begint met dlv_. Blijft gelijk bij elke nieuwe poging van dezelfde levering.
typeAltijd automation.run.
api_versieVersie van de opbouw van dit bericht, nu 1.
created_atTijdstip waarop de levering is aangemaakt (UTC). Blijft gelijk bij nieuwe pogingen.
testtrue bij Test versturen, anders false.
organisatieid en naam van je organisatie.
automatiseringid, naam en versie_id van de gepubliceerde versie die de run uitvoert.
runid en gestart_op van de run, en gestart_door: schema, handmatig, automatisering of data.
triggerWat 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).
rijenBuiten Voor elke rij: de rijen op het moment van de stap, maximaal 100.
rijen_afgekapttrue als er rijen zijn weggelaten omdat het bericht anders boven 100 rijen of 256 KB uitkomt. Er wordt van achteren afgekapt.
rijBinnen Voor elke rij: de huidige rij. rijen ontbreekt dan.
rij_indexBinnen Voor elke rij: het volgnummer van de rij.

Headers

Elk verzoek bevat deze headers:

HeaderInhoud
Content-Typeapplication/json; charset=utf-8
User-AgentAPIboard-Webhooks/1
X-APIboard-DeliveryHet leveringsnummer (dlv_…), gelijk aan id in het bericht.
X-APIboard-SignatureDe 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
  • t is het tijdstip van versturen in Unix-seconden.
  • v1 is 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 t meer 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.

UitkomstBehandeling
2xxGelukt.
5xx, 408 en 429, time-out, geen verbinding of adres niet gevondenTijdelijke fout: APIboard probeert het opnieuw.
Andere 4xx, doorverwijzing (3xx), adres niet toegestaan of ongeldig adresBlijvende 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:

PogingWanneer
1Direct
21 minuut na poging 1
35 minuten na poging 2
430 minuten na poging 3
52 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.1 of 169.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?