Webhooks
Connect external services to your bot via HTTP API. Business tier required.
Getting started
- Open your server's dashboard → Webhooks.
- Click Create webhook. Choose Outgoing to receive events, Incoming to push actions in.
- Copy the generated secret. It signs every request so you can verify us.
- Subscribe to the events (outgoing) or actions (incoming) you care about.
- Hit the Test button next to any outgoing webhook to fire a mock payload through the real retry pipeline.
Authentication
Every delivery carries an HMAC-SHA256 signature in the X-Signature header (prefixed with sha256=) computed over the raw request body using your webhook secret. Always verify the signature before trusting the payload.
Delivery headers
X-Webhook-Id— your webhook's UUID.X-Event— event name (e.g.duel.finish).X-Signature—sha256=<hex>HMAC of the body.X-Attempt— attempt number, starting at 1.X-Idempotency-Key— stable per delivery; dedupe on your side.
Retry policy
A delivery is considered successful on HTTP 2xx. Anything else is a failure:
- Up to 5 attempts per delivery.
- Exponential backoff: 30s → 1m → 2m → 4m → 8m (~15 min total).
- Use the
X-Idempotency-Keyheader to dedupe if you receive the same delivery twice. - After the final failed attempt the log entry is marked
DEAD; you can re-queue it from the dashboard. - If your endpoint is down for planned maintenance, use Pause on the dashboard instead of erroring — it prevents noise in the retry queue.
Event catalogue
Every outgoing event with an example payload. These payloads are also what the dashboard "Test" button fires.
trade.completeA P2P trade finished successfully and goods/funds settled.
Example payload
{
"event": "trade.complete",
"guildId": "123456789012345678",
"timestamp": "2026-04-14T19:00:00.000Z",
"data": {
"tradeId": "trade_test",
"creatorId": "111111111111111111",
"counterpartyId": "222222222222222222",
"currencyId": "cur_test",
"amount": "500.00",
"completedAt": "2026-04-14T19:00:00.000Z"
}
}duel.finishA duel ended; winner is paid, loser refunded per rules.
Example payload
{
"event": "duel.finish",
"guildId": "123456789012345678",
"timestamp": "2026-04-14T19:00:00.000Z",
"data": {
"duelId": "duel_test",
"winnerId": "111111111111111111",
"loserId": "222222222222222222",
"stake": "100.00",
"currencyId": "cur_test",
"finishedAt": "2026-04-14T19:00:00.000Z"
}
}member.joinA member joined the guild.
Example payload
{
"event": "member.join",
"guildId": "123456789012345678",
"timestamp": "2026-04-14T19:00:00.000Z",
"data": {
"userId": "333333333333333333",
"username": "new_member",
"joinedAt": "2026-04-14T19:00:00.000Z"
}
}member.leaveA member left (or was kicked from) the guild.
Example payload
{
"event": "member.leave",
"guildId": "123456789012345678",
"timestamp": "2026-04-14T19:00:00.000Z",
"data": {
"userId": "333333333333333333",
"username": "former_member",
"leftAt": "2026-04-14T19:00:00.000Z"
}
}balance.changeA user’s balance delta that crossed an operator-configured threshold.
Example payload
{
"event": "balance.change",
"guildId": "123456789012345678",
"timestamp": "2026-04-14T19:00:00.000Z",
"data": {
"userId": "111111111111111111",
"currencyId": "cur_test",
"delta": "+250.00",
"newBalance": "1250.00",
"reason": "DUEL_WIN",
"occurredAt": "2026-04-14T19:00:00.000Z"
}
}moderation.actionModerator took an action against a user (warn/mute/kick/ban).
Example payload
{
"event": "moderation.action",
"guildId": "123456789012345678",
"timestamp": "2026-04-14T19:00:00.000Z",
"data": {
"action": "warn",
"targetUserId": "222222222222222222",
"moderatorId": "111111111111111111",
"reason": "Test warning from /docs/webhooks",
"occurredAt": "2026-04-14T19:00:00.000Z"
}
}Code examples
Node.js// Node.js — verifying an incoming outgoing-webhook delivery
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const SECRET = process.env.WEBHOOK_SECRET!;
app.post('/vektor-webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.header('x-signature');
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
if (!sig || sig !== expected) return res.status(401).end();
const payload = JSON.parse(req.body.toString());
console.log('event', payload.event, 'attempt', req.header('x-attempt'));
// ... handle payload.data ...
res.status(200).end();
});
app.listen(3000);Python / Flask# Python / Flask — verifying an incoming outgoing-webhook delivery
from flask import Flask, request, abort
import hmac, hashlib, os
app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"].encode()
@app.post("/vektor-webhook")
def handle():
raw = request.get_data()
sig = request.headers.get("X-Signature", "")
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
payload = request.get_json()
print("event", payload["event"], "attempt", request.headers.get("X-Attempt"))
# ... handle payload["data"] ...
return "", 200curl# Send a test payload manually (normally the dashboard "Test" button does this for you)
curl -X POST "https://your-server.example.com/vektor-webhook" \
-H "Content-Type: application/json" \
-H "X-Signature: sha256=$(echo -n '$BODY' | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')" \
-H "X-Event: duel.finish" \
-H "X-Webhook-Id: wh_abc123" \
-H "X-Attempt: 1" \
--data-raw "$BODY"Incoming webhooks
Incoming webhooks let your services trigger bot actions (add currency, assign a role, post a message) without maintaining a Discord bot of your own. See the allowed actions list in the dashboard; each action is rate-limited and requires the matching HMAC signature.
Common issues and fixes — bot not responding, missing perms, embeds not showing.