DZDEV
// SDK · Webhooks

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" } }
}
HeaderMeaning
dzbot-signaturet=<unix seconds>,v1=<signature> — see below
dzbot-webhook-idThe event's id. The same event can arrive twice; skip ids you have handled
dzbot-eventThe event name
user-agentdzbot-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)) <= 300

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