Protect your script

Discord webhooks

Post to Discord from your script with SA_SendWebhook — signed, rate-limited, and without leaking the webhook URL.

On this page

Why a relay

Posting to a Discord webhook straight from Lua puts its URL in your script — anyone can dump it and spam your channel. Create a relay in Webhooks → Discord notifications instead: SantiAuth keeps the real Discord URL, you design the message and embed in the dashboard, and your script only knows the relay's dispatch ID.

Sending from your script

Inside a script protected by SantiAuth, call SA_SendWebhook with the dispatch ID (or the whole dispatch URL) and the values for your placeholders:

lua
local ok, code = SA_SendWebhook("eccec12b-e6ae-4c6c-901d-d800caa56dfe", {
    item = "Sword",
    amount = 3,
})

if not ok then
    warn("Webhook not sent: " .. code)
end

The template Bought {amount}× {item} — {roblox_username} ({executor}) then arrives as Bought 3× Sword — Builderman (Wave).

  • SA_SendWebhook exists once the whitelist has activated the session — call it from your code, not from SA_INIT_SCRIPT.
  • It never kicks and never throws: it returns true, "OK" or false and a code.
  • Calls are sent one at a time; it yields until Discord answers (usually well under a second). Wrap it in task.spawn if you don't want to wait.

Why it can't be faked

ProtectionWhat it stops
Every call is signed with the running session's secretCalling the relay without a valid key, from another script, or from a browser
Each call carries a sequence number the server checksRecording a request and replaying it
Key, HWID, Roblox account, place, job, script, executor, IP and time come from the server's sessionPretending to be another player or key
Per-session rate limitFlooding your channel from one run
@ in values is neutralised in the message text@everyone / role pings smuggled in through values

What a player can still do: someone who owns a valid key and hooks their own copy of your script can change the values your code passes (like item above) — in their own name, within the rate limit. Don't use webhooks as proof of a purchase; check that server-side.

Placeholders

Write placeholders in curly braces in the message, embed title, description, fields and footer.

PlaceholderFilled byValue
{key} / {key_full}serverKey (first 8 characters) / full key
{hwid} / {hwid_full}serverHWID (first 12 characters) / full HWID
{roblox_username}serverRoblox account that started the script
{place_id}, {roblox_job_id}serverGame and server the script started in
{script_id}, {script_name}serverThe protected script
{service_name}, {provider_name}, {provider_id}serverService and key provider
{executor}serverExecutor name
{ip}, {timestamp}serverPlayer's IP, ISO 8601 time
{roblox_universe_id}your scriptNot known to the server — pass it if you use it
anything else, e.g. {item}your scriptKeys: lowercase letters, digits and _, up to 32 characters

Your values are strings (numbers and booleans are converted), at most 256 characters each and 20 per call. If your script passes a server placeholder like roblox_username, the server's value wins. Unknown placeholders stay in the message as typed, so typos are easy to spot.

Relay settings

Who can send

  • Protected script (default for new relays) — only SA_SendWebhook works. The dispatch URL itself answers 403.
  • Public URL — anyone with the dispatch URL can also post, with any values (only {ip} and {timestamp} are set by the server). Relays created before SA_SendWebhook existed start in this mode; switch them to Protected script once your script uses SA_SendWebhook.

Script — limit the relay to one protected script, or let any of your scripts use it.

Rate limit — max messages per minute: per running script in Protected script mode, per caller IP for the public URL. 0 means no limit for the public URL and 10 per minute for scripts.

Send on every script start — also post the relay automatically every time a key is validated, with the server placeholders filled. Leave it off for relays your script calls itself, or every start produces a message with empty {item}s.

Return codes

CodeMeaning
OKDelivered to Discord
THROTTLEDOver the relay's per-minute limit — the event is dropped
NOT_FOUNDNo such relay, it's turned off, it belongs to another service, or it's limited to another script
KEY_REVOKEDThe key was disabled, banned or expired since the script started
SESSION_INVALIDThe session ended or hasn't been activated yet
UPSTREAM_ERRORDiscord refused the message (deleted webhook, invalid embed)
NO_SESSIONCalled outside a protected script, or after the session ended
INVALID_WEBHOOKThe first argument isn't a dispatch ID or URL
INVALID_VALUESA key or value breaks the limits above
NETWORK_ERRORNo answer from SantiAuth
SECURITY_ERRORThe answer wasn't signed by SantiAuth (request is hooked)

Public URL (outside a protected script)

For relays in Public URL mode, POST JSON to the dispatch URL from anywhere:

lua
request({
    Url = "https://api.santiauth.com/api/v1/discord-webhooks/<dispatch-id>/dispatch",
    Method = "POST",
    Headers = { ["Content-Type"] = "application/json" },
    Body = game:GetService("HttpService"):JSONEncode({
        values = { roblox_username = game.Players.LocalPlayer.Name, place_id = tostring(game.PlaceId) },
    }),
})

Answers: 200 sent, 403 DISCORD_WEBHOOK_SESSION_ONLY the relay is in Protected script mode, 404 no such relay or it's off, 429 over the per-IP limit, 502 Discord refused the message. Opening the dispatch URL in a browser (GET) shows a short usage hint.