Webhooks
konata can call a url of yours when your balance crosses a threshold you set. You can configure the thresholds, the webhook url and its secret under alerts.
The request
Alerts arrive as POST with a json body:
{
"version": 1,
"type": "balance.alert", // "test" for the dashboard's test send
"at": 1767225600, // unix seconds
"user": { "id": "0d4d0f1e-4f5a-4d64-9d2f-2b2a2b7f9a11", "username": "alice" },
"balance": "4200000000",
"trigger": { "kind": "threshold", "threshold": "5000000000" } // null = test send
} balance and threshold are nanodollar strings (integer 1e-9 USD)
It is recommended to verify the version, type and trigger.kind fields, to avoid
silently breaking your receiver.
| header | meaning |
|---|---|
x-knt-event | balance-alert or test, mirroring the payload’s type |
x-knt-delivery | the delivery id; retries of one event repeat it, use it to deduplicate |
x-knt-timestamp | unix seconds, signed together with the body |
x-knt-signature | sha256=<hex>, an HMAC-SHA256 over "{x-knt-timestamp}.{raw body}" keyed with your webhook secret |
Verifying
Sign the timestamp and the raw body - re-serializing the json changes the bytes and breaks the signature. It is advised to reject a timestamp far from your own clock.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(rawBody: string, headers: Headers, secret: string) {
const timestamp = Number(headers.get('x-knt-timestamp'))
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = `sha256=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`
const received = headers.get('x-knt-signature') ?? ''
return expected.length === received.length
&& timingSafeEqual(Buffer.from(expected), Buffer.from(received))
} Delivery
Answer 2xx and the delivery is done; the body is ignored. Anything else is a failure, and the
first 200 characters of your response are shown in the dashboard so you can see why.
408, 425, 429 and 5xx are retried - up to 6 attempts, the wait doubling from 30 seconds to a
ceiling of 15 minutes. Every other status is permanent: nothing is attempted again. A request that
takes longer than 10 seconds counts as a failure.
Retries repeat x-knt-delivery, so make your handler idempotent. Deliveries for one account never
overlap, but a slow endpoint delays only your own alerts.
Rules
A threshold fires once on the way down: the rule disarms when it fires and re-arms when your balance climbs back above it. When one top-down drop crosses several thresholds at once, only the lowest one sends - one drop, one message.
Your URL must resolve to a public address.