Developer documentation · v1
Connect your game.
Automate your community.
Everything you need to use Quark in your Roblox experience: install the integration, choose events, build automations, and connect your tools.
Quickstart
Connect a Discord community, a Roblox experience, and your developer tools. Your dashboard and developer API are available at https://quark.ammocan.dev.
- Add Quark to a Discord server you manage and run
/setup. Configure the logging, alerts, moderation, and event channels. - Open the dashboard, select that Discord server, and create a project. Use the public project ID shown for your experience when configuring the SDK.
- Open Project credentials. Copy the loader options and store the credential in Roblox Secrets as
QUARK_PROJECT_KEY, restricted toquark.ammocan.dev. - Use the project's SDK setup action to get the loader. Place the single Script in
ServerScriptServiceand replace its options block. Enable HTTP requests, publish, and join a new live server. Follow Install Quark in your game for details. - Check for a heartbeat in the dashboard or run
/game servers project_id:YOUR_PUBLIC_PROJECT_ID. Create a webhook subscribed toserver_started, then start a new server to verify delivery.
Try your first automation
Open Automations, choose Game event, and enter server_started. Select a Discord notification action, choose a channel the bot can post to, and use {project} started a server: {server_id}. Save the rule, start a new Roblox server, and check Recent runs and your channel.
Use the event catalog to choose your next trigger. Custom names only work after your game emits them.
Install Quark in your game
Add the Quark bot to your Discord server, then connect your Roblox experience from the dashboard.
- Run
/setupin your Discord server and open the dashboard. Select your server and create a project for the experience. - Open Project credentials. Copy the project credential and the generated loader configuration.
- In your Roblox Creator Dashboard, create a Secret named
QUARK_PROJECT_KEY. Paste the credential and restrict its domain toquark.ammocan.dev. - Use SDK setup to open the Quark loader. Insert the single Script into
ServerScriptService. - Replace only the loader's
local optionsblock with the dashboard configuration. Keep the rest of the Script:
local options = {
ApiUrl = "https://quark.ammocan.dev",
ProjectId = "YOUR_PUBLIC_PROJECT_ID",
SecretName = "QUARK_PROJECT_KEY",
RuntimeAssetId = 0,
}- Enable Allow HTTP Requests in the experience's security settings. Publish and join a new live server where Roblox Secrets are available.
- Confirm a heartbeat in Quark or run
/game servers project_id:YOUR_PUBLIC_PROJECT_ID. Test an announcement and check the player roster.
What the loader handles
The supplied integration sends a heartbeat and roster approximately every 60 seconds, emits server_started once, polls game controls approximately every 15 seconds, and provides announcement and player-report UI. You do not need to add those loops yourself.
Leave RuntimeAssetId = 0 for the standard installation. New game servers load the current Quark integration; running servers keep their loaded version. If setup fails, check Roblox output and the troubleshooting guide.
Upgrading an older installation
Keep the public project ID and Roblox Secret. Remove the old QuarkServer and QuarkCommunity server scripts, old report LocalScript, and old announcement GUI when replacing them with the loader. Two installations can create duplicate UI and events. Custom game scripts can use the configured SDK module as shown in Custom game events.
Events & trigger catalog
Use this catalog when entering event names in Webhooks, choosing Game event in Automations, or creating a custom-event alert. Names are case-sensitive.
Game events usable by webhooks and automations
| Event name | When it happens | Fields you can use |
|---|---|---|
| server_started | Sent automatically once when the supplied Quark integration starts in a game server. | place_id (number). Automation templates also get event, server_id, project. |
| Your own custom name | Only when your server Script calls Quark.emit. Examples: round_finished, match_started, player_joined, player_left. | The top-level fields you pass in the payload, plus event, server_id, project in automations. |
Automation-only triggers
| Builder trigger | When it happens | Available placeholders |
|---|---|---|
| Player report created (report_created) | A report is submitted through Quark. | {report_id}, {player_id}, {reason}, {project} |
| Account linked (account_linked) | A member creates a new Roblox account link through Quark. Refreshing an existing link does not trigger it again. | {discord_id}, {player_id}, {username} |
| Ticket opened (ticket_opened) | A new Discord support ticket is successfully created. Reopening an existing ticket is not this trigger. | {ticket_id}, {subject}, {discord_id} |
| Ticket closed (ticket_closed) | A Discord support ticket is successfully closed. | {ticket_id}, {subject}, {discord_id} |
These are selectable triggers in the automation builder, not automatic webhook subscriptions. Tickets opened by an automation do not trigger additional automations. quark.webhook.test is sent only by the webhook Test button.
Example: player join and leave events
Add one server Script alongside the loader in ServerScriptService. This is optional custom game code. It emits one join for players already present when it starts and handles future joins/leaves:
local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local runtime = game:GetService("ServerScriptService"):WaitForChild("QuarkRuntime", 30)
if not runtime then return end
local Quark = require(runtime:WaitForChild("Quark", 10))
local serverId = game.JobId ~= "" and game.JobId or "studio-test"
local seen = {}
local function emitPlayer(eventName, player)
task.spawn(function()
local ok = Quark.emit(serverId, eventName, {
event_id = HttpService:GenerateGUID(false),
player_id = tostring(player.UserId),
username = player.Name,
})
if not ok then warn("Quark could not send " .. eventName) end
end)
end
local function joined(player)
if seen[player] then return end
seen[player] = true
emitPlayer("player_joined", player)
end
Players.PlayerAdded:Connect(joined)
Players.PlayerRemoving:Connect(function(player)
seen[player] = nil
emitPlayer("player_left", player)
end)
for _, player in Players:GetPlayers() do joined(player) endSubscribe a webhook to player_joined, or select Game event → player_joined in Automations. Use {username} ({player_id}) joined {project} as a notification template. These events consume your event allowance. Leave/shutdown delivery is best effort; do not use it as a guaranteed record of every departure.
Field names in each destination
For an emitted payload {"player_id":"123","username":"Builder"}, automation conditions use player_id and templates use {username}. A webhook receiver reads data.event.player_id and data.event.username. The webhook's top-level type is the emitted event name.
Automation builder
Open Automations → Create automation in your Discord workspace. Each rule has one trigger, an optional exact-match condition, and one action. Up to 25 rules per guild are supported.
- Name the rule so its run history is recognizable.
- Select a trigger. Game events require an exact event name. Optionally filter game events or player reports by project.
- Set an optional field and matching value. Conditions compare the string representation of one top-level value, with case-sensitive equality. They do not support nested paths, ranges, regular expressions, AND/OR groups, or scripts.
- Select a notification, incident, or support-ticket action. Notifications need a Discord channel ID the bot can post to.
- Write a message with supported placeholders. The preview uses sample values and does not execute an action.
- Set the cooldown (1–1440 minutes; default 5), enable, and save. Produce a real matching event to test the saved rule.
| Trigger | Fields for conditions and {placeholders} | Notes |
|---|---|---|
| game_event | event, server_id, project, plus top-level payload fields | Exact event_type required; optional project filter. No trusted Discord owner, so ticket action is rejected. |
| report_created | report_id, player_id, reason, project | Optional project filter. Ticket owner is Discord reporter or guild-linked Roblox reporter, when available. |
| account_linked | discord_id, player_id, username | Emitted by account linking. No project filter. |
| ticket_opened | ticket_id, subject, discord_id | After the Discord operation succeeds; automation-origin tickets do not recursively trigger rules. |
| ticket_closed | ticket_id, subject, discord_id | After successful closure for non-automation-origin tickets. No project filter. |
Actions and run status
notify queues a Discord notification; queued is not proof of delivery. Check alert delivery history and bot permissions. incident creates a medium-severity incident and records completed. ticket queues a ticket for the associated Discord member. It records skipped if no member is known, tickets are not configured, the member already has an active ticket, or the guild has 100 active tickets.
Templates are 1–1500 characters. Placeholder names use letters, digits, and underscores. Missing values become empty strings; rendered messages are truncated at 1500 characters. Generated actions never re-enter the dispatcher.
Recipe: notify staff after a ranked round
Use the custom event example in SDK methods. Create a game_event rule, event round_finished, field mode, value ranked, action notify, and your staff channel ID. Use:
Ranked round finished in {project} on {server_id}. Winner: {winner_id}.
Start with a 1-minute cooldown while testing. Submit one event, inspect Recent runs, and confirm the Discord message. Wait for cooldown before testing again. Cooldown applies to the whole rule, across matching servers and players.
Recipe: turn a report into an incident
Select report_created, optionally choose a project, and use incident with Report {report_id}: player {player_id} — {reason}. This supports staff tracking without requiring a linked Discord ticket owner.
Why a rule did not run
Check enabled state, exact event name, project filter, field casing, and cooldown. Events filtered out before execution do not create history rows. Repeated processing of the same event key is deduplicated per rule. A skipped action still consumes cooldown. Recent runs shows the latest 50 rows; queued actions finish through normal delivery processes.
Alert rules & monitoring
Use Alert rules when you want a Discord notification about server health or player counts. These conditions are separate from webhook event names and automation triggers.
| Rule | What it checks | Setup |
|---|---|---|
| Server offline | A server has not sent a heartbeat for the configured time. Normal server shutdowns also stop heartbeats. | Choose project, 2–1440 minutes, notification channel, cooldown. |
| Players below threshold | Total players across servers with a heartbeat in the last five minutes. Never-connected projects are ignored. | Choose project, player threshold, channel, cooldown. |
| Player count drop | Drop against the previous five-minute player snapshot. Requires a nonzero baseline. | Choose project, percentage (1–100), channel, cooldown. |
| Custom game event | A new exact-name game event from the selected project. | Enter server_started or a custom name your game emits. |
Server/player conditions are checked approximately every minute. Up to 50 rules are supported per workspace. Check Recent alert deliveries after testing. Maintenance windows suppress monitoring notifications for planned downtime. server_offline, players_below, and player_drop are monitoring conditions, not automatically emitted webhook events.
Webhooks & delivery
Outbound webhooks send accepted custom SDK events to your HTTPS service. They are separate from the automation builder. This release does not automatically deliver every report, ticket, or account-link trigger as a webhook.
- Deploy a public HTTPS receiver with a valid certificate.
- Open Webhooks. Enter a name, final HTTPS URL, and one exact event type per line (1–20 subscriptions).
- Copy the one-time signing secret to the receiver's secret manager.
- Use Test. It sends
quark.webhook.testand records the result. Then submit a subscribed SDK event to verify a real event delivery. - Inspect delivery history for status, attempts, response code, and errors.
Envelope
{
"type": "round_finished",
"data": {
"project_id": "YOUR_PUBLIC_PROJECT_ID",
"server_id": "ROBLOX_JOB_ID",
"event": {"event_id": "UNIQUE_EVENT_ID", "mode": "ranked"}
}
}
The test envelope is {"type":"quark.webhook.test","data":{"guild_id":"DISCORD_GUILD_ID"}}. Normal events use data.project_id for the public ID and place custom payload under data.event. Do not confuse top-level type with this nested payload.
Signing contract
| Header | Meaning |
|---|---|
| X-Quark-Timestamp | Unix timestamp in seconds, decimal text. |
| X-Quark-Nonce | Random string for this attempt. |
| X-Quark-Signature | Lowercase hex HMAC-SHA256 digest. |
| Content-Type | application/json |
Compute HMAC-SHA256 using the endpoint secret over the bytes of timestamp + "." + nonce + "." + rawBody. Compare digests in constant time. Verify before parsing JSON: reserializing can change bytes and invalidate signatures.
Retries and idempotency
Quark normally makes up to five total attempts, waiting 30, 60, 120, and 240 seconds after successive failures. Any 2xx succeeds. Redirects are not followed; other status codes and network failures retry. Request timeout is 8 seconds, with a 5-second connect timeout. Deliveries can arrive more than once; design your receiver to handle duplicates.
After exhausted retries, the endpoint is disabled. Fix the destination, re-enable it, and use delivery retry where available. Dashboard test sends immediately and records delivered/failed; also test a subscribed game event to check your complete integration.
Acknowledge with 2xx only after durably accepting work. Keep processing fast, or enqueue and process asynchronously. Never acknowledge a failed enqueue as successful. A receiver that processed an event but lost the response must accept it again safely.
Verify webhook signatures
If you build your own webhook receiver, verify that each request came from Quark before acting on it. Use the signing secret shown when you created that webhook. A normal Discord webhook URL does not understand Quark's event envelope; you need a receiver or integration that accepts Quark payloads.
This Node.js helper checks the signature and a five-minute freshness window. Pass the exact raw request body as a Buffer and a Fetch-style Headers object. Call it in your receiving service, not in your Roblox game or a browser:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyQuark(rawBody, headers, secret) {
const timestamp = headers.get("x-quark-timestamp") || "";
const nonce = headers.get("x-quark-nonce") || "";
const signature = headers.get("x-quark-signature") || "";
if (!/^\d+$/.test(timestamp) ||
Math.abs(Date.now() / 1000 - Number(timestamp)) > 300 ||
!nonce || nonce.length > 256 || !/^[a-f0-9]{64}$/.test(signature)) {
throw new Error("Invalid or stale webhook headers");
}
const expected = createHmac("sha256", secret)
.update(timestamp + "." + nonce + ".")
.update(rawBody)
.digest();
if (!timingSafeEqual(expected, Buffer.from(signature, "hex"))) {
throw new Error("Invalid webhook signature");
}
const event = JSON.parse(rawBody.toString("utf8"));
if (!event || typeof event.type !== "string" ||
!event.data || typeof event.data !== "object" || Array.isArray(event.data)) {
throw new Error("Invalid event envelope");
}
return { event, nonce };
}Handle replay and duplicate delivery
After verification, atomically record the nonce for this endpoint for at least 601 seconds and reject reused nonces. Your receiving service must provide that shared storage; the helper above only verifies authenticity and freshness.
Retries have new nonces. To avoid repeated side effects, include an event_id in your custom game payload and deduplicate on project_id + event_id when accepting work. Return 2xx for an already processed event without processing it again. Do not use nonce as the logical event identifier.
Respond correctly
Return 2xx only once your service has reliably accepted the event. Return non-2xx if acceptance fails so Quark can retry. Keep response time short. Catch helper exceptions in your route and reject invalid requests. Set a request-body limit appropriate to your receiver and never log your signing secret.
Verify your integration
- The dashboard Test succeeds with your configured signing secret.
- Changing one body byte or using a different secret fails verification.
- Old timestamps and repeated nonces are rejected.
- A retried event gets 2xx without repeating its action.
SDK methods & custom events
Configure before making calls. With the loader, the runtime configures the module. Use a bounded wait in custom server Scripts so missing runtime startup does not block forever:
local ServerScriptService = game:GetService("ServerScriptService")
local runtime = ServerScriptService:WaitForChild("QuarkRuntime", 30)
if not runtime then
warn("Quark runtime unavailable")
return
end
local module = runtime:WaitForChild("Quark", 10)
if not module then return end
local Quark = require(module)
local serverId = game.JobId ~= "" and game.JobId or "studio-test"
local ok, result = Quark.emit(serverId, "round_finished", {
event_id = game:GetService("HttpService"):GenerateGUID(false),
mode = "ranked",
winner_id = "123456789",
})
if not ok then warn("Quark event was not accepted: " .. tostring(result)) end| Method | Arguments | Behavior |
|---|---|---|
| configure(options) | ApiUrl (optional); ProjectId and SecretName strings | Initializes local configuration. Invalid options assert. |
| heartbeat(serverId, playerCount, metadata) | Server ID; optional count (defaults to current players); optional table | POST heartbeat. Returns success boolean and raw body, or failure message. |
| emit(serverId, eventName, attributes) | Nonempty event name; optional payload table | POST event. Returns success boolean and raw body, or failure message. |
| pollControls(handler) | Function(action, payload) | Retrieves up to 25 pending controls; invokes handler and attempts acknowledgement. Returns true and count when polling succeeds. |
| syncPlayers(serverId) | Server ID | Sends current Players roster. Returns HTTP success and body/message. |
| reportPlayer(serverId, reporter, targetId, reason) | Actual Roblox Player instance; target ID; reason | Submits report with reporter.UserId. Reporter must be in recent roster. |
pcall, but argument assertions, calling before configuration, and JSON decoding can raise errors. A controls handler is acknowledged completed if it does not throw; returning false does not mark it failed. Throw for unsupported actions. The module does not expose acknowledgement failures in its final poll result.Design event names and payloads
Use stable names such as round_finished. Names are 1–100 characters using letters, digits, underscore, dot, or hyphen. Payloads are JSON objects; include only needed information. Add your own event_id for receiver idempotency. Reuse it when retrying the same logical event, but generate a new HTTP nonce.
Automation conditions read top-level payload fields. Reserved values event, server_id, and project are replaced by Quark event values. Use flat primitive fields for predictable matching. Subscriptions require exact names; no wildcards.
Controls
The runtime handles announce, lock, unlock, kick, and shutdown. Controls are project-level: polling marks retrieved controls delivered, so they are not guaranteed broadcasts to every server. Acknowledgements record completed or failed. Monitor status before assuming an action ran.
SDK HTTP reference
Prefix: /sdk/v1/projects/{public_id}. All six routes require a project Bearer credential and fresh X-Quark-Nonce. JSON POSTs also need Content-Type: application/json. Use the public ID, not the internal UUID.
| Method / suffix | Request | Success (HTTP 200) |
|---|---|---|
| POST /heartbeat | server_id: string (1–128); place_id: integer > 0; player_count: integer 0–200; metadata: object, default {} | {"status":"accepted"} |
| POST /events | server_id: string (1–128); event_type: string (1–100, allowed characters above); payload: object, default {} | {"status":"accepted"} |
| GET /controls | No body | Array of {id, action, payload}; max 25, ordered by creation. Retrieved records become delivered. |
| POST /controls/{control_id} | control_id: UUID; body status: completed or failed | {"status":"accepted"}; 404 if no matching delivered control. |
| POST /players | server_id: string (1–128); players: array, max 200; each {user_id: positive integer below 2^63, username: string (1–50)} | {"status":"accepted"}; replaces this server’s roster; repeated user IDs collapse to one. |
| POST /reports | server_id: string (1–128); reporter_id and target_id: positive integers below 2^63; reason: string (3–1500) | {"id":"report UUID"}; requires reporter seen in same server roster in last 5 minutes. |
The Luau SDK encodes Roblox user IDs as decimal strings, which input models accept. Preserve large identifiers as strings in JavaScript to avoid precision loss. HTTP 200 acceptance confirms API processing, not downstream Discord or webhook delivery.
Custom event request
POST /sdk/v1/projects/YOUR_PUBLIC_PROJECT_ID/events
Authorization: Bearer YOUR_PROJECT_CREDENTIAL
X-Quark-Nonce: UNIQUE_REQUEST_NONCE
Content-Type: application/json
{
"server_id": "ROBLOX_JOB_ID",
"event_type": "round_finished",
"payload": {"event_id": "UNIQUE_EVENT_ID", "mode": "ranked"}
}
Credentials & API keys
Use Project credentials to connect your Roblox experience. Use API keys only if you are building an external integration with Quark's public API. Each belongs to the Discord workspace where it was created.
| Credential | Where to use it | How to manage it |
|---|---|---|
| Project credential | Roblox Secret QUARK_PROJECT_KEY. The SDK reads it for you. | Project credentials in the dashboard. |
| API key (qk_…) | Your external integration’s server, as Authorization: Bearer qk_… | Create/revoke in API keys. The full key appears once. |
| Webhook signing secret | Your custom HTTPS webhook receiver. Verifies incoming deliveries. | Copy once when creating a webhook. |
Scopes
Use projects:read to list your workspace's projects through the public API. This is the supported public scope in this release. Manage projects, automations, and webhooks through the dashboard.
Replacing a credential
If you rotate a project credential, update the Secret in Roblox immediately: the previous credential stops working. Verify the connection afterward. Give each external integration its own API key so you can revoke one independently. For webhooks, create a replacement endpoint, configure its secret at your receiver, test it, and disable the old endpoint.
Never put credentials in client code, public logs, screenshots, or a shared game asset. If you cannot access a workspace, check that you manage that Discord server and sign in again.
REST API reference
Base URL: https://quark.ammocan.dev/api/v1. Responses use JSON. Use this endpoint when an external tool needs to list your Quark projects. Configure your game and automations through the dashboard.
GET /api/v1/projects
Required scope: projects:read. No request body or query parameters. Returns all project records in the key's guild, including disabled projects; it does not filter by enabled state or offer pagination.
[
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "My experience",
"public_id": "YOUR_PUBLIC_PROJECT_ID"
}
]| Field | Type | Meaning |
|---|---|---|
| id | string (UUID) | Internal project identifier. Use in dashboard project filters. |
| name | string | Project display name. |
| public_id | string | Public SDK identifier used in SDK URL paths. |
200 returns the array. 401 means missing, revoked, expired, or invalid API key. 403 means the key lacks projects:read. Each key accesses only its own Discord workspace.
JavaScript / Node.js example
This uses built-in fetch from a server process. It is an HTTP example, not a published Quark JavaScript SDK.
const response = await fetch("https://quark.ammocan.dev/api/v1/projects", {
headers: { Authorization: `Bearer ${process.env.QUARK_API_KEY}` },
signal: AbortSignal.timeout(10000),
});
if (!response.ok) throw new Error(`Quark returned ${response.status}`);
const projects = await response.json();
console.log(projects);
Python example
This uses Python's standard library; no Quark Python SDK is provided in this release.
import json
import os
from urllib.request import Request, urlopen
request = Request(
"https://quark.ammocan.dev/api/v1/projects",
headers={"Authorization": f"Bearer {os.environ['QUARK_API_KEY']}"},
)
with urlopen(request, timeout=10) as response:
projects = json.load(response)
print(projects)
Troubleshooting & limits
My game is not connected
Check the public ProjectId in the loader, the QUARK_PROJECT_KEY Secret, its allowed domain, and Allow HTTP Requests. Publish and test in a live server where Secrets are available. Look for Quark messages in Roblox output. If Quark itself is unavailable, try again later or contact Quark support.
My event never arrives
Check the exact name in the catalog. server_started is automatic, but player_joined, player_left, and your other custom events must be emitted by your game. Enter the same spelling and case in webhook subscriptions or the automation event field. Create the subscription before sending a fresh event.
My automation does nothing
Check enabled state, selected project, exact name, condition field/value, and cooldown. The preview does not run the action. Conditions use top-level fields such as player_id, not data.event.player_id. Review Recent runs: skipped explains an unavailable action; queued means the action is waiting to complete. For notifications, check channel ID and the bot's permission to post.
My webhook fails
Check delivery history and your receiving tool's logs. Use the final HTTPS URL without redirects, configure the correct signing secret, and return 2xx on acceptance. If retries disable the endpoint, fix your receiver, enable it in Quark, then retry the failed delivery. The Test button checks a special test message; also test a real subscribed event.
Reports or announcements fail
Reports require the reporting player in the recent roster; wait for roster sync and retry. Check that the player is testing in the correct experience/project. For duplicated announcements or report UI, remove old manual scripts after switching to the loader. For a control that does not complete, check its status and game output.
| Response | What to check |
|---|---|
| 401 | Correct project credential / API key; whether it was rotated or revoked. |
| 403 | API scope or your Discord workspace permissions. |
| 404 | Correct project or resource ID. |
| 409 | Fresh SDK nonce; recent reporter roster; conflicting ticket state. |
| 422 | Required fields, allowed values, and lengths. Read the response detail. |
| 429 | Slow down. Honor Retry-After if present; otherwise use bounded backoff. |
| 503 or network error | Retry later; contact Quark support if it persists. Avoid repeatedly sending the same action. |
Current integration limits
| Feature | Limit |
|---|---|
| Heartbeats + custom game events | 1,000 per project/hour on free; 50,000 on premium. Heartbeats use the same allowance. |
| Player roster updates | 4 per minute per game server. |
| Player reports | 3 per 10 minutes per reporter in a Discord workspace. |
| Automations | 25 per workspace; cooldown 1–1440 minutes per rule. |
| Webhook endpoints | 2 free / 25 premium; 1–20 exact event subscriptions per endpoint. |
| Roster and heartbeat player count | Up to 200 players. |
| Support tickets | One active ticket per member; 100 active per workspace. |
Do not automatically retry state-changing actions without checking whether the previous attempt succeeded. Event delivery is not guaranteed to happen exactly once.
Quark