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.

headermeaning
x-knt-eventbalance-alert or test, mirroring the payload’s type
x-knt-deliverythe delivery id; retries of one event repeat it, use it to deduplicate
x-knt-timestampunix seconds, signed together with the body
x-knt-signaturesha256=<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.