Reference

Webhooks

Keepcord POSTs a signed JSON payload to your endpoint for every event you subscribe to. Verify the signature, respond 2xx quickly, and do the work asynchronously.

Setup

  1. 1

    Dashboard → Developer → Webhooks → Add

    HTTPS URL, one or more events (wildcards allowed: * or pull.*). Private and loopback addresses are refused.
  2. 2

    Copy the secret

    Shown once. Used to sign every delivery.
  3. 3

    Send a test

    The Test button delivers a settings.updated event immediately so you can check your handler.

Webhooks are a Premium and Plus feature.

Events

EventMeaningNotes
verify.successA member verified and was allowed.targetId = user id; data: status, trustScore, country, flags
verify.flaggedVerified but flagged (alt suspect, rule).data: flags, reason
verify.blockedVerification refused.data: reason (vpn, blacklist, country, age, rule), vpnProvider
verify.revokedMember revoked the app or the token could not be refreshed.
member.deletedVerification data deleted (dashboard, /deluser, API, retention).
member.blacklistedUser added to the blacklist.data: reason, kinds
member.whitelistedUser added to the whitelist.
pull.started / pull.progress / pull.completed / pull.failedPull job lifecycle. progress is sent at most every 30 seconds.data: jobId, progress
backup.started / backup.completed / backup.failedBackup job lifecycle.data: jobId, version, stats
restore.started / restore.completed / restore.failedRestore job lifecycle.data: jobId, mode, plan
incident.nukeAnti-nuke triggered.data: actorId, actions, action taken, backupVersion
incident.mass_banMany bans in a short window.data: actorId, count
incident.guild_deletedKeepcord lost the server (deleted or bot kicked and server gone).
incident.bot_removedThe bot was removed from the server.
guild.joined / guild.leftBot joined or left.
settings.updatedDashboard settings changed.data: keys
team.updatedTeam member added, changed or removed.
job.cancelledA job was cancelled.data: jobId, type

Payload

Delivery
POST /hooks/keepcord HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: Keepcord-Webhooks/1.0
X-Keepcord-Event: verify.blocked
X-Keepcord-Timestamp: 1760950872
X-Keepcord-Signature: sha256=5b1c0f…e2a9

{"event":"verify.blocked","guildId":"987654321098765432","timestamp":"2026-10-20T09:41:12.000Z","actorId":null,"targetId":"240000000000000077","message":"Blocked: VPN detected (NordVPN)","data":{"reason":"vpn","vpnProvider":"NordVPN","country":"NL"}}
HeaderValue
X-Keepcord-EventEvent type, same as the body's event field.
X-Keepcord-TimestampUnix seconds when the delivery was signed. Reject deliveries older than 5 minutes to prevent replays.
X-Keepcord-Signaturesha256= + hex HMAC-SHA256 of {timestamp}.{rawBody} with your secret.

Verifying the signature

Use the raw body

Compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialization. Frameworks that parse JSON automatically usually expose the raw body through a dedicated option.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SECRET = process.env.KEEPCORD_WEBHOOK_SECRET;

app.post("/hooks/keepcord", express.raw({ type: "application/json" }), (req, res) => {
  const ts = req.header("x-keepcord-timestamp") ?? "";
  const given = req.header("x-keepcord-signature") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);

  const expected = "sha256=" + createHmac("sha256", SECRET).update(`${ts}.${req.body}`).digest("hex");
  if (given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body);
  res.sendStatus(204); // ack first
  queueMicrotask(() => handle(event)); // then work
});

function handle(event) {
  if (event.event === "verify.blocked") console.log("blocked", event.targetId, event.data.reason);
}

app.listen(3000);

Retries & disabling

  • A delivery is successful on any 2xx within 8 seconds.
  • Failures are retried with exponential back-off after 1, 2, 4 and 8 minutes (5 attempts in total), then the delivery is marked failed.
  • Each delivery, its response code and attempt count is visible under Developer → Webhooks → Deliveries.
  • A successful delivery resets the failure counter; after 25 consecutive failed attempts the webhook is disabled and shown as such in the dashboard. Re-enable it once your endpoint is back.
  • Deliveries are at-least-once. Use X-Keepcord-Timestamp plus targetId/data.jobId as an idempotency key if duplicates matter.

Testing locally

Expose a local port with a tunnel (ngrok, cloudflared, tailscale funnel) and point the webhook at the public URL. The Test button in the dashboard sends a real signed delivery. You can also replay the signature logic offline:

import { createHmac } from "node:crypto";
const secret = "whsec_test";
const ts = Math.floor(Date.now() / 1000);
const body = JSON.stringify({ event: "settings.updated", guildId: "1", timestamp: new Date().toISOString(), actorId: null, targetId: null, message: "test", data: {} });
console.log("X-Keepcord-Timestamp:", ts);
console.log("X-Keepcord-Signature: sha256=" + createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex"));

Prefer polling? The same events are available through GET /v1/logs.