Skip to content

Developers

One signed webhook. Clean JSON, keyed by your own Fields.

When a Document is approved, Vink sends its data to your endpoint as one JSON POST. This page is everything you need to build the receiver.

Overview

An Integration is an HTTPS endpoint you add in Vink and attach to one or more Forms. When a Document of that Form is approved, by a person or by Auto-Send, Vink sends one Delivery to each attached Integration.

The body is the Payload: the Document's values keyed by the Form's own Fields, inside a small envelope. Every request is signed, so your receiver can check that Vink sent it.

  • One POST per Delivery, Content-Type: application/json
  • Signed with HMAC-SHA256 in the X-Vink-Signature header
  • Retried for about 8 hours on timeouts, 408, 429 and 5xx
  • At-least-once: deduplicate on deliveryId

Vink reads PDFs with Gemini on Vertex AI, EU region.

Getting documents in

Documents come in through the app today: upload several PDFs at once, or email them in. No code? Point any system's email at the Form's Intake Address, and each PDF attachment becomes a Document. There is no upload API yet. Need one? Tell us.

Envelope

This is a real Payload from a demo Form called Invoices, with a List Field for the invoice lines. The keys under data are your own Field keys, so every Form has its own shape.

POSThttps://your-system.example/vink
{
  "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": []
  }
}

Three rules your parser can rely on

  • Every key is always present, including optional Fields.
  • null means no value.
  • [] means a List without entries.
KeyMeaning
eventAlways document.approved.
deliveryIdUnique per Delivery. It stays the same when a Delivery is retried or sent again, so use it to deduplicate.
testtrue for a test-send, false for a real Delivery.
documentThe Document's id, its file name and when it was uploaded (ISO 8601, UTC).
formThe Form's id and the Form Version the Document was read with.
approvalmanual or auto (Auto-Send), the approving user's id (null for Auto-Send) and when it was approved.
dataThe values, keyed by Field. Numbers are numbers, dates are YYYY-MM-DD, yes/no Fields are booleans and Lists are arrays of objects.

Verify the signature

Each request carries a header with a timestamp and an HMAC-SHA256 signature. Every Integration has its own secret, starting with whsec_. You find it on the Integration in the app.

X-Vink-Signature: t=1790757291,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

To verify a request

  1. Split the header on commas and read t (Unix seconds) and v1 (hex).
  2. Compute HMAC-SHA256 with your secret over the string {t}.{rawBody}: the timestamp, a dot, and the raw request body exactly as received.
  3. Compare your result with v1 using a constant-time comparison.
  4. Reject the request if t is more than 5 minutes away from your clock. This stops a captured request from being replayed.

Use the raw body bytes. Parsing and re-serialising the JSON changes whitespace and key order, and the signature no longer matches.

Node.js
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();
// }

Delivery and retries

Vink waits up to 15 seconds for your answer.

Your answerWhat Vink does
Any 2xxDelivered.
Timeout, network error, 408, 429 or 5xxTried again after about 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours: 6 attempts over roughly 8 hours.
Any other 4xxFailed right away. An Admin can send a failed Delivery again from the app.

A Retry-After header (seconds or an HTTP date) is honoured, up to 24 hours.

Redirects are not followed. Your endpoint must use HTTPS.

Delivery is at-least-once. The same Delivery can arrive twice, for example when your receiver saved it but timed out before answering. Store deliveryId and skip one you have already handled.

Headers

You can add your own headers to an Integration, such as an API key or Basic auth. Mark a header as secret and its value is stored encrypted and only shown masked afterwards. Vink sets Content-Type and X-Vink-Signature itself.

Vink · Document · Deliveries

Test-send

From an Integration, send a test to your endpoint before any real Document goes out. Choose example values for every Field, or empty values so your receiver sees null and []. You can also send an approved Document again as a test.

A test carries "test": true, is signed like a real one and is never a Delivery: it isn't retried and doesn't appear in a Document's Delivery log. The answer from your endpoint is shown right away.

Vink · Integrations

Integration service

No developer, or no time? Rob builds the connection for you.

A one-off build, never part of a plan. Rob writes the receiver that takes the Payload into your system, whether that is an ERP, an accounting package, a spreadsheet or a database, and sets up your Forms with you.

What you get

  • A receiver that verifies the signature and writes the data into your system
  • Your Forms and Fields set up with you, tested on your own documents
  • You host it, or Rob hosts it for a monthly fee on request
  • Maintenance and changes later, billed per job

What we need from you

  • Which system the data should land in, and access to a test environment
  • A few example documents
  • One person who knows how the data is used

From €950 per connection, excl. VAT

A fixed price, agreed before we start.

Ask about a connection

Tell us a little about your system and documents. Rob replies personally.

We only use your details to reply. The request is emailed to us and not stored on the site.