# Agreli API: full documentation > Agreli is a Dutch SaaS for onboarding new employees in hospitality and retail. The new hire fills in their details on their own phone, two different people check them (four-eyes principle), the contract is signed digitally and the employee is created in the payroll system. The Hosted Onboarding API lets other software (an ATS, a scheduling tool, an n8n workflow) start an onboarding, send the employee to a hosted page and follow progress through signed webhooks. This file is the complete API documentation in one Markdown document, written for AI assistants and developers. The product, its UI and the API error messages are Dutch. The endpoint reference and the tables in this file are generated from the OpenAPI spec and keep its Dutch descriptions; paths, field names, codes and JSON are language-neutral. - Base URL: `https://app.agreli.nl/api/public` (every path starts with `/v1`) - OpenAPI 3.1 spec (source of truth): https://app.agreli.nl/api/public/v1/openapi.json - Human-readable docs (Dutch): https://agreli.nl/docs, also as Markdown: https://agreli.nl/docs.md, https://agreli.nl/docs/snelstart.md, https://agreli.nl/docs/authenticatie.md, https://agreli.nl/docs/webhooks.md, https://agreli.nl/docs/sandbox.md, https://agreli.nl/docs/fouten.md, https://agreli.nl/docs/api.md - Index: https://agreli.nl/llms.txt ## Concepts ### Hosted Onboarding: Create → Redirect → Verify → Webhook 1. **Create.** `POST /v1/onboardings` with the employee and the employment terms. The response has `id` (`onb_…`), `status: "pending_employee"`, `hosted_url` and `expires_at`. 2. **Redirect.** Send the employee to `hosted_url` (or set `options.send_invitation: true` and Agreli emails the link). The page uses the employer's branding. 3. **Verify.** The employee confirms with a one-time code sent by email or SMS. The link alone never gives access to a file. 4. **Webhook.** Agreli posts signed events as the onboarding moves on: submitted, approved, contract signed, processed in payroll, completed. Fetch details with `GET /v1/onboardings/{id}`. In between, Agreli handles the forms, the ID document scan, the four-eyes review, the contract (from a template the employer approved) and the payroll integration (Nmbrs first; manual processing is always possible). The public API calls the same backend services as the dashboard, so every rule that applies in the dashboard also applies through the API. Conventions: JSON with snake_case fields; ids with a prefix (`onb_` onboarding, `org_` organization, `loc_` location, `emp_` employee, `doc_` document, `wh_` webhook endpoint, `evt_` event, `req_` request); amounts in cents (`hourly_wage_cents`); dates as `YYYY-MM-DD`, timestamps as ISO 8601 in UTC. The link (`hosted_url`) is valid for 336 hours (14 days) by default; choose 1 to 336 hours with `options.expires_in_hours`. `POST /v1/onboardings/{id}/hosted-link` issues a new link and invalidates the previous one. After submitting, the status page shows a button back to `options.return_url`: https only, on one of the allowed domains (see "Embedding and return_url"). ### Statuses `status` tells where an onboarding is; `waiting_on` tells who has to act next. | Status | Betekenis | Aan zet (`waiting_on`) | | --- | --- | --- | | `pending_employee` | Uitgenodigd of bezig met invullen. | `employee` | | `in_review` | Gegevens verstuurd; de controle (vierogen) loopt. | de rollen die nog moeten goedkeuren, of `manager` na terugsturen | | `contract_ready` | Goedgekeurd; het contract kan worden gemaakt en verstuurd. | `manager` | | `awaiting_employee_signature` | Contract verstuurd; wacht op de handtekening van de medewerker. | `employee` | | `awaiting_employer_signature` | De medewerker heeft getekend; wacht op de werkgever. | `manager` | | `processing` | Getekend; de verwerking in de salarisadministratie loopt. | `system` | | `action_required` | De verwerking in de salarisadministratie vraagt om een handeling. | `manager`, `payroll_reviewer` | | `completed` | Afgerond. | niemand | | `cancelled` | Geannuleerd. | niemand | ### The onboarding object Returned by `GET /v1/onboardings/{id}` and by the write actions on one onboarding. Lists leave out `steps`, `documents`, `email`, `phone` and the employment terms. For a single onboarding, `email`, `phone` and the terms are only present when the key may open the file. ```json { "id": "onb_…", "object": "onboarding", "status": "in_review", "waiting_on": ["payroll_reviewer"], "returned": false, "employee": { "id": "emp_…", "first_name": "Sam", "last_name": "Jansen", "email": "sam@example.com", "phone": null }, "employment": { "position": "Bediening", "location_id": "loc_…", "start_date": "2026-12-01", "contract_type": "on_call", "hours_per_week": 16, "hourly_wage_cents": 1450 }, "workflow": { "template_id": "t1", "version": 1 }, "data_version": 2, "approvals": { "required": ["manager", "payroll_reviewer"], "given": [{ "role": "manager", "valid": true, "data_version": 2, "at": "2026-11-20T10:15:00.000Z" }] }, "contract": { "status": "none" }, "payroll": null, "hosted_link": { "status": "activated", "expires_at": "2026-10-26T12:00:00.000Z", "activated": true }, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-11-20T10:15:00.000Z", "steps": [{ "id": "…", "phase": "gegevens", "kind": "document", "title": "…", "applies": true, "done": true }], "documents": [{ "id": "doc_…", "kind": "passport", "title": null, "uploaded_at": "2026-11-20T09:58:00.000Z", "downloadable": false }] } ``` - `contract.status`: `none`, `sent`, `signed_by_employee` or `signed`. - `hosted_link.status`: `open`, `activated` or `revoked`. - `payroll`: `null` until processing starts, then `status`, `external_id` and `error_code`. - `data_version` increases when the data or the terms change. An approval is valid for one version. This object is richer than the `Onboarding` schema in the OpenAPI spec; the fields above come from the API implementation. ## Authentication Every request uses a personal API key as a Bearer token: ```http Authorization: Bearer hrs_live_… ``` - `hrs_live_…` works in the real organization; `hrs_test_…` works in the sandbox (fictitious employees, captured emails and SMS, simulated payroll). - A key acts as the user who created it, within one organization and with the chosen scopes. What that user may not do, the key may not do either. Roles are read from the user's memberships on every request: a user removed from the organization has a key that stops working immediately. - The key is shown once; Agreli stores only a SHA-256 hash. Optional expiry: 1 to 730 days. - Only an owner or manager can create test keys and keys with `onboardings:write`, `contracts:write` or `webhooks:manage`. - There are no cookies: only the Bearer key counts. Call the API from your server, never from a browser. - Keys are created, rotated and revoked in the app under Instellingen → API en webhooks (the developer dashboard). Every response carries `X-Request-Id`, `Agreli-Mode` (`live` or `test`) and the rate limit headers. ### Scopes | Recht | Wat het toestaat | Endpoints | | --- | --- | --- | | `approvals:read` | Openstaande controles lezen | `GET /v1/approvals` | | `approvals:write` | Goedkeuren en de ID-controle bevestigen, als jijzelf | `POST /v1/onboardings/{id}/identity-verification`, `POST /v1/onboardings/{id}/approvals` | | `contracts:write` | Contract maken en ter ondertekening versturen | `POST /v1/onboardings/{id}/contract` | | `documents:read` | Documenten en toegestane downloads | `GET /v1/onboardings/{id}/documents`, `GET /v1/onboardings/{id}/documents/{documentId}` | | `employees:read` | Medewerkers en dienstverbanden lezen | `GET /v1/employees` | | `integrations:read` | Koppelingen en synchronisatiestatus lezen | `GET /v1/integrations` | | `onboardings:read` | Onboardings en hun status lezen | `GET /v1/onboardings`, `GET /v1/onboardings/{id}`, `GET /v1/events` | | `onboardings:write` | Onboardings starten, links maken en annuleren | `POST /v1/onboardings`, `PATCH /v1/onboardings/{id}`, `POST /v1/onboardings/{id}/hosted-link`, `POST /v1/onboardings/{id}/cancel`, `POST /v1/sandbox/onboardings/{id}/advance` | | `organization:read` | Organisatie en vestigingen lezen | `GET /v1/organizations`, `GET /v1/organization` | | `webhooks:manage` | Webhooks beheren | `GET /v1/webhooks`, `POST /v1/webhooks`, `GET /v1/webhooks/{id}`, `DELETE /v1/webhooks/{id}`, `POST /v1/webhooks/{id}/test`, `POST /v1/webhooks/{id}/rotate-secret`, `GET /v1/webhooks/{id}/deliveries`, `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry` | Approving through the API (`approvals:write`) happens as the key's owner. The four-eyes rule still applies: one person cannot fill two roles, and `data_version` must equal the version that was reviewed. Approving and confirming that the original ID document was seen are off by default through the API: a human should do that. An owner enables it in Settings, API and webhooks; otherwise the API returns `403 api_approvals_disabled`. Everything done with a key is recorded with that key in the onboarding's timeline, including opening it. A key of a member restricted to certain locations only sees onboardings of those locations. ### Idempotency-Key Send an `Idempotency-Key` header (at most 200 characters, for example a UUID stored with your own record) on write requests (POST, PATCH and DELETE). - The same request with the same key returns the same response for 24 hours, with the header `Idempotent-Replayed: true`. - The same key with a different body returns `422` with code `idempotency_conflict`. - While the first request is still running, a repeat gets `409` with code `idempotency_in_progress`; retry after a few seconds. The work never runs twice. - Only successful responses are stored, so after an error you can simply retry with the same key. - Keys are scoped to the API key and to the employer: a partner reusing a key for two employers never gets one employer's response for the other. ### API versions The API uses date versions; the current one is `2026-10-01`. - An API key and a webhook endpoint remember the version from when they were created. Webhooks always use the endpoint's version; every payload has `api_version`. - Pick another version per request with the header `Agreli-Version: 2026-10-01`. An unknown version returns `422 api_version_invalid`. Responses carry the version used in the `Agreli-Version` header. - New fields, events and endpoints are added without a new version: ignore fields you do not know. - Only breaking changes (a field removed or changing meaning) get a new version. An old version keeps working for at least 12 months after its successor, and we email you well in advance. ### Acting for employers (API partners): Agreli-Organization A software company that serves many employers works as an API partner. A partner key belongs to the partner organization; each request picks the employer with the `Agreli-Organization` header (`org_…`): ```bash curl https://app.agreli.nl/api/public/v1/onboardings \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Agreli-Organization: org_…" ``` - `GET /v1/organizations` lists the employers that explicitly admitted the partner, with the permissions they granted. - Effective scopes are the intersection of the key's scopes and the permissions the employer granted. Partners choose from `organization:read`, `onboardings:read`, `onboardings:write`, `integrations:read` and `webhooks:manage`. - Without the header a partner key gets `422 organization_required`; an employer that has not admitted the partner (or ended the relationship) gives `403`. - A partner only sees and manages the onboardings that were created with its own keys. Onboardings started by the employer itself (or by another partner) do not exist for the partner: they return `404`, also in lists and in `GET /v1/events`. - Webhooks and the request log of a partner belong to the partner itself; `organization_id` in a webhook says which employer it is about. - An employer key does not need the header; if sent, it must be the key's own organization. - Test keys of a partner work in the partner's sandbox, a fictitious employer, without the header. - An employer admits a partner through an email invitation and can end the relationship at any time; the key stops working for that employer immediately. ### Key rotation In the developer dashboard a key can be rotated without downtime: - The new key gets the same name, scopes, mode and owner, and the same lifetime as the old key, counted from the rotation. - The old key stays valid for a grace period of 0 to 168 hours (default 24). With 0 hours it is revoked immediately. - An owner or manager can also rotate or revoke other people's keys; those keep acting as their owner. ## Quickstart ```bash export AGRELI_API_KEY="hrs_test_…" # 1. Find a location_id and template_id curl https://app.agreli.nl/api/public/v1/organization -H "Authorization: Bearer $AGRELI_API_KEY" # 2. Create an onboarding curl -X POST https://app.agreli.nl/api/public/v1/onboardings \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: 5d1f0b6e-4711-4c2a-9b1e-2f6f0e9a7c31" \ -H "Content-Type: application/json" \ -d '{ "reference": "sollicitant-4711", "employee": { "first_name": "Sam", "last_name": "Jansen", "email": "sam@example.com" }, "employment": { "position": "Bediening", "start_date": "2026-12-01", "location_id": "loc_…", "template_id": "t1", "hours_per_week": 16, "hourly_wage_cents": 1450 }, "options": { "return_url": "https://jouwapp.nl/onboarding/klaar" } }' # 3. Send the employee to hosted_url from the response, then follow the status curl https://app.agreli.nl/api/public/v1/onboardings/onb_… -H "Authorization: Bearer $AGRELI_API_KEY" # 4. In the sandbox: play the next step (repeat until step = payroll_completed) curl -X POST https://app.agreli.nl/api/public/v1/sandbox/onboardings/onb_…/advance -H "Authorization: Bearer $AGRELI_API_KEY" ``` The same create call in TypeScript: ```ts const BASE = "https://app.agreli.nl/api/public"; export async function createOnboarding(idempotencyKey: string) { const res = await fetch(`${BASE}/v1/onboardings`, { method: "POST", headers: { Authorization: `Bearer ${process.env.AGRELI_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, // reuse the same key when retrying }, body: JSON.stringify({ reference: "sollicitant-4711", employee: { first_name: "Sam", last_name: "Jansen", email: "sam@example.com" }, employment: { position: "Bediening", start_date: "2026-12-01", location_id: "loc_…", template_id: "t1", hours_per_week: 16, hourly_wage_cents: 1450 }, options: { return_url: "https://jouwapp.nl/onboarding/klaar" }, }), }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${res.status} ${error.type}/${error.code}: ${error.message} (${error.request_id})`); } return (await res.json()) as { id: string; status: string; hosted_url: string; expires_at: string; invitation_sent: boolean }; } ``` ## Endpoints Agreli API 1.0.0. Basis-URL: `https://app.agreli.nl/api/public`. OpenAPI 3.1: `https://app.agreli.nl/api/public/v1/openapi.json`. | Methode | Pad | Recht | Wat | | --- | --- | --- | --- | | `GET` | [`/v1/organizations`](#get-v1-organizations) | `organization:read` | Organisaties die deze sleutel mag gebruiken | | `GET` | [`/v1/organization`](#get-v1-organization) | `organization:read` | Organisatie, vestigingen, templates en toegestane domeinen | | `GET` | [`/v1/onboardings`](#get-v1-onboardings) | `onboardings:read` | Onboardings | | `POST` | [`/v1/onboardings`](#post-v1-onboardings) | `onboardings:write` | Hosted onboarding aanmaken | | `GET` | [`/v1/onboardings/{id}`](#get-v1-onboardings-id) | `onboardings:read` | Eén onboarding met stappen en documenten | | `PATCH` | [`/v1/onboardings/{id}`](#patch-v1-onboardings-id) | `onboardings:write` | Referentie en metadata bijwerken | | `POST` | [`/v1/onboardings/{id}/hosted-link`](#post-v1-onboardings-id-hosted-link) | `onboardings:write` | Nieuwe hosted link (de vorige vervalt) | | `POST` | [`/v1/onboardings/{id}/cancel`](#post-v1-onboardings-id-cancel) | `onboardings:write` | Annuleren | | `POST` | [`/v1/onboardings/{id}/identity-verification`](#post-v1-onboardings-id-identity-verification) | `approvals:write` | ID-controle bevestigen (als manager) | | `POST` | [`/v1/onboardings/{id}/approvals`](#post-v1-onboardings-id-approvals) | `approvals:write` | Goedkeuren als de eigenaar van de sleutel | | `POST` | [`/v1/onboardings/{id}/contract`](#post-v1-onboardings-id-contract) | `contracts:write` | Contract maken en ter ondertekening versturen | | `GET` | [`/v1/onboardings/{id}/documents`](#get-v1-onboardings-id-documents) | `documents:read` | Documenten | | `GET` | [`/v1/onboardings/{id}/documents/{documentId}`](#get-v1-onboardings-id-documents-documentid) | `documents:read` | Document downloaden | | `GET` | [`/v1/employees`](#get-v1-employees) | `employees:read` | Medewerkers | | `GET` | [`/v1/approvals`](#get-v1-approvals) | `approvals:read` | Wat jij nu kunt goedkeuren | | `GET` | [`/v1/integrations`](#get-v1-integrations) | `integrations:read` | Koppelingen en synchronisatiestatus | | `GET` | [`/v1/webhooks`](#get-v1-webhooks) | `webhooks:manage` | Webhookadressen | | `POST` | [`/v1/webhooks`](#post-v1-webhooks) | `webhooks:manage` | Webhookadres toevoegen | | `GET` | [`/v1/webhooks/{id}`](#get-v1-webhooks-id) | `webhooks:manage` | Eén webhookadres met recente afleveringen | | `DELETE` | [`/v1/webhooks/{id}`](#delete-v1-webhooks-id) | `webhooks:manage` | Verwijderen | | `POST` | [`/v1/webhooks/{id}/test`](#post-v1-webhooks-id-test) | `webhooks:manage` | Testbericht (ping) sturen | | `POST` | [`/v1/webhooks/{id}/rotate-secret`](#post-v1-webhooks-id-rotate-secret) | `webhooks:manage` | Geheim vernieuwen | | `GET` | [`/v1/webhooks/{id}/deliveries`](#get-v1-webhooks-id-deliveries) | `webhooks:manage` | Afleverlog van één adres | | `POST` | [`/v1/webhooks/{id}/deliveries/{deliveryId}/retry`](#post-v1-webhooks-id-deliveries-deliveryid-retry) | `webhooks:manage` | Aflevering opnieuw versturen | | `GET` | [`/v1/events`](#get-v1-events) | `onboardings:read` | Gebeurtenissen (om gemiste webhooks na te lopen) | | `POST` | [`/v1/sandbox/onboardings/{id}/advance`](#post-v1-sandbox-onboardings-id-advance) | `onboardings:write` | Sandbox: volgende stap naspelen | ### GET /v1/organizations Organisaties die deze sleutel mag gebruiken. Een werkgeverssleutel: de eigen organisatie. Een sleutel van een API-partner: alle werkgevers die de partner hebben toegelaten, met de rechten die ze gaven. Gebruik het id in de kop Agreli-Organization. Recht: `organization:read` ```bash curl https://app.agreli.nl/api/public/v1/organizations \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "id": "org_…", "object": "organization", "name": "…", "permissions": [ "…" ] } ], "has_more": false, "next_cursor": null } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/organization Organisatie, vestigingen, templates en toegestane domeinen. Recht: `organization:read` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `Agreli-Organization` | header | string | nee | Alleen voor sleutels van een API-partner: de werkgever namens wie je werkt. Verplicht als de partner meer dan één werkgever heeft. | ```bash curl https://app.agreli.nl/api/public/v1/organization \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "id": "org_…", "name": "Brasserie De Haven", "locations": [ { "id": "loc_…", "name": "De Haven Centrum", "city": "Amsterdam" } ], "templates": [ { "id": "t1", "name": "Oproepkracht bediening", "contract_type": "on_call", "cao": "Cao Horeca" } ], "branding": { "color": "#0f5132", "logo_url": null }, "allowed_origins": [ "https://jouwapp.nl" ] } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/onboardings Onboardings. Nieuwste eerst, ook geannuleerde (filter met status). Een API-partner ziet alleen de onboardings die via zijn eigen sleutels liepen. Recht: `onboardings:read` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `status` | query | `pending_employee`, `in_review`, `contract_ready`, `awaiting_employee_signature`, `awaiting_employer_signature`, `processing`, `action_required`, `completed`, `cancelled` | nee | — | | `reference` | query | string | nee | — | | `updated_since` | query | string (date-time) | nee | — | | `limit` | query | integer | nee | 1 tot 100. Standaard `25`. | | `starting_after` | query | string | nee | — | ```bash curl https://app.agreli.nl/api/public/v1/onboardings \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } ], "has_more": false, "next_cursor": null } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/onboardings Hosted onboarding aanmaken. Alleen met een lopende proef of een abonnement van wie betaalt (anders 403 subscription_required). Recht: `onboardings:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | Request body (JSON): | Veld | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | | `reference` | string | nee | Je eigen kenmerk; komt terug in de API en in webhooks. Hooguit 200 tekens. | | `metadata` | object (sleutel → string \| number \| boolean) | nee | 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. | | `employee` | object | ja | — | | `employee.first_name` | string | ja | — | | `employee.last_name` | string | ja | — | | `employee.email` | string (email) | ja | — | | `employee.phone` | string | nee | — | | `employment` | object | ja | — | | `employment.position` | string | ja | — | | `employment.start_date` | string (date) | ja | — | | `employment.location_id` | string | ja | — | | `employment.template_id` | string | ja | — | | `employment.contract_type` | `on_call`, `fixed_term`, `permanent` | nee | Standaard die van de template. | | `employment.hours_per_week` | number | ja | 0 tot 60. | | `employment.hourly_wage_cents` | integer | ja | Minimaal 1. | | `options` | object | nee | — | | `options.send_invitation` | boolean | nee | true: wij mailen de medewerker de link. false: jij stuurt de medewerker naar hosted_url. Standaard `false`. | | `options.expires_in_hours` | integer | nee | 1 tot 336. Standaard `336`. | | `options.return_url` | string (uri) | nee | Na afronden kan de medewerker hierheen terug. Alleen https, op een domein uit allowed_origins (van de werkgever, of van de API-partner); anders 422 return_url_not_allowed. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/onboardings \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "employee": { "first_name": "Sam", "last_name": "Jansen", "email": "sam@example.com", "phone": "+31 6 12345678" }, "employment": { "position": "Bediening", "start_date": "2026-12-01", "location_id": "loc_…", "template_id": "t1", "contract_type": "on_call", "hours_per_week": 16, "hourly_wage_cents": 1450 }, "options": { "send_invitation": false, "expires_in_hours": 336, "return_url": "https://jouwapp.nl/onboarding/klaar" } }' ``` Antwoord `201` (Aangemaakt): ```json { "id": "onb_…", "object": "onboarding", "status": "pending_employee", "hosted_url": "https://app.agreli.nl/m/…", "expires_at": "2026-10-26T12:00:00.000Z", "invitation_sent": false, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" } } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/onboardings/{id} Eén onboarding met stappen en documenten. Recht: `onboardings:read` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | ```bash curl https://app.agreli.nl/api/public/v1/onboardings/onb_… \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### PATCH /v1/onboardings/{id} Referentie en metadata bijwerken. Voegt metadata samen; null verwijdert een sleutel. Gegevens van de medewerker wijzig je niet via de API (field_not_updatable). De data_version blijft gelijk, dus goedkeuringen blijven geldig. Recht: `onboardings:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | Request body (JSON): | Veld | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | | `reference` | string \| null | nee | Hooguit 200 tekens. | | `metadata` | object (sleutel → string \| number \| boolean \| null) | nee | 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. | ```bash curl -X PATCH https://app.agreli.nl/api/public/v1/onboardings/onb_… \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" } }' ``` Antwoord `200` (OK): ```json { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/onboardings/{id}/hosted-link Nieuwe hosted link (de vorige vervalt). Recht: `onboardings:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | Request body (JSON, optioneel): | Veld | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | | `expires_in_hours` | integer | nee | 1 tot 336. | | `send_invitation` | boolean | nee | Standaard `false`. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/onboardings/onb_…/hosted-link \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "expires_in_hours": 336, "send_invitation": false }' ``` Antwoord `201` (OK): ```json { "onboarding_id": "onb_…", "hosted_url": "https://app.agreli.nl/m/…", "expires_at": "2026-10-26T12:00:00.000Z", "invitation_sent": false } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/onboardings/{id}/cancel Annuleren. Recht: `onboardings:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/onboardings/onb_…/cancel \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Antwoord `200` (OK): ```json { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/onboardings/{id}/identity-verification ID-controle bevestigen (als manager). Staat standaard uit: een eigenaar zet goedkeuren via de API aan bij Instellingen, API en webhooks (anders 403 api_approvals_disabled). Recht: `approvals:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/onboardings/onb_…/identity-verification \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Antwoord `200` (OK): ```json { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/onboardings/{id}/approvals Goedkeuren als de eigenaar van de sleutel. Vierogen: één persoon kan niet twee rollen vervullen. data_version moet gelijk zijn aan de gecontroleerde versie. Staat standaard uit: een eigenaar zet goedkeuren via de API aan bij Instellingen, API en webhooks (anders 403 api_approvals_disabled). Recht: `approvals:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | Request body (JSON): | Veld | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | | `role` | `manager`, `payroll_reviewer`, `owner` | ja | — | | `data_version` | integer | ja | — | ```bash curl -X POST https://app.agreli.nl/api/public/v1/onboardings/onb_…/approvals \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "role": "manager", "data_version": 1 }' ``` Antwoord `200` (OK): ```json { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/onboardings/{id}/contract Contract maken en ter ondertekening versturen. Recht: `contracts:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/onboardings/onb_…/contract \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Antwoord `200` (OK): ```json { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/onboardings/{id}/documents Documenten. Recht: `documents:read` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | ```bash curl https://app.agreli.nl/api/public/v1/onboardings/onb_…/documents \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ], "has_more": false, "next_cursor": null } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/onboardings/{id}/documents/{documentId} Document downloaden. Kopieën van identiteitsbewijzen alleen voor de salarisadministratie. Elke download staat in de audit. Recht: `documents:read` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `documentId` | path | string | ja | — | ```bash curl https://app.agreli.nl/api/public/v1/onboardings/onb_…/documents/doc_… \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200`: Het bestand (`application/pdf`, `image/png`, `image/jpeg`). Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/employees Medewerkers. Recht: `employees:read` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `limit` | query | integer | nee | 1 tot 100. | | `starting_after` | query | string | nee | — | ```bash curl https://app.agreli.nl/api/public/v1/employees \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "id": "emp_…", "first_name": "Sam", "last_name": "Jansen", "position": "Bediening", "location_id": "loc_…", "start_date": "2026-12-01", "onboarding": {} } ], "has_more": false, "next_cursor": null } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/approvals Wat jij nu kunt goedkeuren. Recht: `approvals:read` ```bash curl https://app.agreli.nl/api/public/v1/approvals \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "onboarding_id": "onb_…", "data_version": 1, "can_approve_as": [ "payroll_reviewer" ], "blocked_by": [] } ], "has_more": false, "next_cursor": null } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/integrations Koppelingen en synchronisatiestatus. Recht: `integrations:read` ```bash curl https://app.agreli.nl/api/public/v1/integrations \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): een JSON-object; de velden staan niet in de spec. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/webhooks Webhookadressen. Recht: `webhooks:manage` ```bash curl https://app.agreli.nl/api/public/v1/webhooks \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "id": "wh_…", "url": "https://jouwapp.nl/webhooks/agreli", "events": [ "onboarding.approval_required", "onboarding.completed" ], "status": "enabled", "api_version": "…", "previous_secret_expires_at": "2026-10-12T12:00:00.000Z", "secret": "whsec_…", "recent_deliveries": [ { "id": "whd_…", "object": "webhook_delivery", "event_id": "evt_…", "event_type": "…", "status": "pending", "attempts": 1, "last_status_code": 1, "last_error": "…", "created_at": "2026-10-12T12:00:00.000Z", "delivered_at": "2026-10-12T12:00:00.000Z", "next_attempt_at": "2026-10-12T12:00:00.000Z" } ] } ], "has_more": false, "next_cursor": null } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/webhooks Webhookadres toevoegen. Het geheim (whsec_…) staat alleen in dit antwoord. Recht: `webhooks:manage` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | Request body (JSON): | Veld | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | | `url` | string (uri) | ja | — | | `description` | string | nee | — | | `events` | array van `contract.sent`, `contract.signed`, `contract.signed_by_employee`, `integration.sync_failed`, `integration.sync_succeeded`, `onboarding.approval_given`, `onboarding.approval_required`, `onboarding.approved`, `onboarding.cancelled`, `onboarding.completed`, `onboarding.contract_signed`, `onboarding.created`, `onboarding.employee_submitted`, `onboarding.failed`, `onboarding.invitation_sent`, `onboarding.returned`, `onboarding.started`, `onboarding.submitted`, `onboarding.terms_changed`, `payroll.completed` | nee | Leeg = alle gebeurtenissen. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/webhooks \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "url": "https://jouwapp.nl/webhooks/agreli", "description": "Productie", "events": [ "onboarding.approval_required", "onboarding.completed" ] }' ``` Antwoord `201` (OK): ```json { "id": "wh_…", "url": "https://jouwapp.nl/webhooks/agreli", "events": [ "onboarding.approval_required", "onboarding.completed" ], "status": "enabled", "api_version": "…", "previous_secret_expires_at": "2026-10-12T12:00:00.000Z", "secret": "whsec_…", "recent_deliveries": [ { "id": "whd_…", "object": "webhook_delivery", "event_id": "evt_…", "event_type": "…", "status": "pending", "attempts": 1, "last_status_code": 1, "last_error": "…", "created_at": "2026-10-12T12:00:00.000Z", "delivered_at": "2026-10-12T12:00:00.000Z", "next_attempt_at": "2026-10-12T12:00:00.000Z" } ] } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/webhooks/{id} Eén webhookadres met recente afleveringen. Recht: `webhooks:manage` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | ```bash curl https://app.agreli.nl/api/public/v1/webhooks/wh_… \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "id": "wh_…", "url": "https://jouwapp.nl/webhooks/agreli", "events": [ "onboarding.approval_required", "onboarding.completed" ], "status": "enabled", "api_version": "…", "previous_secret_expires_at": "2026-10-12T12:00:00.000Z", "secret": "whsec_…", "recent_deliveries": [ { "id": "whd_…", "object": "webhook_delivery", "event_id": "evt_…", "event_type": "…", "status": "pending", "attempts": 1, "last_status_code": 1, "last_error": "…", "created_at": "2026-10-12T12:00:00.000Z", "delivered_at": "2026-10-12T12:00:00.000Z", "next_attempt_at": "2026-10-12T12:00:00.000Z" } ] } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### DELETE /v1/webhooks/{id} Verwijderen. Recht: `webhooks:manage` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | ```bash curl -X DELETE https://app.agreli.nl/api/public/v1/webhooks/wh_… \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Antwoord `200` (OK): een JSON-object; de velden staan niet in de spec. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/webhooks/{id}/test Testbericht (ping) sturen. Recht: `webhooks:manage` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/webhooks/wh_…/test \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Antwoord `200` (OK): een JSON-object; de velden staan niet in de spec. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/webhooks/{id}/rotate-secret Geheim vernieuwen. Het nieuwe geheim (alleen in dit antwoord) ondertekent direct. Het vorige ondertekent nog overlap_hours mee als tweede v1 in Agreli-Signature, zodat je zonder gemiste webhooks overstapt. 0 = het vorige is direct ongeldig. Recht: `webhooks:manage` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | Request body (JSON, optioneel): | Veld | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | | `overlap_hours` | integer | nee | 0 tot 168. Standaard `24`. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/webhooks/wh_…/rotate-secret \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "overlap_hours": 24 }' ``` Antwoord `200` (OK): ```json { "id": "wh_…", "url": "https://jouwapp.nl/webhooks/agreli", "events": [ "onboarding.approval_required", "onboarding.completed" ], "status": "enabled", "api_version": "…", "previous_secret_expires_at": "2026-10-12T12:00:00.000Z", "secret": "whsec_…", "recent_deliveries": [ { "id": "whd_…", "object": "webhook_delivery", "event_id": "evt_…", "event_type": "…", "status": "pending", "attempts": 1, "last_status_code": 1, "last_error": "…", "created_at": "2026-10-12T12:00:00.000Z", "delivered_at": "2026-10-12T12:00:00.000Z", "next_attempt_at": "2026-10-12T12:00:00.000Z" } ] } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/webhooks/{id}/deliveries Afleverlog van één adres. Nieuwste eerst. Afleveringen blijven 30 dagen zichtbaar. Recht: `webhooks:manage` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `status` | query | `pending`, `in_progress`, `succeeded`, `failed`, `dead` | nee | — | | `limit` | query | integer | nee | 1 tot 100. Standaard `25`. | | `starting_after` | query | string | nee | next_cursor van de vorige pagina. | ```bash curl https://app.agreli.nl/api/public/v1/webhooks/wh_…/deliveries \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "id": "whd_…", "object": "webhook_delivery", "event_id": "evt_…", "event_type": "…", "status": "pending", "attempts": 1, "last_status_code": 1, "last_error": "…", "created_at": "2026-10-12T12:00:00.000Z", "delivered_at": "2026-10-12T12:00:00.000Z", "next_attempt_at": "2026-10-12T12:00:00.000Z" } ], "has_more": false, "next_cursor": null } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry Aflevering opnieuw versturen. Met hetzelfde event-id en dezelfde inhoud, dus je kunt ontdubbelen. Kan bij geslaagde, mislukte en definitief mislukte afleveringen. Recht: `webhooks:manage` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `deliveryId` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/webhooks/wh_…/deliveries/{deliveryId}/retry \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Antwoord `200` (OK): ```json { "id": "whd_…", "object": "webhook_delivery", "event_id": "evt_…", "event_type": "…", "status": "pending", "attempts": 1, "last_status_code": 1, "last_error": "…", "created_at": "2026-10-12T12:00:00.000Z", "delivered_at": "2026-10-12T12:00:00.000Z", "next_attempt_at": "2026-10-12T12:00:00.000Z" } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### GET /v1/events Gebeurtenissen (om gemiste webhooks na te lopen). Dezelfde gebeurtenissen als de webhooks, met hetzelfde id en dezelfde inhoud, ook als je geen webhookadres hebt. Oudste eerst; bewaard 30 dagen. Bewaar het laatste id en vraag daarna op met starting_after. Recht: `onboardings:read` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `Agreli-Organization` | header | string | nee | Alleen voor sleutels van een API-partner: de werkgever namens wie je werkt. Verplicht als de partner meer dan één werkgever heeft. | | `since` | query | string (date-time) | nee | — | | `type` | query | `contract.sent`, `contract.signed`, `contract.signed_by_employee`, `integration.sync_failed`, `integration.sync_succeeded`, `onboarding.approval_given`, `onboarding.approval_required`, `onboarding.approved`, `onboarding.cancelled`, `onboarding.completed`, `onboarding.contract_signed`, `onboarding.created`, `onboarding.employee_submitted`, `onboarding.failed`, `onboarding.invitation_sent`, `onboarding.returned`, `onboarding.started`, `onboarding.submitted`, `onboarding.terms_changed`, `payroll.completed` | nee | — | | `limit` | query | integer | nee | 1 tot 100. Standaard `50`. | | `starting_after` | query | string | nee | next_cursor van de vorige pagina. | ```bash curl https://app.agreli.nl/api/public/v1/events \ -H "Authorization: Bearer $AGRELI_API_KEY" ``` Antwoord `200` (OK): ```json { "object": "list", "data": [ { "id": "evt_…", "object": "event", "type": "contract.sent", "api_version": "…", "created_at": "2026-10-12T12:00:00.000Z", "organization_id": "org_…", "data": { "onboarding_id": "onb_…", "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "data_version": 1, "role": "…", "returned_to": "…", "kind": "invitation", "connector": "…", "error_code": "…" } } ], "has_more": false, "next_cursor": null } ``` Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ### POST /v1/sandbox/onboardings/{id}/advance Sandbox: volgende stap naspelen. Alleen met een testsleutel. Vult de gegevens in, keurt goed met testgebruikers, tekent en rondt de gesimuleerde salarisverwerking af. Recht: `onboardings:write` | Parameter | In | Type | Verplicht | Omschrijving | | --- | --- | --- | --- | --- | | `id` | path | string | ja | — | | `Idempotency-Key` | header | string | nee | Zelfde sleutel en inhoud binnen 24 uur geeft hetzelfde antwoord. Loopt het eerste verzoek nog, dan 409 idempotency_in_progress: probeer het zo opnieuw. Hooguit 200 tekens. | ```bash curl -X POST https://app.agreli.nl/api/public/v1/sandbox/onboardings/onb_…/advance \ -H "Authorization: Bearer $AGRELI_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Antwoord `200` (OK): ```json { "step": "employee_submitted", "onboarding": { "id": "onb_…", "status": "pending_employee", "waiting_on": [ "employee" ], "employee": {}, "employment": {}, "data_version": 1, "approvals": {}, "contract": {}, "payroll": null, "hosted_link": null, "reference": "sollicitant-4711", "metadata": { "vacature_id": "vac-311" }, "return_url": "https://jouwapp.nl/onboarding/klaar", "source": "api", "created_at": "2026-10-12T12:00:00.000Z", "updated_at": "2026-10-12T12:00:00.000Z", "steps": [], "documents": [ { "id": "doc_…", "kind": "contract_signed", "title": "Arbeidsovereenkomst", "uploaded_at": "2026-10-12T12:00:00.000Z", "downloadable": true } ] } } ``` Een `{}` in dit voorbeeld is een object waarvan de spec de velden niet vastlegt. Fouten: `401`, `403`, `404`, `409`, `422`, `429`, in de vaste foutvorm. ## Webhooks ### Registering an endpoint Register a URL in the developer dashboard or with `POST /v1/webhooks`. The secret (`whsec_…`) is only in that response; store it to verify signatures. - HTTPS only, to a publicly reachable address, without credentials in the URL. The host name is resolved right before every delivery and internal addresses are refused. - Omit `events` (or send an empty list) to receive all events; the API shows that as `["*"]`. An unknown event name gives `422 events_invalid`. - At most 20 endpoints per organization. - An endpoint registered with a test key belongs to the sandbox and only receives sandbox events. ### Events | Gebeurtenis | Ook verstuurd als | Wanneer | Extra in `data` | | --- | --- | --- | --- | | `onboarding.created` | — | Onboarding aangemaakt, via de API of het dashboard. | — | | `onboarding.invitation_sent` | — | Uitnodiging, nieuwe link of herinnering naar de medewerker verstuurd (niet bij de contractmail). | `kind`: `invitation`, `new_link` of `reminder` | | `onboarding.started` | — | De medewerker is begonnen met invullen. | — | | `onboarding.employee_submitted` | `onboarding.submitted` | De medewerker heeft de gegevens verstuurd. | — | | `onboarding.approval_required` | — | Er is een controle nodig: na het versturen, of opnieuw als gewijzigde voorwaarden goedkeuringen lieten vervallen. | — | | `onboarding.returned` | — | Teruggestuurd voor correctie. | `returned_to` | | `onboarding.approval_given` | — | Eén goedkeuring gegeven. | `role`: `manager`, `payroll_reviewer` of `owner` | | `onboarding.approved` | — | Alle vereiste goedkeuringen zijn binnen. | — | | `onboarding.terms_changed` | — | Arbeidsvoorwaarden gewijzigd. | — | | `contract.sent` | — | Contract ter ondertekening verstuurd. | — | | `contract.signed_by_employee` | — | De medewerker heeft getekend. | — | | `onboarding.contract_signed` | `contract.signed` | De werkgever heeft ook getekend; het contract is rond. | — | | `integration.sync_succeeded` | `payroll.completed` | De medewerker staat in de salarisadministratie. | `connector` | | `integration.sync_failed` | — | Verwerking in de salarisadministratie lukt niet: geen koppeling, een fout of definitief mislukt (niet bij handmatige invoer). | `connector`, `error_code` | | `onboarding.failed` | — | Tegelijk met `integration.sync_failed`. | `connector`, `error_code` | | `onboarding.completed` | — | Onboarding afgerond. | — | | `onboarding.cancelled` | — | Onboarding geannuleerd. | — | | `ping` | — | Testbericht via `POST /v1/webhooks/{id}/test`; `data` bevat alleen `message`. | `message` | Some events are also sent under an older name ("Ook verstuurd als" = "also sent as"). Those are separate messages with their own `id`. Subscribe to one of the two names; without a filter you receive both. ### Payload and headers ```http 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… ``` ```json { "id": "evt_…", "type": "onboarding.employee_submitted", "created_at": "2026-11-20T10:15:00.000Z", "organization_id": "org_…", "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.sent`, `contract.signed`, `contract.signed_by_employee`, `integration.sync_failed`, `integration.sync_succeeded`, `onboarding.approval_given`, `onboarding.approval_required`, `onboarding.approved`, `onboarding.cancelled`, `onboarding.completed`, `onboarding.contract_signed`, `onboarding.created`, `onboarding.employee_submitted`, `onboarding.failed`, `onboarding.invitation_sent`, `onboarding.returned`, `onboarding.started`, `onboarding.submitted`, `onboarding.terms_changed`, `payroll.completed`, `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.onboarding_id` | string | — | | `data.reference` | string \| null | — | | `data.metadata` | 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.data_version` | integer \| null | — | | `data.role` | string | Bij onboarding.approval_given. | | `data.returned_to` | string | Bij onboarding.returned. | | `data.kind` | `invitation`, `new_link`, `reminder` | Bij onboarding.invitation_sent. | | `data.connector` | string \| null | Bij integration.*: nmbrs, afas, loket, manual, … | | `data.error_code` | string \| null | Bij integration.sync_failed en onboarding.failed. | Events never contain personal data: only ids, your own `reference` and `metadata`, and `data_version`. Fetch details with the API. ### Verifying the signature `Agreli-Signature` has the form `t=,v1=`, where `v1` is HMAC-SHA256 with the endpoint secret over the string `"."`. Verify it before acting on the event, compare in constant time and reject messages older than 5 minutes. Use the raw body, not a re-serialized object. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function isValidAgreliSignature(header: string | null, rawBody: string, secret: string, now = Date.now()): boolean { const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? ""); if (!m) return false; const t = m[1]!; const v1 = m[2]!; if (Math.abs(now / 1000 - Number(t)) > 300) return false; // older than 5 minutes const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest(); return timingSafeEqual(Buffer.from(v1, "hex"), expected); } // Next.js route handler export async function POST(request: Request) { const rawBody = await request.text(); if (!isValidAgreliSignature(request.headers.get("agreli-signature"), rawBody, process.env.AGRELI_WEBHOOK_SECRET!)) { return new Response("Invalid signature", { status: 400 }); } const event = JSON.parse(rawBody) as { id: string; type: string; organization_id: string; data: { onboarding_id: string } }; // Store event.id once (deduplicate), queue the work and answer quickly. return new Response(null, { status: 200 }); } ``` ### Delivery and retries - Any `2xx` response counts as received. Redirects are not followed; the timeout is 10 seconds. - Failed deliveries are retried after 1, 5, 30, 120, 360, 720 and 1440 minutes; after that the delivery is dead. - After 10 consecutive dead deliveries the endpoint is disabled; re-enable it in the developer dashboard, which also shows recent deliveries. - A message can arrive more than once: deduplicate on `id` (also in the `Agreli-Event-Id` header). - Do not rely on ordering: use `data_version` and, when in doubt, fetch the current state with `GET /v1/onboardings/{id}`. - `POST /v1/webhooks/{id}/test` sends a `ping` to one endpoint along the same path as real events. ### Secret rotation, delivery log and redelivery - `POST /v1/webhooks/{id}/rotate-secret` creates a new secret (only in that response). It signs immediately; the previous secret keeps signing for `overlap_hours` (default 24, at most 168, `0` = invalid immediately). During the overlap `Agreli-Signature` has two values, `t=…,v1=,v1=`: accept the message if one matches. `previous_secret_expires_at` says until when. - `GET /v1/webhooks/{id}/deliveries` lists all deliveries of an endpoint, newest first (filter `status`, paginate with `limit` and `starting_after`). - `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry` sends a delivery again, also a succeeded or dead one, with the same event `id`. ### Catching up on missed events `GET /v1/events` returns the same events as the webhooks, with the same `id` and payload, also when you have no webhook endpoint. Oldest first, kept for 30 days. Store the last processed `id` and continue with `starting_after`; without a cursor start from a timestamp with `since`; filter with `type`. Requires `onboardings:read`. A partner only sees events of onboardings created with its own keys. ### Webhooks for API partners A partner receives the events of an onboarding when it was created with one of the partner's keys, the employer still admits the partner, and the partner has the `onboardings:read` scope. Events go to the endpoints in the partner's own organization; `organization_id` says which employer. When the employer ends the relationship, new events stop immediately. Onboardings that the employer started itself are never visible to the partner. ## Embedding and return_url An owner or manager sets **allowed domains** in the developer dashboard (not with an API key, so a leaked key cannot redirect employees to a lookalike page). One `https://` origin per line, no paths or wildcards, at most 20. The sandbox follows the organization's list. For a partner key, both the employer's and the partner's lists count. `GET /v1/organization` returns them as `allowed_origins`. - `options.return_url` must be https on an allowed domain, otherwise `422 return_url_not_allowed` (or `return_url_invalid` for a non-https URL). - To show the onboarding inside your own site, load `https://app.agreli.nl/embed.js` and call `Agreli.mount("#onboarding", { url: hostedUrl, onEvent })`. It creates an auto-resizing iframe with camera permission (for the ID scan). The page sets `Content-Security-Policy: frame-ancestors` per link, so the iframe only works on allowed domains. - `onEvent` receives messages `{ source: "agreli", type }` without personal data: `ready`, `resize` (`height`), `verified`, `submitted`, `signed`, `status` (`state`), `return` (`return_url`; the whole page navigates there) and `closed`. Do not trust them for bookkeeping; use webhooks or the API. - Embedded, the employee still confirms with a one-time code. Browsers often block third-party cookies, so the session lives only as long as the iframe; after a reload the employee requests a new code. - If your site has a CSP, add `https://app.agreli.nl` to `script-src` and `frame-src`. Without the script, use an iframe to `https://app.agreli.nl/m/…?embed=1&origin=` with `allow="camera"` and check `event.origin` on messages. ## Metadata `metadata` is your own flat key-value object, for example a vacancy or candidate id. It is returned by the API and included in every webhook. - At most 20 keys; keys match `[A-Za-z0-9_.-]{1,40}`; values are strings (at most 500 characters), numbers or booleans. - `PATCH /v1/onboardings/{id}` changes only `reference` and `metadata`. Metadata is merged; `null` removes a key. `data_version` does not change, so approvals stay valid. Other fields give `422 field_not_updatable`. - Never put personal data in metadata. Values that look like an email address, an IBAN, a Dutch citizen service number (BSN) or a Dutch mobile number are rejected with `422 metadata_personal_data`. This detection is a safety net, not a guarantee. ## Sandbox The sandbox opens with your first test key (`hrs_test_…`). It is a separate organization ("… (sandbox)") with a test location, your branding, two test users (Test Manager and Test Accountant, so the four-eyes review works) and a test payroll system. Emails and SMS messages are captured instead of sent, including the one-time codes; read them in the developer dashboard. `POST /v1/sandbox/onboardings/{id}/advance` (test key only, scope `onboardings:write`) plays the next step and returns `{ "step": "…", "onboarding": { … } }`: | `step` | What happens | Status afterwards | | --- | --- | --- | | `employee_submitted` | The employee fills in everything, uploads a test ID scan and submits. | `in_review` | | `approved` | The test users verify the identity, review and approve. | `contract_ready` | | `contract_sent` | The contract is generated and sent for signing. | `awaiting_employee_signature` | | `employee_signed` | The employee signs. | `awaiting_employer_signature` | | `employer_signed` | The employer signs. | `processing` | | `payroll_completed` | The test payroll system processes the employee. | `completed` | A live key gets `403`. A completed or cancelled onboarding gives `409 nothing_to_advance`; payroll that is already running or waiting for an action gives `409 payroll_busy`. ## Going live - **Employers:** create a live key (`hrs_live_…`) in the developer dashboard; code and webhooks stay the same. During the trial, use only fictitious data or the sandbox until the data processing agreement is signed. - **Software partners (production access):** build with test keys in the sandbox first. Request production access in the app under Instellingen → API en webhooks with your product website, what you build and for whom, and the expected volume. Agreli reviews the request; a rejection comes with an explanation and you can apply again (a suspended partner cannot). After approval you can create live keys and invite employers through the partner portal; each employer accepts and chooses your permissions. ## Errors Every error has the same shape. `message` is Dutch and meant for humans; programs should use `type` and `code`. ```json { "error": { "type": "invalid_request", "code": "template_invalid", "message": "Onbekende template_id; zie GET /v1/organization voor de beschikbare templates", "request_id": "req_…" } } ``` `request_id` is also in the `X-Request-Id` header; mention it when contacting support. | Type | HTTP | Betekenis | | --- | --- | --- | | `invalid_request` | 400, 413, 415, 422 | Ongeldige JSON, verkeerde Content-Type, te groot verzoek of een veld dat niet klopt. `code` zegt welk. | | `authentication_error` | 401 | Geen, ongeldige of verlopen API-sleutel. | | `permission_error` | 403 | De sleutel mist het recht, of de werkgever heeft de partner niet (meer) toegelaten. | | `not_found` | 404 | Bestaat niet, of is niet zichtbaar voor deze sleutel. | | `conflict` | 409 | Kan niet in deze toestand, bijvoorbeeld een onboarding die al is afgerond. | | `rate_limited` | 429 | Te veel verzoeken. Wacht het aantal seconden uit `Retry-After`. | | `api_error` | 500 | Fout aan onze kant. Opnieuw proberen met dezelfde `Idempotency-Key` is veilig. | Common codes: `unauthorized` (401), `forbidden` (403), `organization_required`, `organization_invalid`, `idempotency_conflict`, `api_version_invalid`, `subscription_required` (403, the employer's trial ended without a plan or the plan was cancelled), `api_approvals_disabled` (403), `template_invalid`, `location_invalid`, `terms_invalid`, `return_url_invalid`, `return_url_not_allowed`, `overlap_invalid`, `since_invalid`, `type_invalid`, `metadata_invalid`, `metadata_personal_data`, `field_not_updatable`, `data_version_required`, `limit_invalid`, `cursor_invalid`, `events_invalid`, `url_insecure`, `url_not_public` (all 422), `idempotency_in_progress`, `nothing_to_advance` and `payroll_busy` (409), `invalid_json` (400), `content_type` (415), `too_large` (413, bodies over 100 kB), `rate_limited` (429) and `internal` (500). The list is not exhaustive: handle unknown codes by `type`. Retry advice: on `429` wait `Retry-After` seconds; on `500`, timeouts or dropped connections retry with the same `Idempotency-Key`; do not blindly repeat other `4xx` errors. ## Rate limits 120 requests per minute per API key. Every response includes `RateLimit-Limit` (120), `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets); a `429` also includes `Retry-After`. ## Pagination Lists (`GET /v1/onboardings`, `GET /v1/employees`) return `{ "object": "list", "data": [...], "has_more": true, "next_cursor": "onb_…" }`, newest first. Request the next page with `starting_after=`; `limit` is 1 to 100 (default 25). `GET /v1/onboardings` also filters on `status`, `reference` and `updated_since`. ## Pricing The sandbox is free. In production the Hosted Onboarding API costs EUR 7 per fully completed onboarding (introductory price, excluding VAT); volume pricing will follow. Details: https://agreli.nl/developers.