Developers
Eén ondertekende webhook. Schone JSON, met je eigen velden als sleutels.
Als een document is goedgekeurd, stuurt Vink de data als één JSON-POST naar je endpoint. Op deze pagina staat alles wat je nodig hebt om de ontvanger te bouwen.
Overzicht
Een Integration is een HTTPS-endpoint dat je in Vink toevoegt en aan één of meer formulieren koppelt. Als een document van dat formulier wordt goedgekeurd, door een mens of door Auto-Send, stuurt Vink één Delivery naar elke gekoppelde Integration.
De body is de Payload: de waarden van het document met de velden van het formulier als sleutels, in een kleine envelope. Elk verzoek is ondertekend, zodat je ontvanger kan controleren dat het van Vink komt.
- Eén
POSTper Delivery,Content-Type: application/json - Ondertekend met HMAC-SHA256 in de header
X-Vink-Signature - Ongeveer 8 uur lang opnieuw geprobeerd bij timeouts, 408, 429 en 5xx
- At-least-once: ontdubbel op
deliveryId
Vink leest pdf's met Gemini op Vertex AI, EU-regio.
Documenten naar binnen krijgen
Documenten komen vandaag via de app binnen: upload meerdere pdf's tegelijk, of mail ze in. Geen code? Laat een systeem mailen naar het Intake Address van het formulier, en elke pdf-bijlage wordt een document. Er is nog geen upload-API. Heb je die nodig? Laat het ons weten.
Envelope
Dit is een echte Payload van een demoformulier Invoices, met een lijstveld voor de factuurregels. De sleutels onder data zijn je eigen veldsleutels, dus elk formulier heeft zijn eigen vorm.
{
"event": "document.approved",
"deliveryId": "dlv_3f6c2a1e-8b4d-4e2f-9a71-5c0d8e7b2f14",
"test": false,
"document": {
"id": "k97d4m2x8q1v6c3n5b0e7h9r2t4w8a1f",
"filename": "invoice-F-2026-0418.pdf",
"uploadedAt": "2026-09-30T08:12:04.000Z"
},
"form": {
"id": "jd72k9m3x5q8v1c4n6b0e3h7r2t9w5as",
"version": 3
},
"approval": {
"mode": "manual",
"by": "k1754bq2m9x6c8v3n0b5e7h1r4t2w6yd",
"at": "2026-09-30T08:14:51.000Z"
},
"data": {
"supplier_name": "Drukkerij Hoekstra B.V.",
"invoice_number": "F-2026-0418",
"invoice_date": "2026-09-14",
"purchase_order": null,
"total_excl_vat": 1240,
"vat_amount": 260.4,
"iban": "NL91ABNA0417164300",
"paid": false,
"lines": [
{
"description": "Flyers A5, 5,000",
"quantity": 5000,
"amount": 740
},
{
"description": "Posters A2, 200",
"quantity": 200,
"amount": 500
}
],
"credit_notes": []
}
}Drie regels waar je parser op kan rekenen
- Elke sleutel is er altijd, ook bij optionele velden.
nullbetekent geen waarde.[]betekent een lijst zonder regels.
| Sleutel | Betekenis |
|---|---|
| event | Altijd document.approved. |
| deliveryId | Uniek per Delivery. Blijft gelijk als een Delivery opnieuw wordt geprobeerd of opnieuw verstuurd, dus gebruik het om te ontdubbelen. |
| test | true bij een test-send, false bij een echte Delivery. |
| document | Het id van het document, de bestandsnaam en wanneer het is geüpload (ISO 8601, UTC). |
| form | Het id van het formulier en de formulierversie waarmee het document is gelezen. |
| approval | manual of auto (Auto-Send), het id van de gebruiker die goedkeurde (null bij Auto-Send) en wanneer. |
| data | De waarden, per veld. Getallen zijn getallen, datums zijn YYYY-MM-DD, ja/nee-velden zijn booleans en lijsten zijn arrays van objecten. |
Handtekening controleren
Elk verzoek heeft een header met een tijdstempel en een HMAC-SHA256-handtekening. Elke Integration heeft een eigen secret dat begint met whsec_. Je vindt het bij de Integration in de app.
X-Vink-Signature: t=1790757291,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdZo controleer je een verzoek
- Splits de header op komma's en lees
t(Unix-seconden) env1(hex). - Bereken HMAC-SHA256 met je secret over de string
{t}.{rawBody}: de tijdstempel, een punt en de ruwe body precies zoals ontvangen. - Vergelijk je uitkomst met
v1met een vergelijking in constante tijd. - Weiger het verzoek als
tmeer dan 5 minuten van je eigen klok afwijkt. Zo kan een onderschept verzoek niet opnieuw worden afgespeeld.
Gebruik de ruwe bytes van de body. Als je de JSON parset en opnieuw serialiseert, veranderen witruimte en volgorde, en klopt de handtekening niet meer.
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
// rawBody: the request body exactly as received (a Buffer or string),
// before any JSON parsing. With Express: express.raw({ type: "application/json" }).
export function verifyVink(rawBody, header, secret) {
const parts = Object.fromEntries(
(header ?? "").split(",").map((part) => part.trim().split("=", 2)),
);
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp) || !parts.v1) return false;
const age = Math.abs(Date.now() / 1000 - timestamp);
if (age > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
const received = Buffer.from(parts.v1, "hex");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
// if (!verifyVink(req.body, req.get("X-Vink-Signature"), process.env.VINK_SECRET)) {
// return res.status(401).end();
// }import hashlib
import hmac
import time
TOLERANCE_SECONDS = 5 * 60
def verify_vink(raw_body: bytes, header: str, secret: str) -> bool:
"""raw_body: the request body exactly as received, before JSON parsing."""
parts = dict(p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p)
try:
timestamp = int(parts["t"])
received = parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)
# Flask: verify_vink(request.get_data(), request.headers.get("X-Vink-Signature"), SECRET)<?php
const TOLERANCE_SECONDS = 5 * 60;
// $rawBody: the request body exactly as received, before json_decode.
function verify_vink(string $rawBody, ?string $header, string $secret): bool
{
$parts = [];
foreach (explode(',', $header ?? '') as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, null);
$parts[$key] = $value;
}
if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) {
return false;
}
$timestamp = (int) $parts['t'];
if (abs(time() - $timestamp) > TOLERANCE_SECONDS) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
// $ok = verify_vink(file_get_contents('php://input'), $_SERVER['HTTP_X_VINK_SIGNATURE'] ?? null, $secret);Bezorging en nieuwe pogingen
Vink wacht maximaal 15 seconden op je antwoord.
| Jouw antwoord | Wat Vink doet |
|---|---|
| Elke 2xx | Bezorgd. |
| Timeout, netwerkfout, 408, 429 of 5xx | Opnieuw geprobeerd na ongeveer 1 minuut, 5 minuten, 30 minuten, 2 uur en 6 uur: 6 pogingen in ongeveer 8 uur. |
| Elke andere 4xx | Meteen mislukt. Een Admin kan een mislukte Delivery opnieuw versturen vanuit de app. |
Een Retry-After-header (seconden of een HTTP-datum) wordt gerespecteerd, tot 24 uur.
Redirects worden niet gevolgd. Je endpoint moet HTTPS gebruiken.
Bezorging is at-least-once. Dezelfde Delivery kan twee keer aankomen, bijvoorbeeld als je ontvanger hem opsloeg maar een timeout gaf voordat hij antwoordde. Bewaar deliveryId en sla een Delivery over die je al hebt verwerkt.
Headers
Je kunt eigen headers aan een Integration toevoegen, zoals een API-sleutel of Basic auth. Markeer een header als geheim en de waarde wordt versleuteld opgeslagen en daarna alleen gemaskeerd getoond. Vink zet Content-Type en X-Vink-Signature zelf.
De bezorgstatus van een document in Vink, met een nieuwe poging
Test-send
Stuur vanuit een Integration een test naar je endpoint voordat er een echt document weggaat. Kies voorbeeldwaarden voor elk veld, of lege waarden zodat je ontvanger null en [] ziet. Je kunt ook een goedgekeurd document opnieuw als test versturen.
Een test heeft "test": true, is net zo ondertekend als een echte en is nooit een Delivery: hij wordt niet opnieuw geprobeerd en staat niet in de bezorglog van een document. Het antwoord van je endpoint zie je meteen.
Een Integration in Vink met de uitkomst van een test-send
Koppelservice
Geen developer, of geen tijd? Rob bouwt de koppeling voor je.
Een eenmalige klus, nooit onderdeel van een abonnement. Rob schrijft de ontvanger die de Payload in je systeem zet, of dat nu een ERP, boekhoudpakket, spreadsheet of database is, en richt samen met jou je formulieren in.
Wat je krijgt
- Een ontvanger die de handtekening controleert en de data in je systeem zet
- Je formulieren en velden samen ingericht, getest op je eigen documenten
- Jij host het, of Rob host het op verzoek voor een vast bedrag per maand
- Onderhoud en aanpassingen later, per klus gefactureerd
Wat we van je nodig hebben
- In welk systeem de data moet komen, en toegang tot een testomgeving
- Een paar voorbeelddocumenten
- Eén persoon die weet hoe de data gebruikt wordt
Vanaf €950 per koppeling, excl. btw
Een vaste prijs, afgesproken voordat we beginnen.
Vraag naar een koppeling
Vertel ons iets over je systeem en documenten. Rob antwoordt persoonlijk.