Skip to content

Webhooks ​

The Webhooks tab of /nay (scope nay.admin) sends script events to your URLs. Pick the events (or all of them), press Test to check the setup, and see the last answer of each webhook.

There are two types:

  • Discord: paste the webhook URL of a Discord channel (https://discord.com/api/webhooks/...). Each event becomes an embed, titled and labeled in the server language, with the event fields in order. Discord markdown in values is escaped, nobody is pinged, and long messages are cut to the 6000 character limit.
  • Signed JSON: for your own website or bot. Each webhook gets a secret, shown when it is created or regenerated.

Deliveries ​

A delivery is a POST of JSON:

json
{ "id": "3f9c2a1b7d4e8f60", "event": "nay_car_wash.wash", "resource": "nay_car_wash", "time": 1791670794, "data": { "player": "John", "price": 150 } }

Headers: X-Nay-Event, X-Nay-Delivery (same as id), X-Nay-Timestamp (Unix seconds) and X-Nay-Signature: sha256=<hex>.

  • Answer with any 2xx status. No answer within 15 seconds counts as a network error.
  • Network errors, 5xx and 429 answers are retried after 1, 10 and 60 seconds with the same X-Nay-Delivery. Other 4xx answers are not retried.
  • After 20 failed deliveries in a row the webhook is turned off. Turn it back on in the tab, or save it with a new URL.
  • Discord deliveries are not signed and follow Discord's retry_after when rate limited.

Events ​

nay_lib events:

EventWhenData
nay.settingA setting is changed or reset.resource, key, action, actor
nay.permissionA permission rule is granted or revoked.action, type, identifier, scope, actor
nay.tokenAn API token is created or revoked.action, id, label, actor
nay.testThe Test button.message

Each script lists its own events on its page, for example Car Wash.

Check the signature ​

Before trusting a signed delivery, check its signature: HMAC-SHA256 of <X-Nay-Timestamp>.<raw body>, keyed with the secret exactly as shown (the hex text itself). Refuse old timestamps.

Node.js (Express):

js
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
app.post("/nayretis", express.raw({ type: "application/json" }), (req, res) => {
	const timestamp = req.get("X-Nay-Timestamp") ?? "";
	const expected = "sha256=" + createHmac("sha256", process.env.NAY_WEBHOOK_SECRET).update(`${timestamp}.${req.body}`).digest("hex");
	const given = req.get("X-Nay-Signature") ?? "";
	const valid = given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
	if (!valid || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
		return res.sendStatus(401);
	}
	const delivery = JSON.parse(req.body);
	res.sendStatus(204);
});

PHP:

php
<?php
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_NAY_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, getenv('NAY_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_NAY_SIGNATURE'] ?? '') || abs(time() - (int) $timestamp) > 300) {
	http_response_code(401);
	exit;
}
$delivery = json_decode($body, true);
http_response_code(204);