kaneoHelp
Integrations

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 Kaneo, 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 Integrationer → 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

EventWhen
henvendelse.oprettetA new inquiry from a form, widget, API, email or phone
henvendelse.opdateretStatus, case status, priority, category or assignee changed
henvendelse.besvaretA reply was sent to the customer, by a team member or by Neo
opkald.afsluttetNeo finished a phone call or a website conversation
kontakt.oprettetA new customer was added to the customer list
booking.oprettetNeo booked a time slot or a table
underskrift.underskrevetA 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
    }
  }
}
  • version is 1. We may add fields, but we never rename or remove a field within the same version.
  • id stays 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):

HeaderContent
webhook-idThe delivery's id
webhook-timestampUnix time in seconds
webhook-signaturev1,<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 "", 204

The 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.

See also

On this page