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.

JavaScript
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.