Webhooks
Get notified in your own system when an inquiry, call, booking or signature comes in — and check that the message is from us.
This page is for whoever sets up your own system — for example a CRM, a webshop or an automation tool like Zapier, Make or n8n.
A webhook is an address on your side. When something happens in Telenoa, we send a POST with JSON to that address. If it doesn't answer with 2xx, we retry for just over a day.
Webhooks require Pro, Business or Enterprise, and only administrators can set them up.
Set up a webhook
Go to Forbindelser → API og webhooks, and click Tilføj webhook under Webhooks.
Enter the Adresse. It must start with https:// and be public — addresses on an internal network are rejected.
Choose the events you want, and click Tilføj webhook.
Copy the signing secret (it starts with whsec_) and store it in your system. It is shown only once. If you lose it, remove the webhook and create it again.
Under Leveringer og test you can send a test event and see the latest deliveries — Leveret (delivered), Venter (pending) or Fejlet (failed) — with the response from your address. Send igen delivers it once more.
Events
| Event | When |
|---|---|
henvendelse.oprettet | A new inquiry from a form, widget, API, email or phone |
henvendelse.opdateret | Status, case status, priority, category or assignee changed |
henvendelse.besvaret | A reply was sent to the customer, by a team member or by Noa |
opkald.afsluttet | Noa finished a phone call or a website conversation |
kontakt.oprettet | A new customer was added to the customer list |
booking.oprettet | Noa booked a time slot or a table |
underskrift.underskrevet | A customer signed an agreement |
Every event has the same shape:
{
"type": "henvendelse.oprettet",
"version": 1,
"id": "evt_…",
"tidspunkt": "2026-10-03T09:30:00.000Z",
"virksomhed_id": "…",
"data": {
"henvendelse": {
"id": "…",
"kilde_id": "…",
"kanal": "formular",
"status": "active",
"sagsstatus": "received",
"prioritet": "medium",
"kategori_id": null,
"ansvarlig_id": null,
"team_id": null,
"kontakt": { "navn": "Anna Jensen", "email": "anna@eksempel.dk", "telefon": "+45 12 34 56 78" },
"emne": "Tilbud på køkken",
"besked": "Hej, kan I give et tilbud på et nyt køkken?",
"oprettet": "2026-10-03T09:30:00.000Z",
"opdateret": null
}
}
}versionis 1. We may add fields, but we never rename or remove a field within the same version.idstays the same if the same event arrives twice. Use it to ignore duplicates.- The test event contains example data and
"id": "evt_eksempel".
Check that it's us
Every delivery has three headers (following Standard Webhooks):
| Header | Content |
|---|---|
webhook-id | The delivery's id |
webhook-timestamp | Unix time in seconds |
webhook-signature | v1,<signature> — there can be several, separated by spaces |
The signature is an HMAC-SHA256, base64-encoded, of the text <webhook-id>.<webhook-timestamp>.<body>. The key is the secret without whsec_, base64-decoded.
Use the raw body exactly as received. If your framework parses the JSON and serializes it again, the signature won't match. Also reject deliveries whose timestamp is more than 5 minutes old.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const secret = process.env.TELENOA_WEBHOOK_SECRET; // whsec_…
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
function isFromTelenoa(id, timestamp, signatures, body) {
if (!id || !timestamp || !signatures) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest();
return signatures.split(" ").some((s) => {
const [version, sig] = s.split(",");
if (version !== "v1" || !sig) return false;
const received = Buffer.from(sig, "base64");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post("/telenoa", express.raw({ type: "application/json" }), (req, res) => {
const body = req.body.toString("utf8");
if (!isFromTelenoa(req.get("webhook-id"), req.get("webhook-timestamp"), req.get("webhook-signature"), body)) {
return res.sendStatus(401);
}
const event = JSON.parse(body);
console.log(event.type, event.data);
res.sendStatus(204);
});
app.listen(3000);Python (Flask)
import base64, hashlib, hmac, json, os, time
from flask import Flask, abort, request
secret = os.environ["TELENOA_WEBHOOK_SECRET"] # whsec_…
key = base64.b64decode(secret.removeprefix("whsec_"))
def is_from_telenoa(id, timestamp, signatures, body: bytes) -> bool:
if not id or not timestamp or not signatures or not timestamp.isdigit():
return False
if abs(time.time() - int(timestamp)) > 300:
return False
text = f"{id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, text, hashlib.sha256).digest()).decode()
return any(
s.startswith("v1,") and hmac.compare_digest(s[3:], expected)
for s in signatures.split(" ")
)
app = Flask(__name__)
@app.post("/telenoa")
def telenoa():
body = request.get_data() # the raw body
h = request.headers
if not is_from_telenoa(h.get("webhook-id"), h.get("webhook-timestamp"), h.get("webhook-signature"), body):
abort(401)
event = json.loads(body)
print(event["type"], event["data"])
return "", 204The standardwebhooks package (npm and PyPI) can also verify the signature for you: give it the secret and the three headers.
Deliveries and retries
- Answer with 2xx within 30 seconds. Anything else counts as a failure.
- If a delivery fails, we retry after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours.
- If an address keeps failing for 5 days, it is disabled. Remove it and create it again once it works.
- Delivery contents are kept for 30 days. After that they can't be resent.
- If you downgrade to Essential, nothing is sent until you upgrade again.
The old webhook
The webhook under Opsætning → Kategorier → Regler ("Send nye henvendelser til jeres eget system") keeps working as before, with its own format and the x-telenoa-signature header. See API and webhooks. Both can be on at the same time. For new setups we recommend the webhooks on this page: more events, a fixed format and retries for just over a day.