Een adres registreren
Registreer een adres in het developer dashboard of met de API. Het geheim (whsec_…) staat alleen in het antwoord; bewaar het veilig.
curl -X POST https://app.agreli.nl/api/public/v1/webhooks \
-H "Authorization: Bearer $AGRELI_API_KEY" \
-H "Idempotency-Key: 9a3e…" \
-H "Content-Type: application/json" \
-d '{ "url": "https://jouwapp.nl/webhooks/agreli", "description": "Productie", "events": ["onboarding.approval_required", "onboarding.completed"] }'- Alleen
https, naar een publiek bereikbaar adres en zonder inloggegevens in de URL. Vlak voor elke aflevering zoeken we de naam op en weigeren we interne adressen. - Laat
eventsweg of leeg voor alle gebeurtenissen; de API toont dat als["*"]. Een onbekende naam geeft422 events_invalid. - Hooguit 20 adressen per organisatie.
- Een adres dat je met een testsleutel registreert, hoort bij je sandbox en krijgt alleen gebeurtenissen uit de sandbox.
Gebeurtenissen
| Gebeurtenis | Ook verstuurd als | Wanneer | Extra in data |
|---|---|---|---|
onboarding. | — | Onboarding aangemaakt, via de API of het dashboard. | — |
onboarding. | — | Uitnodiging, nieuwe link of herinnering naar de medewerker verstuurd (niet bij de contractmail). | kind: invitation, new_link of reminder |
onboarding. | — | De medewerker is begonnen met invullen. | — |
onboarding. | onboarding. | De medewerker heeft de gegevens verstuurd. | — |
onboarding. | — | Er is een controle nodig: na het versturen, of opnieuw als gewijzigde voorwaarden goedkeuringen lieten vervallen. | — |
onboarding. | — | Teruggestuurd voor correctie. | returned_to |
onboarding. | — | Eén goedkeuring gegeven. | role: manager, payroll_reviewer of owner |
onboarding. | — | Alle vereiste goedkeuringen zijn binnen. | — |
onboarding. | — | Arbeidsvoorwaarden gewijzigd. | — |
contract. | — | Contract ter ondertekening verstuurd. | — |
contract. | — | De medewerker heeft getekend. | — |
onboarding. | contract. | De werkgever heeft ook getekend; het contract is rond. | — |
integration. | payroll. | De medewerker staat in de salarisadministratie. | connector |
integration. | — | Verwerking in de salarisadministratie lukt niet: geen koppeling, een fout of definitief mislukt (niet bij handmatige invoer). | connector, error_code |
onboarding. | — | Tegelijk met integration.. | connector, error_code |
onboarding. | — | Onboarding afgerond. | — |
onboarding. | — | Onboarding geannuleerd. | — |
ping | — | Testbericht via POST /; data bevat alleen message. | message |
Sommige gebeurtenissen worden ook onder een oudere naam verstuurd ("Ook verstuurd als"). Dat zijn aparte berichten met een eigen id. Abonneer je op één van de twee namen; zonder filter krijg je ze allebei.
Inhoud van een bericht
POST /webhooks/agreli HTTP/1.1
Content-Type: application/json
User-Agent: Agreli-Webhooks/1
Agreli-Event-Id: evt_…
Agreli-Event-Type: onboarding.employee_submitted
Agreli-Signature: t=1795176900,v1=5f2c…{
"id": "evt_…",
"type": "onboarding.employee_submitted",
"created_at": "2026-11-20T10:15:00.000Z",
"organization_id": "org_…",
"api_version": "2026-10-01",
"data": {
"onboarding_id": "onb_…",
"reference": "sollicitant-4711",
"metadata": { "vacature_id": "vac-311" },
"data_version": 2
}
}| Veld | Type | Omschrijving |
|---|---|---|
id | string | — |
object | "event" | Alleen in GET /v1/events. |
type | contract., contract., contract., integration., integration., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., onboarding., payroll., ping | — |
api_version | string | Bij webhooks die van het adres; bij GET /v1/events die van het verzoek. |
created_at | string (date-time) | — |
organization_id | string | De werkgever waar de onboarding bij hoort. |
data | object | — |
data. | string | — |
data. | string | null | — |
data. | object (sleutel → string | number | boolean) | Je eigen kenmerken, bijvoorbeeld een vacature-id (sleutels: letters, cijfers, _ . -, hooguit 40 tekens; tekst hooguit 500). Komt terug in de API en in webhooks. Geen persoonsgegevens: e-mail, IBAN, BSN en telefoonnummers worden geweigerd (metadata_personal_data). Hooguit 20 sleutels. |
data. | integer | null | — |
data. | string | Bij onboarding.approval_given. |
data. | string | Bij onboarding.returned. |
data. | invitation, new_link, reminder | Bij onboarding.invitation_sent. |
data. | string | null | Bij integration.*: nmbrs, afas, loket, manual, … |
data. | string | null | Bij integration.sync_failed en onboarding.failed. |
metadata is wat jij bij het aanmaken meegaf. Zet er nooit persoonsgegevens in: waarden die op een e-mailadres, IBAN, BSN of Nederlands mobiel nummer lijken, weigert de API met 422 metadata_personal_data. Die herkenning is een vangnet, geen garantie.
De handtekening controleren
De kop Agreli-Signature heeft de vorm t=<unix>,v1=<hex>. v1 is HMAC-SHA256 met je geheim over "<t>., waarbij body de ruwe tekst van het verzoek is. Controleer de handtekening voordat je iets met het bericht doet, en weiger berichten ouder dan 5 minuten.
Tijdens het vernieuwen van het geheim staan er twee v1-waarden in de kop: t=…,v1=<nieuw>,v1=<vorig>. Accepteer het bericht als er één klopt.
import { createHmac, timingSafeEqual } from "node:crypto";
/** Controleert Agreli-Signature (t=<unix>,v1=<hex>[,v1=<hex>]) over "<t>.<ruwe body>". */
export function geldigeHandtekening(kop: string | null, rawBody: string, secret: string, nu = Date.now()): boolean {
const delen = (kop ?? "").split(",").map((d) => d.trim().split("="));
const t = delen.find(([k]) => k === "t")?.[1];
const handtekeningen = delen.filter(([k, v]) => k === "v1" && /^[0-9a-f]{64}$/.test(v ?? "")).map(([, v]) => Buffer.from(v!, "hex"));
if (!t || !/^\d+$/.test(t) || !handtekeningen.length) return false;
if (Math.abs(nu / 1000 - Number(t)) > 300) return false; // ouder dan 5 minuten
const verwacht = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return handtekeningen.some((h) => timingSafeEqual(h, verwacht));
}Gebruik de ruwe body, niet een opnieuw geserialiseerd object: een andere volgorde of spatiëring geeft een andere handtekening. In een Next.js-route ziet dat er zo uit:
import { geldigeHandtekening } from "@/lib/agreli-webhook";
export async function POST(request: Request) {
const rawBody = await request.text();
if (!geldigeHandtekening(request.headers.get("agreli-signature"), rawBody, process.env.AGRELI_WEBHOOK_SECRET!)) {
return new Response("Ongeldige handtekening", { status: 400 });
}
const event = JSON.parse(rawBody) as { id: string; type: string; organization_id: string; data: { onboarding_id: string; reference: string | null } };
// Jouw eigen opslag: sla elk event-id één keer op en verwerk het daarna in de achtergrond.
if (await alOntvangen(event.id)) return new Response(null, { status: 200 });
await zetInWachtrij(event);
return new Response(null, { status: 200 });
}Afleveren en opnieuw proberen
- Elke
2xxtelt als ontvangen. We volgen geen redirects en wachten hooguit 10 seconden op een antwoord. Antwoord dus snel en verwerk daarna. - Lukt het niet, dan proberen we het opnieuw na 1, 5, 30, 120, 360, 720 en 1440 minuten. Daarna is de aflevering definitief mislukt.
- Na 10 definitief mislukte afleveringen achter elkaar zetten we het adres uit. In het developer dashboard zie je de laatste afleveringen en zet je het adres weer aan.
- Elk bericht heeft
api_version: de versie van het adres, vastgezet bij het aanmaken. - Een bericht kan meer dan eens aankomen. Ontdubbel op
id(ook in de kopAgreli-Event-Id). - Reken niet op de volgorde. Gebruik
data_versionen haal bij twijfel de actuele stand op metGET /.v1/ onboardings/ {id}
Het geheim vernieuwen
POST / maakt een nieuw geheim. Het staat alleen in het antwoord. Het nieuwe geheim ondertekent direct; het vorige ondertekent nog overlap_hours mee (standaard 24, hooguit 168). Zet het nieuwe geheim in je configuratie, en accepteer in de tussentijd beide. previous_secret_expires_at zegt tot wanneer het vorige meetelt. Met "overlap_hours": 0 is het vorige direct ongeldig, bijvoorbeeld als het gelekt is. In het developer dashboard heet dit Nieuw geheim.
curl -X POST https://app.agreli.nl/api/public/v1/webhooks/wh_…/rotate-secret \
-H "Authorization: Bearer $AGRELI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "overlap_hours": 24 }'Afleverlog en opnieuw versturen
GET /geeft alle afleveringen van een adres, nieuwste eerst, met status, pogingen, de laatste HTTP-status en de fout. Filter metv1/ webhooks/ {id}/ deliveries statusen blader metlimitenstarting_after.POST /verstuurt een aflevering opnieuw, ook een geslaagde of definitief mislukte. Hetv1/ webhooks/ {id}/ deliveries/ {deliveryId}/ retry idvan het bericht blijft gelijk, dus je ontdubbeling vangt een herhaling op. In het developer dashboard is dit de knop Opnieuw.
Gemiste gebeurtenissen nalopen
Lag je server plat, of wil je zeker weten dat je niets mist? GET / geeft dezelfde gebeurtenissen als de webhooks, met hetzelfde id en dezelfde inhoud. Dat werkt ook als je geen webhookadres hebt. De oudste staan bovenaan, en we bewaren ze 30 dagen.
curl "https://app.agreli.nl/api/public/v1/events?starting_after=evt_…&limit=100" \
-H "Authorization: Bearer $AGRELI_API_KEY"Bewaar het id van de laatste gebeurtenis die je verwerkte en vraag daarna op met starting_after. Zonder cursor kun je beginnen bij een tijdstip met since. Filter op één soort met type. Daarvoor is het recht onboardings:read nodig. Een softwarepartner ziet alleen de gebeurtenissen van onboardings die via zijn eigen sleutels liepen.
Testen
POST / stuurt een ping naar één adres, langs dezelfde weg als echte berichten. In de sandbox speel je met POST / de hele onboarding na; bij elke stap komen de echte gebeurtenissen binnen.
Webhooks voor softwarepartners
Een API-partner krijgt de gebeurtenissen van een onboarding als die via een sleutel van de partner is aangemaakt, de werkgever de partner (nog) toelaat en de partner het recht onboardings:read heeft. De berichten gaan naar de adressen in de eigen organisatie van de partner; organization_id zegt van welke werkgever ze zijn. Beëindigt de werkgever de relatie, dan stoppen nieuwe berichten direct. Onboardings die de werkgever zelf startte, ziet de partner nooit.