Server API
Webhooks
With a v2 key, Uplink can send your server a signed JSON POST whenever something happens: a new case, a mod call, a session starting, a staff member clocking in.
#Adding one
- Add up to 3 webhooks per key with
POST /webhooks, giving an httpsurland theeventsyou want. - The answer has the webhook's secret. It is shown once, so save it next to your key.
POST /webhooks/:id/testsends a ping so you can check your address works.- After 20 failed deliveries in a row the webhook is switched off. Fix your address, then call
POST /webhooks/:id/enable.
Terminal
curl -X POST https://uplinkrblx.xyz/api/server/v2/webhooks \
-H "Authorization: Bearer usk_your_key" \
-H "Content-Type: application/json" \
-d '{"url":"https://bot.example.com/uplink","events":["case.created","modcall"]}'#Every delivery
Every delivery looks like {"id", "event", "server", "at", "data"}. Answer with any 2xx within 5 seconds.
| Header | What it holds |
|---|---|
x-uplink-event | The event name, like case.created |
x-uplink-timestamp | When it was sent, in seconds |
x-uplink-signature | sha256= and the HMAC-SHA256 of timestamp.body with your webhook secret |
#Events
| Event | When it is sent |
|---|---|
case.created | A case was made, in Uplink, in game or through the API |
case.voided | A case was voided |
session.started | A session started |
session.ended | A session ended, with its stats |
modcall | A player called for a moderator in game |
shift.started | A staff member clocked in |
shift.ended | A staff member clocked out |
#case.created
JSON
{
"id": "b1f0c3e2-…",
"event": "case.created",
"server": "1234567890123456789",
"at": 1760000000000,
"data": {
"case": {
"number": 42,
"type": "warn",
"status": "active",
"target": { "robloxId": "1520318804", "name": "DashCamDan" },
"moderator": { "discordId": "200000000000000007", "name": "Ava (API: Staff bot)" },
"reason": "Driving on the sidewalk",
"evidence": null,
"inGame": true,
"createdAt": 1760000000000,
"voidedAt": null,
"voidedBy": null
}
}
}#case.voided
JSON
{
"id": "b1f0c3e2-…",
"event": "case.voided",
"server": "1234567890123456789",
"at": 1760000300000,
"data": {
"case": {
"number": 42,
"type": "warn",
"status": "voided",
"target": { "robloxId": "1520318804", "name": "DashCamDan" },
"reason": "Driving on the sidewalk",
"voidedAt": 1760000300000,
"voidedBy": "200000000000000007"
}
}
}#session.started
JSON
{
"id": "b1f0c3e2-…",
"event": "session.started",
"server": "1234567890123456789",
"at": 1760000000000,
"data": {
"session": {
"id": 7,
"name": "Evening patrol",
"message": "Peacetime is on.",
"startedBy": { "discordId": "200000000000000007", "name": "Ava" },
"startedAt": 1760000000000,
"endedAt": null,
"endedBy": null,
"peakPlayers": 0
}
}
}#session.ended
JSON
{
"id": "b1f0c3e2-…",
"event": "session.ended",
"server": "1234567890123456789",
"at": 1760007200000,
"data": {
"session": { "id": 7, "name": "Evening patrol", "endedAt": 1760007200000 },
"stats": { "joins": 48, "cases": 3 }
}
}#modcall
JSON
{
"id": "b1f0c3e2-…",
"event": "modcall",
"server": "1234567890123456789",
"at": 1760000000000,
"data": {
"caller": { "robloxId": "1520318802", "name": "RiverCityRP" },
"at": 1760000000000
}
}#shift.started
JSON
{
"id": "b1f0c3e2-…",
"event": "shift.started",
"server": "1234567890123456789",
"at": 1760000000000,
"data": {
"shift": {
"id": 12,
"discordId": "200000000000000001",
"name": "builderman",
"robloxId": "1520318801",
"startedAt": 1760000000000,
"endedAt": null,
"seconds": null
}
}
}#shift.ended
JSON
{
"id": "b1f0c3e2-…",
"event": "shift.ended",
"server": "1234567890123456789",
"at": 1760003600000,
"data": {
"shift": {
"id": 12,
"discordId": "200000000000000001",
"name": "builderman",
"robloxId": "1520318801",
"startedAt": 1760000000000,
"endedAt": 1760003600000,
"seconds": 3600
}
}
}#Check every delivery
Check the signature on every delivery, and refuse anything older than five minutes. Use the raw body exactly as it arrived, before parsing it.
Node.js
import crypto from "node:crypto";
function verify(req, rawBody, secret) {
const t = req.headers["x-uplink-timestamp"];
const want = "sha256=" + crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const got = String(req.headers["x-uplink-signature"] ?? "");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
}