Docs and helpConnecting
Send orders to your own tools with a webhook
A signed message to an address you choose for every new order: that is how you pass orders on to Zapier, Make or n8n, and from there to Notion, Google Sheets or your own system.
For every new order, Mark2Form sends a short JSON message to an https address you choose. That is called a webhook. It lets you connect Zapier, Make or n8n, and through them Notion, Google Sheets or your ERP, for example: every order lands there by itself, without retyping.
Setting it up
In your portal, open your software and go to the Use it tab. Click Connect, choose On my website and open More explanation. Connect step by step takes you to the Connect screen; there, under Notification on a new order, choose Add address. During your trial the Connect button is not there yet.
- Only the owner of the business can add an address.
- After saving, you see the secret for this address (it starts with
whsec_). You see it once: store it right away, for example in the settings of your Zapier, Make or n8n step. - Send test message sends you a message of type
ping, so you can try your connection without a real order.
The message
We send a POST with these headers:
Content-Type: application/json
User-Agent: Mark2Form-Webhooks/1
Mark2Form-Signature: t=<unix time>,v1=<hex>
Mark2Form-Event: bestelling.aangemaakt
Mark2Form-Delivery: <id of this delivery>
And this body. The field names are Dutch; the name, address and order are made up:
{
"id": "b66adce7-ec1d-8d38-89c3-f668edd73ca4",
"type": "bestelling.aangemaakt",
"versie": 1,
"aangemaakt_op": "2026-09-23T09:41:07.412Z",
"data": {
"bestelling_id": "3f2b8c1e-7a4d-4e21-9b6f-0c5d8e2a1b47",
"ordernummer": "3F2B8C1E",
"soort": "bestelling",
"aangemaakt_op": "2026-09-23T09:41:07.412Z",
"omschrijving": "Neon sign 'Open' — 80 cm, warm white",
"klant": { "naam": "Jane Example", "email": "jane@example.com" },
"bedrag": { "centen": 34950, "valuta": "EUR" },
"software": { "slug": "neon-studio", "naam": "Neon Studio" },
"portaal_url": "https://app.mark2form.com/portaal/bestellingen/3f2b8c1e-7a4d-4e21-9b6f-0c5d8e2a1b47"
}
}
A test message is smaller:
{ "id": "…", "type": "ping", "versie": 1, "aangemaakt_op": "…", "data": { "test": true } }
Nothing else is in the message, on purpose: no design files, no download
links and no internal data. You fetch those in your portal, via the address in
portaal_url. That way a production file never ends up in a spreadsheet or a
mailbox where it does not belong.
The amount is in cents (34950 is 349.50 euros), so no rounding difference
creeps into your books.
Is it really from us? Checking the signature
Anyone who knows your address can send something to it. That is why we sign every message. Check the signature before you do anything with the message.
The Mark2Form-Signature header holds a time (t) and a signature (v1). The
signature is an HMAC-SHA256 over <t>.<raw body>, with your secret as the key.
Three things have to be right:
- Use the raw body, exactly as it arrived, not the body after
JSON.parse. One space of difference and the signature no longer matches. - The key is the whole secret, including
whsec_, as UTF-8 text. - The time may not differ from now by more than 300 seconds. That way nobody can replay an old message.
An example in Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
export function isReallyFromMark2Form(rawBody, header, secret) {
if (!header) return false;
const parts = header.split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
return parts
.filter(([k]) => k === "v1")
.some(([, v]) => {
const b = Buffer.from(v ?? "");
return b.length === expected.length && timingSafeEqual(b, expected);
});
}
If the signature does not match, answer with a 401 and do nothing else with
the message.
Using Node? Let @mark2form/contracts do it for you
The same check is in our package @mark2form/contracts, which is public on npm
(MIT licence). We sign the messages with that same package.
npm install @mark2form/contracts
import { verifieerHandtekening, WEBHOOK_HEADERS } from "@mark2form/contracts/webhooks";
const rawBody = await request.text(); // the raw body: not request.json() first
const result = await verifieerHandtekening({
geheim: process.env.MARK2FORM_WEBHOOK_SECRET,
ruweBody: rawBody,
header: request.headers.get(WEBHOOK_HEADERS.handtekening),
});
if (!result.geldig) return new Response(null, { status: 401 });
const message = JSON.parse(rawBody); // only now
This belongs in a route that receives a standard Request, as in Next.js, Hono
or an edge function. Good to know:
- The names in the package are Dutch, and they are the same in every language:
verifieerHandtekeningis “verify signature”,geheimis “secret”,ruweBodyis “raw body” andgeldigis “valid”. - The package is an ES module: use
import, notrequire. It needs WebCrypto, so Node 20 or newer. - If the signature does not match,
result.reden(“reason”) says why, such asgeen-header(no header),buiten-tolerantie(outside the time window) orgeen-overeenkomst(no match). Useful for your own log.
Answers, retries and duplicate messages
- Answer within a few seconds with a 2xx (for example
200). Do the heavy work afterwards, not while we wait. - If that fails, we try again: after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. That is six attempts in total.
- If five orders in a row do not arrive at all, we switch the address off and email the owner.
- The same message can arrive twice. We deliver “at least once”, and a
retry carries the same
id. If you have already processed thatid, skip the message.
Lost the secret, or the address switched off?
In the portal you can, for an address:
- Renew the secret. The old secret stops working straight away, so put the new one into your own system, or your Zapier, Make or n8n step, right away.
- Switch the address back on, if it was switched off because five orders in a row did not arrive. The counter then starts again at zero.
With Zapier, Make or n8n
You do not need a server of your own: these services give you an address to paste into your portal.
- Zapier. Start a Zap with Webhooks by Zapier and choose Catch Hook. Zapier gives you an address; paste it into your portal and send a test message. Then choose the next step, for example a row in Google Sheets.
- Make. Start a scenario with the Webhooks module and choose Custom webhook. Copy the address into your portal and send a test message, so Make learns the fields of the message.
- n8n. Start a workflow with a Webhook node, set the method to
POSTand use that node’s production address. Send a test message to see the fields.
Check the signature there too if the service can, for example in a code step. If it cannot, share the address with nobody, and when in doubt work with the data from your portal rather than from the message.
See also Adding Neon Studio to your site.