Webhooks
Get confirmed enquiries pushed to Make, Zapier, n8n — or your own endpoint — signed and versioned.
When somebody confirms an enquiry on your brand page, Ikrames can POST it to a URL you choose. That is the whole feature: structured handover, not a CRM. What you build on the other side is yours.
Set it up
- Go to Anfragen ▸ Übergabe and add your endpoint URL. It must be
https. - Copy the signing secret. It is shown exactly once. If you lose it, you can rotate it — but rotating invalidates any automation still using the old one.
- Press Test senden. You get a sample payload with
.invaliddata, so your automation can learn the shape without a real enquiry.
The events
There are four. Every active endpoint of a brand receives all of them.
ereignis | When it fires |
|---|---|
lead.confirmed | The person clicked the confirmation link in their email — not when the form was submitted. An unconfirmed enquiry is not yet an enquiry. |
lead.file_uploaded | A file was attached to a confirmed enquiry. Always after lead.confirmed for that enquiry — a file cannot arrive before the confirmation. |
lead.deleted | The brand removed the enquiry from its inbox. What you do with your copy is your decision. |
lead.erasure_requested | The person asked for erasure. This one is not a notification. See below. |
lead.erasure_requested obliges you
When you receive lead.erasure_requested, delete that record — and anything
you derived from it. The payload carries "pflicht": "loeschen" so you can
branch on a field rather than on a name. We have deleted our copy already; if
yours stays, the person's erasure request is only half honoured, and the half
that remains is in your system, not ours.
lead.deleted is the other case and deliberately carries a different name: a
brand tidying its inbox tells you nothing about that person's wishes.
The two deletion events carry no personal data
No address, no answers, no filenames — only anfrage.id, the same identifier
you already received with lead.confirmed. Sending an address in order to
announce its deletion would be the processing that is supposed to end. Match on
the id.
The payload
{
"nutzlast_fassung": "2026-09-23",
"ereignis": "lead.confirmed",
"event_id": "8f14e45f-…",
"erzeugt_at": "2026-09-23T10:00:00.000Z",
"marke": { "name": "Studio Nord" },
"formular": {
"titel": "Illustrations-Anfrage",
"slug": "illustration",
"fassung": 2,
"bedingungen": "Zwei Revisionsrunden sind enthalten.",
"fassung_hash": "sha256:1f3a…"
},
"gruppe": null,
"anfrage": {
"id": "…",
"email": "person@example.com",
"eingegangen_at": "2026-09-23T09:58:00.000Z",
"bestaetigt_at": "2026-09-23T10:00:00.000Z",
"einwilligung_fassung": "2026-09-23",
"antworten": [
{ "schluessel": "projektziel", "label": "Worum geht es?", "wert": "Ein Buchcover, Aquarell." }
],
"anhaenge": 2,
"posteingang_url": "https://ikrames.com/anfragen"
}
}Two things worth knowing about it:
- Answers carry labels, not just keys.
labelis the question as the person actually read it — taken from a copy stored with the enquiry, so it stays correct even if you edit the form afterwards. - There are no file URLs. Attachments belong to the person who uploaded
them;
anhaengetells you how many there are, andposteingang_urltakes you to them behind your login. A link that survives in a Notion row would not be a link we could take back.
The other three payloads
They share the same envelope — nutzlast_fassung, ereignis, event_id,
erzeugt_at, marke, formular — and differ only in the body.
lead.file_uploaded:
{
"ereignis": "lead.file_uploaded",
"anfrage": { "id": "…", "anhaenge": 2, "posteingang_url": "https://ikrames.com/anfragen" },
"datei": { "typ": "application/pdf", "bytes": 248512 }
}anhaenge is the count after this upload, so use the field rather than
counting events — a retry would make you count twice. There is no filename:
the name of someone else's file is often itself a disclosure.
lead.deleted and lead.erasure_requested:
{
"ereignis": "lead.erasure_requested",
"anfrage": { "id": "…" },
"pflicht": "loeschen"
}pflicht appears only on lead.erasure_requested.
Testing
The Test senden button in Übergabe sends any of the four shapes to your
endpoint. A test always carries "ereignis": "test" and names the shape in
test_fuer, so you can reject test runs on one stable field — otherwise a
rehearsal of lead.deleted would be indistinguishable from the real thing.
Versions
nutzlast_fassung versions this schema. formular.fassung_hash versions
the form the person answered. They change for different reasons — pin your
automation to the first, and use the second to spot when a form was edited.
Idempotency
event_id is unique per delivery. We retry on failure, so the same event_id
can arrive more than once — store it and skip duplicates.
Verifying the signature
Every request carries:
Ikrames-Signature: t=1758600000,v1=<64 hex chars>
Ikrames-Event-Id: <uuid>The signed material is `${t}.${rawBody}`, and the key is derived from
your secret:
key = HMAC_SHA256(key = <your secret>, msg = "ikrames:uebergabe-webhook:v1")
v1 = HMAC_SHA256(key = key, msg = `${t}.${rawBody}`) // hexNode example:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header.trim());
if (!m) return false;
const t = Number(m[1]);
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const key = createHmac("sha256", secret).update("ikrames:uebergabe-webhook:v1").digest();
const expected = createHmac("sha256", key).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(m[2], "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}A worked example you can paste into a test — secret whsec_beispiel,
t = 1758600000, body {"ereignis":"test"}:
v1 = da447fda5709e61fd58df14be025f88cad14a66c4d1ef6b3a03eb131f6c8aa85Three details that are easy to miss:
- Sign the raw body, not a re-serialised object.
JSON.parsefollowed byJSON.stringifychanges key order and whitespace, and the signature no longer matches. - Check the timestamp. Without it, anybody who once captured a valid request can replay it forever. Five minutes is a reasonable window.
- Compare in constant time.
===on hex strings leaks how much of the signature was right.
Retries and pausing
A delivery that fails is retried with backoff. If a target fails 10 times in a row, we pause it and say so on the Übergabe page, with the reason. All the retries for one enquiry count as one failure — a single outage on your side will not pause anything.
A paused target sends nothing until you press Wieder aktivieren.
What we keep
The delivery log keeps status, attempt count and response code. The payload itself is deleted after seven days — it is a second copy of the enquiry, and the enquiry is already in your inbox.