Docs navigation: WebhooksWhen a message is sent
Operations

Webhooks for new values

On the Pro plan and above, Quellenkontor reports every new or changed value to your server. That way you keep values current without checking yourself.

When a message is sent

#

Webhooks are available from the Pro plan onward. As soon as a new, changed or corrected value appears in the changelog, Quellenkontor sends a message to your address.

  • Delivery happens once a day, at 07:00 UTC. So a change from Monday reaches you by Tuesday morning at the latest.
  • You only get changes recorded after you created the webhook. Fetch the state from before that once, via the history.
  • We add new values once they are officially promulgated, usually weeks before they take effect. That gives your software a lead time.

Create a webhook

#
  1. Enter your server's address under Webhooks in your account. It must start with https:// and have a domain name, not an IP address or an internal name.
  2. Choose the datasets you are interested in. Without a selection, you get all of them.
  3. Copy the secret. It starts with whsec_ and is shown only once. You use it to verify the signature.
  4. Trigger a test message with "Send test" and check that your server accepts it.

Up to 10 webhooks are possible per account.

Events

#
ereignisMeaning
wert.neuA new step has been recorded, for example next year's contribution ceilings
ankuendigungA promulgated change whose value or field only arrives later, for example a new formula starting next year
wert.geaendertAn existing step has changed due to a new rule
korrekturWe corrected an error in a value. Check any calculations that are affected
datensatz.neuA new dataset is available
testTest message from your account, without a dataset

Message format

#

The message arrives via POST with the header Content-Type: application/json:

Message
{
  "ereignis": "wert.neu",
  "id": 42,
  "datensatz": "mindestlohn",
  "gueltig_ab": "2027-01-01",
  "text": "Mindestlohn ab 01.01.2027 eingetragen",
  "beleg_url": "https://www.recht.bund.de/bgbl/1/2025/268/VO.html",
  "abruf": "https://api.quellenkontor.dev/v1/hr/mindestlohn?datum=2027-01-01",
  "gesendet_am": "2026-11-05T07:00:04.000Z"
}
FieldMeaning
ereignisType of change, see above
idNumber of the entry in the changelog, unique and ascending
datensatzAffected dataset, null for test
gueltig_abWhen the new value takes effect, otherwise null
textShort description of the change, in German
beleg_urlOfficial source of the change
abrufReady-made API address for fetching the new value
gesendet_amTime the message was sent, in UTC

Verify the signature

#

Every message carries the header Quellenkontor-Signatur in the form t=<timestamp>,v1=<signature>. The signature is an HMAC-SHA256 in hex over the timestamp, a period, and the raw text of the request, using the webhook's secret as the key. Here is how you verify it:

  1. Read the raw text before you parse it as JSON. Even a single different whitespace character changes the signature.
  2. Compute the signature yourself and compare it to v1 in constant time.
  3. Discard messages with a timestamp older than five minutes. This protects against replayed messages.

Node.js

JavaScript
import crypto from "node:crypto";

// kopf: value of the "Quellenkontor-Signatur" header, body: the raw text of the request
export function istEcht(kopf, body, geheimnis) {
  const teile = Object.fromEntries(kopf.split(",").map((t) => t.split("=")));
  const soll = crypto.createHmac("sha256", geheimnis).update(`${teile.t}.${body}`).digest("hex");
  const frisch = Math.abs(Date.now() / 1000 - Number(teile.t)) <= 300;
  return frisch && typeof teile.v1 === "string" && teile.v1.length === soll.length
    && crypto.timingSafeEqual(Buffer.from(soll), Buffer.from(teile.v1));
}

Python

Python
import hashlib
import hmac
import time

def ist_echt(kopf: str, body: bytes, geheimnis: str) -> bool:
    teile = dict(t.split("=", 1) for t in kopf.split(","))
    soll = hmac.new(geheimnis.encode(), teile["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    frisch = abs(time.time() - int(teile["t"])) <= 300
    return frisch and hmac.compare_digest(soll, teile.get("v1", ""))
Like the API key, the secret belongs in an environment variable. If you lose it, delete the webhook and create a new one.

Responses and retries

#
  • Respond within 8 seconds with a 2xx status. Do any heavy work afterward, for example through a queue.
  • We do not follow redirects (3xx); they count as a failure.
  • On failure, we retry at the next daily run, up to five times in total. After that we give up on that message.

Processing messages

#

Store the id of every message you process. If your server processed a message but responded too late, it arrives again at the next run. You recognize this by the id. You then fetch the new value using the address in abruf, which counts as one request.

Next.js, Route Handler
// app/api/quellenkontor/route.ts
import { istEcht } from "@/lib/signatur";

export async function POST(request: Request) {
  const body = await request.text(); // raw text, before any JSON.parse
  const kopf = request.headers.get("quellenkontor-signatur") ?? "";
  if (!istEcht(kopf, body, process.env.QK_WEBHOOK_GEHEIMNIS!)) return new Response(null, { status: 401 });

  const nachricht = JSON.parse(body);
  if (await schonVerarbeitet(nachricht.id)) return new Response(null, { status: 200 });
  await inWarteschlange(nachricht); // do the actual work later
  return new Response(null, { status: 204 });
}

Something missing or unclear? Write to us and we will extend the docs.