API and webhooks
Read your server's cases, tickets and appeals, and hear about them as they happen. Part of Quark Enterprise.
Tokens
A server manager makes API tokens in the dashboard, under Settings › API tokens. A token belongs to one server. Admin and Basic tokens read everything here. A Read only token reads only what it was given: cases, tickets, appeals, or any of them. A token is shown once. Keep it on your server, never in a browser or an app.
curl https://quark.bot/api/v1/tickets?status=open \
-H "Authorization: Bearer your_token"A token stops working when it is revoked, or when the server no longer has Quark Enterprise.
Limits
120 requests a minute for each token. Every answer says where you are:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1793635260Past the limit the answer is 429 with Retry-After, in seconds.
Pages
Lists are newest first. Each answer has data and nextCursor. Pass nextCursor back as cursor for the next page; it is null on the last one. A cursor only works on the list it came from.
{
"data": [ ... ],
"nextCursor": "dGlja2V0czo0MTI"
}Cases
GET/api/v1/casescases:read
Bans, kicks, timeouts and warns, and their reversals, as far back as the server's plan keeps.
| Parameter | What it does |
|---|---|
| member | Only cases about this Discord user ID |
| after, before | Unix seconds |
| limit | 1 to 100. 50 if left out |
| cursor | The nextCursor from the last page |
| bot | pro for Quark Pro's records |
GET/api/v1/cases/:caseNocases:read
One case by its number, the one Quark shows in the modlog and in /profile.
| Parameter | What it does |
|---|---|
| bot | pro for Quark Pro's records |
{
"caseNo": 148,
"kind": "ban",
"logType": 39,
"at": 1793634000,
"member": { "id": "1029384756473829101", "name": "nightowl_77" },
"moderator": { "id": "1029384756473829102", "name": "Mara" },
"reason": "Scam links",
"edited": false,
"modlogMessageId": "1302938475610293847"
}Tickets
GET/api/v1/ticketstickets:read
Every ticket, newest first.
| Parameter | What it does |
|---|---|
| status | open or closed |
| opener | Opened by this Discord user ID |
| claimedBy | Claimed by this Discord user ID |
| limit | 1 to 100. 50 if left out |
| cursor | The nextCursor from the last page |
| bot | pro for Quark Pro's records |
GET/api/v1/tickets/:ticketNotickets:read
One ticket by its number.
| Parameter | What it does |
|---|---|
| transcript | 1 to add its messages, oldest first, while they are kept |
{
"ticketNo": 412,
"status": "closed",
"category": { "id": "17", "name": "Bug report", "kind": "bug" },
"openerId": "1029384756473829101",
"threadId": "1302938475610299999",
"claimedBy": "1029384756473829102",
"members": [],
"form": [{ "label": "What happened?", "value": "Crash on load" }],
"openedAt": 1793630000,
"firstResponseAt": 1793630420,
"firstResponseBy": "1029384756473829102",
"closedAt": 1793634000,
"closedBy": "1029384756473829102",
"closeReason": "Fixed in 1.4",
"reopened": 0,
"rating": 5,
"ratingComment": null,
"lastActivityAt": 1793634000
}A transcript message has id, source (discord, slack, teams, zendesk, freshdesk, helpshift, jira, linear or system), author with id, name, company and staff, content, attachments, at, editedAt and deletedAt.
Appeals
GET/api/v1/appealsappeals:read
Waiting appeals come oldest first, the order they are answered in. The rest come newest first.
| Parameter | What it does |
|---|---|
| status | waiting, decided or all. all if left out |
| limit | 1 to 100. 50 if left out |
| cursor | The nextCursor from the last page |
| bot | pro for Quark Pro's records |
Errors
An error has a status and a body that says what to fix:
{ "error": { "code": "missing_scope", "message": "This token does not have tickets:read" } }| 400 | bad_limit, bad_cursor, bad_member, bad_status, bad_time |
| 401 | unauthorized |
| 403 | missing_scope, needs_enterprise |
| 404 | not_found |
| 429 | rate_limited |
| 503 | unavailable |
Webhooks
Add an https address in Settings › API and webhooks and pick its events. Quark sends a POST with a JSON body when one happens:
case.createdA ban, kick, timeout or warnappeal.decidedAn appeal is accepted or deniedticket.openedA member opens a ticketticket.closedA ticket is closedreview.decidedA review item is decidedraid.detectedQuark sees a raid
POST /your/endpoint
Content-Type: application/json
Quark-Event: ticket.opened
Quark-Signature: t=1793630000,v1=5f2c...
{
"id": "88213",
"event": "ticket.opened",
"guildId": "1029384756473829100",
"at": 1793630000,
"data": { "ticketNo": 412, "openerId": "1029384756473829101", ... }
}Answer with any 2xx within 10 seconds. Anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, then each day, 8 tries in all. The same event keeps its id on every try, so use it to skip repeats. Quark doesn't follow redirects.
Send test, beside each webhook, sends an event called test.
Signatures
Each webhook has a signing secret, shown once when it is made. Quark-Signature has the time it was sent (t) and an HMAC-SHA256 of the time, a full stop and the raw body (v1). Check it before you trust the body, and refuse one more than 5 minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(
header.split(",").map((part) => part.split("=")),
);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return (
expected.length === v1.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
);
}IDs and times
Discord IDs are strings, so nothing loses digits. Times are unix seconds, UTC.