Webhooks
dzbot tells your bot when something happens on your server — a kill, a new order, a ticket reply — instead of your bot asking every minute. Every delivery is signed, so your bot knows it comes from dzbot.
// SetupSet up
In your server's admin area under Developers, open your app, then Webhooks: add your endpoint's URL (https, reachable from the internet) and pick the events it receives. Copy the signing secret. Send test delivers a sample right away.
An endpoint only receives events its app may read: each event names the rights that open it, and the app's rights ceiling has to hold one of them.
// CodeReceive
Express — the signature is computed over the raw body, so mount express.raw on this route, before any JSON parser:
import express from 'express'
import { expressWebhook, isEvent } from '@dzbot/sdk'
const app = express()
app.post('/webhooks/dzbot', express.raw({ type: 'application/json' }), expressWebhook(process.env.DZBOT_WEBHOOK_SECRET, async (event) => {
if (isEvent(event, 'log.player_kill')) {
const { killer, victim, weapon, distance } = event.payload.data
console.log(`${killer.username} killed ${victim.username} with ${weapon} at ${distance} m`)
}
}))Workers, Deno, Bun, Next.js route handlers — anything with Request and Response:
import { webhookHandler } from '@dzbot/sdk'
export const POST = webhookHandler(process.env.DZBOT_WEBHOOK_SECRET, async (event) => { /* … */ })Both answer 400 on a bad signature, 200 once your handler is done and 500 when it throws — then dzbot tries again later. verifyWebhook(rawBody, header, secret) and parseEvent(rawBody, header, secret) are there for everything else.
// FormatDelivery
A POST with content-type: application/json; the body is the event:
{
"id": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a90",
"name": "order.created",
"organizationId": 7,
"serverId": 12,
"occurredAt": "2026-10-27T18:04:11.532+00:00",
"payload": { "order": { "id": 4711, "code": "7KQM", "total": 2500 }, "player": { "id": 1609285, "username": "Survivor" } }
}| Header | Meaning |
|---|---|
dzbot-signature | t=<unix seconds>,v1=<signature> — see below |
dzbot-webhook-id | The event's id. The same event can arrive twice; skip ids you have handled |
dzbot-event | The event name |
user-agent | dzbot-webhooks/1 |
Answer with any 2xx within 10 seconds; do slow work after answering. Events from the game logs arrive as soon as dzbot has read them, which is the 5–15 minutes it takes Nitrado to hand out the logs.
// SecuritySignature
v1 is the hex HMAC-SHA256 of <t>.<raw body> with your signing secret. Check it in constant time and reject deliveries whose t is more than 5 minutes off your clock.
// Node, without the SDK
import { createHmac, timingSafeEqual } from 'node:crypto'
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - Number(t)) <= 300The SDK and dzbot test against the same signature examples — use them to test your own implementation.
// ReliabilityRetries
A delivery that gets no 2xx is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then given up. An endpoint that has failed for 24 hours is paused, and the organization's admins get a Discord message; switch it on again in the app once it works. The app shows every delivery of the last 7 days with its answer.