Webhooks
What happened, without asking.
KRONENWERK calls your address as soon as something you subscribed to happens in the books. Every delivery is signed, and every one is retried until it is accepted.
| Event | When it is sent |
|---|---|
| invoice.issued | An invoice was issued: the number is spent, the document is archived and immutable. |
| invoice.paid | An invoice has been settled in full — by a payment, however it was recorded. |
| invoice.cancelled | An invoice was cancelled: a reversal document exists that voids it in full. Nothing is deleted. |
| purchase.recorded | A supplier's invoice or an expense was recorded, with supplier, date and amounts. |
| payment.recorded | A payment was recorded, with direction, amount, day and the document it settles. |
| bank.transaction.imported | A bank transaction was imported and is ready to be matched. |
| kronenwerk.test | A test delivery you trigger yourself, to exercise your own verification. |
The verification, in full.
The order carries weight: the timestamp first, then the digest. A replayed old delivery has a perfectly valid digest, and only its age gives it away. Compare in constant time, over the raw bytes rather than over re-serialised JSON.
# KRONENWERK-Signature: t=1775462400,v1=9f86d0…
t, v1 = parse(header) # t=<unix seconds>, v1=<hex hmac>
if abs(now() - t) > 300: reject() # replay window, checked first
expected = hmac_sha256(key=endpoint_secret, msg=f"{t}.{raw_body}").hexdigest()
if not constant_time_equals(expected, v1): reject()
accept() # then dedupe on Idempotency-Key
Expect duplicates.
Every delivery carries an idempotency header holding the event's identifier. A response that does not reach us produces another attempt under the same identifier — remember it, and a second arrival changes nothing on your side.
Retries
Six attempts, and then it stands in the log.
If your receiver does not answer with a success, KRONENWERK tries again: after thirty seconds, then twice as long each time, up to six attempts. After that the delivery counts as failed and stands in the log with its time, status and an excerpt of the answer, where you can send it again by hand. Nothing is retried forever.