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:
local ok, code = SA_SendWebhook("eccec12b-e6ae-4c6c-901d-d800caa56dfe", {
item = "Sword",
amount = 3,
})
if not ok then
warn("Webhook not sent: " .. code)
endThe template Bought {amount}× {item} — {roblox_username} ({executor}) then arrives as Bought 3× Sword — Builderman (Wave).
SA_SendWebhookexists once the whitelist has activated the session — call it from your code, not fromSA_INIT_SCRIPT.- It never kicks and never throws: it returns
true, "OK"orfalseand a code. - Calls are sent one at a time; it yields until Discord answers (usually well under a second). Wrap it in
task.spawnif you don't want to wait.
Why it can't be faked
| Protection | What it stops |
|---|---|
| Every call is signed with the running session's secret | Calling the relay without a valid key, from another script, or from a browser |
| Each call carries a sequence number the server checks | Recording a request and replaying it |
| Key, HWID, Roblox account, place, job, script, executor, IP and time come from the server's session | Pretending to be another player or key |
| Per-session rate limit | Flooding 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.
| Placeholder | Filled by | Value |
|---|---|---|
{key} / {key_full} | server | Key (first 8 characters) / full key |
{hwid} / {hwid_full} | server | HWID (first 12 characters) / full HWID |
{roblox_username} | server | Roblox account that started the script |
{place_id}, {roblox_job_id} | server | Game and server the script started in |
{script_id}, {script_name} | server | The protected script |
{service_name}, {provider_name}, {provider_id} | server | Service and key provider |
{executor} | server | Executor name |
{ip}, {timestamp} | server | Player's IP, ISO 8601 time |
{roblox_universe_id} | your script | Not known to the server — pass it if you use it |
anything else, e.g. {item} | your script | Keys: 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_SendWebhookworks. The dispatch URL itself answers403. - Public URL — anyone with the dispatch URL can also post, with any values (only
{ip}and{timestamp}are set by the server). Relays created beforeSA_SendWebhookexisted start in this mode; switch them to Protected script once your script usesSA_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
| Code | Meaning |
|---|---|
OK | Delivered to Discord |
THROTTLED | Over the relay's per-minute limit — the event is dropped |
NOT_FOUND | No such relay, it's turned off, it belongs to another service, or it's limited to another script |
KEY_REVOKED | The key was disabled, banned or expired since the script started |
SESSION_INVALID | The session ended or hasn't been activated yet |
UPSTREAM_ERROR | Discord refused the message (deleted webhook, invalid embed) |
NO_SESSION | Called outside a protected script, or after the session ended |
INVALID_WEBHOOK | The first argument isn't a dispatch ID or URL |
INVALID_VALUES | A key or value breaks the limits above |
NETWORK_ERROR | No answer from SantiAuth |
SECURITY_ERROR | The 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:
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.