Quark / Developers

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.

  1. Add Quark to a Discord server you manage and run /setup. Configure the logging, alerts, moderation, and event channels.
  2. Open the dashboard, select that Discord server, and create a project. Use the public project ID shown for your experience when configuring the SDK.
  3. Open Project credentials. Copy the loader options and store the credential in Roblox Secrets as QUARK_PROJECT_KEY, restricted to quark.ammocan.dev.
  4. Use the project's SDK setup action to get the loader. Place the single Script in ServerScriptService and replace its options block. Enable HTTP requests, publish, and join a new live server. Follow Install Quark in your game for details.
  5. Check for a heartbeat in the dashboard or run /game servers project_id:YOUR_PUBLIC_PROJECT_ID. Create a webhook subscribed to server_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.

  1. Run /setup in your Discord server and open the dashboard. Select your server and create a project for the experience.
  2. Open Project credentials. Copy the project credential and the generated loader configuration.
  3. In your Roblox Creator Dashboard, create a Secret named QUARK_PROJECT_KEY. Paste the credential and restrict its domain to quark.ammocan.dev.
  4. Use SDK setup to open the Quark loader. Insert the single Script into ServerScriptService.
  5. Replace only the loader's local options block 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,
}
  1. Enable Allow HTTP Requests in the experience's security settings. Publish and join a new live server where Roblox Secrets are available.
  2. 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.

Keep the project credential in Roblox Secrets. Code contains the Secret's name, never the credential value. Do not put the credential in a LocalScript, RemoteEvent, or replicated storage.

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 nameWhen it happensFields you can use
server_startedSent 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 nameOnly 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.
player_joined and player_left are not automatic events. The supplied loader updates the roster, but does not emit join/leave webhooks. Add the custom example below if you want those events. Heartbeat and roster updates are not webhook events either.

Automation-only triggers

Builder triggerWhen it happensAvailable 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) end

Subscribe 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.

  1. Name the rule so its run history is recognizable.
  2. Select a trigger. Game events require an exact event name. Optionally filter game events or player reports by project.
  3. 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.
  4. Select a notification, incident, or support-ticket action. Notifications need a Discord channel ID the bot can post to.
  5. Write a message with supported placeholders. The preview uses sample values and does not execute an action.
  6. Set the cooldown (1–1440 minutes; default 5), enable, and save. Produce a real matching event to test the saved rule.
TriggerFields for conditions and {placeholders}Notes
game_eventevent, server_id, project, plus top-level payload fieldsExact event_type required; optional project filter. No trusted Discord owner, so ticket action is rejected.
report_createdreport_id, player_id, reason, projectOptional project filter. Ticket owner is Discord reporter or guild-linked Roblox reporter, when available.
account_linkeddiscord_id, player_id, usernameEmitted by account linking. No project filter.
ticket_openedticket_id, subject, discord_idAfter the Discord operation succeeds; automation-origin tickets do not recursively trigger rules.
ticket_closedticket_id, subject, discord_idAfter 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.

RuleWhat it checksSetup
Server offlineA 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 thresholdTotal players across servers with a heartbeat in the last five minutes. Never-connected projects are ignored.Choose project, player threshold, channel, cooldown.
Player count dropDrop against the previous five-minute player snapshot. Requires a nonzero baseline.Choose project, percentage (1–100), channel, cooldown.
Custom game eventA 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.

  1. Deploy a public HTTPS receiver with a valid certificate.
  2. Open Webhooks. Enter a name, final HTTPS URL, and one exact event type per line (1–20 subscriptions).
  3. Copy the one-time signing secret to the receiver's secret manager.
  4. Use Test. It sends quark.webhook.test and records the result. Then submit a subscribed SDK event to verify a real event delivery.
  5. 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

HeaderMeaning
X-Quark-TimestampUnix timestamp in seconds, decimal text.
X-Quark-NonceRandom string for this attempt.
X-Quark-SignatureLowercase hex HMAC-SHA256 digest.
Content-Typeapplication/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.

No stable delivery ID is sent on the wire. Retries get new timestamps and nonces. Nonce replay protection does not deduplicate retries. Include event_id in custom payloads and persist an idempotency key such as project_id + event_id before side effects.

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
MethodArgumentsBehavior
configure(options)ApiUrl (optional); ProjectId and SecretName stringsInitializes local configuration. Invalid options assert.
heartbeat(serverId, playerCount, metadata)Server ID; optional count (defaults to current players); optional tablePOST heartbeat. Returns success boolean and raw body, or failure message.
emit(serverId, eventName, attributes)Nonempty event name; optional payload tablePOST 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 IDSends current Players roster. Returns HTTP success and body/message.
reportPlayer(serverId, reporter, targetId, reason)Actual Roblox Player instance; target ID; reasonSubmits report with reporter.UserId. Reporter must be in recent roster.
Failure handling: HTTP calls use 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 / suffixRequestSuccess (HTTP 200)
POST /heartbeatserver_id: string (1–128); place_id: integer > 0; player_count: integer 0–200; metadata: object, default {}{"status":"accepted"}
POST /eventsserver_id: string (1–128); event_type: string (1–100, allowed characters above); payload: object, default {}{"status":"accepted"}
GET /controlsNo bodyArray 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 /playersserver_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 /reportsserver_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.

CredentialWhere to use itHow to manage it
Project credentialRoblox 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 secretYour 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"
  }
]
FieldTypeMeaning
idstring (UUID)Internal project identifier. Use in dashboard project filters.
namestringProject display name.
public_idstringPublic 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.

ResponseWhat to check
401Correct project credential / API key; whether it was rotated or revoked.
403API scope or your Discord workspace permissions.
404Correct project or resource ID.
409Fresh SDK nonce; recent reporter roster; conflicting ticket state.
422Required fields, allowed values, and lengths. Read the response detail.
429Slow down. Honor Retry-After if present; otherwise use bounded backoff.
503 or network errorRetry later; contact Quark support if it persists. Avoid repeatedly sending the same action.

Current integration limits

FeatureLimit
Heartbeats + custom game events1,000 per project/hour on free; 50,000 on premium. Heartbeats use the same allowance.
Player roster updates4 per minute per game server.
Player reports3 per 10 minutes per reporter in a Discord workspace.
Automations25 per workspace; cooldown 1–1440 minutes per rule.
Webhook endpoints2 free / 25 premium; 1–20 exact event subscriptions per endpoint.
Roster and heartbeat player countUp to 200 players.
Support ticketsOne 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.