Developers
Webhooks
On this page
Webhooks tell your server about the creators you follow (up to 20 addresses per endpoint). Add an endpoint, rotate its secret and send a test event on the developer dashboard; up to 5 endpoints per account.
#Events
| Event | When | data |
|---|---|---|
post.created |
a top-level post (replies are not sent) | { post, url } (a locked post is redacted) |
follow.created |
a new follow of the creator | { follower, target, at } |
drop.created |
a coin launch by the creator | { token, name, symbol, creator, txHash, block, launchedAt, url } |
drop.sale |
a buy of the creator's coin | { token, symbol, creator, buyer, ethInWei, tokenAmount, priceEth, txHash, block, at, url } |
ping |
Send test event on the dashboard | { hook, message } |
Body: { id, type, created, network: "gully-chain-testnet", chainId: 485590, testnet: true, data }. Headers:
X-Kre8-Event, X-Kre8-Event-Id (stable per event, the same on every retry and every endpoint: dedupe on it),
X-Kre8-Delivery, X-Kre8-Attempt and X-Kre8-Signature.
#Endpoint rules
https://only, port 443 or 1024 and up, no credentials in the URL.- Every DNS answer must be public (no loopback, private, link-local or CGNAT ranges); the connection is pinned to the vetted address and redirects are not followed.
- Answer 2xx within 10 seconds. Anything else is retried after 1 min, 5 min, 30 min, 2 h and 24 h (6 attempts).
- The signing secret (
k8whsec_…) is shown once. Rotate it from the dashboard; the old one stops signing at once. - The last 20 deliveries of each endpoint are kept for 7 days and shown on the dashboard.
#Check the signature
X-Kre8-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>, signed per attempt. Refuse a t
more than 300 seconds off, and sign the exact bytes you received, never re-serialised JSON.
import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody: the exact bytes you received (do not re-serialise the JSON)
export function verifyKre8(rawBody, signatureHeader, secret, now = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(String(signatureHeader || '').split(',').map((kv) => kv.trim().split('=', 2)))
const t = Number(parts.t)
if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 || '')) return false
if (Math.abs(now - t) > 300) return false // older than 5 minutes: a replay
const want = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
const got = Buffer.from(parts.v1, 'hex')
return want.length === got.length && timingSafeEqual(want, got)
}
// Express: app.post('/kre8', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verifyKre8(req.body.toString('utf8'), req.get('x-kre8-signature'), process.env.KRE8_WEBHOOK_SECRET)) return res.sendStatus(401)
// const event = JSON.parse(req.body) // dedupe on event.id
// res.sendStatus(204)
// })Drop events come from the indexer feed, polled every 20 seconds only while an endpoint listens; there is no backfill.
The API calls behind the dashboard (/v1/developers/webhooks, /keys, /usage) are signed with the app's device key,
so use the dashboard rather than calling them by hand.