Skip to content

Developer API ​

The developer API lets your own software talk to SnowstormBot. An API is simply a set of web addresses that a program can call, instead of a person clicking buttons on the dashboard. Think of a web shop that gives a buyer a VIP role, a game server that checks whether a player is in your Discord server, or a website that shows your team's nicknames and avatars.

With the API your program can:

  • see which of your Discord servers it can reach
  • look up members (name, nickname, avatar and roles)
  • check whether someone is a member of your server
  • list all members of a server
  • give and take roles
  • set or clear a nickname
  • view support tickets, including their history and messages
  • open, close and reopen support tickets
  • run an automation that uses the API triggered trigger

Paid plan required

The developer API is only included in paid plans that list it. See the plans page to pick one.

Your API key ​

Every request your program sends must prove it comes from you. It does that with an API key: a long secret piece of text that starts with ssb_. It works like a password for your program.

You have one key per account. It works for every server you own where the bot is active. People you gave dashboard access to through delegated managers cannot use the API, and your key never reaches servers you do not own.

Creating a key ​

  1. Open the dashboard and click your name at the top right.
  2. Choose Developer API.
  3. Click Create a key.
  4. Copy the key from the window that opens.

The key is only shown once. We do not store it in a readable form, so nobody (not even us) can show it to you again. If you lose it, rotate it to get a new one.

Keep the key secret

Treat the key like a password. Store it in your app's secret settings, not in your code. Never put it in a web page or a browser script, because anyone can read those. If the key ever leaks, delete it straight away (see below).

Rotating a key ​

Rotating means swapping your key for a new one. Click Rotate key on the Developer API page. You get a new key right away, and your old key keeps working for one more hour. That hour gives you time to put the new key in your apps without anything breaking. After the hour the old key stops working on its own.

If you rotate again during that hour, the oldest key stops working immediately, so there are never more than two working keys.

A program that keeps sending a wrong or expired key gets its address blocked for a few minutes (too_many_failed_attempts). Requests with a working key always get through, even from that address, so a forgotten old script never locks out your updated app.

Deleting a key ​

  • Delete old key now stops the previous key right away instead of waiting for the hour to pass. Use it when the old key leaked.
  • Delete all keys now stops every key on your account at once. Your apps get "invalid key" errors until you create a new key.

Your first request ​

Every request is sent to https://snowstormbot.net/api/v1/ and carries your key in a header (an extra line of information sent along with the request) called Authorization:

http
Authorization: Bearer ssb_your_key_here

The easiest first request is GET /api/v1/me. It answers with your plan, your limits and how much you used today, so it tells you straight away whether your key works.

bash
curl https://snowstormbot.net/api/v1/me \
  -H "Authorization: Bearer ssb_your_key_here"
js
const response = await fetch("https://snowstormbot.net/api/v1/me", {
  headers: { Authorization: `Bearer ${process.env.SNOWSTORM_API_KEY}` },
});
console.log(await response.json());
python
import os, requests

response = requests.get(
    "https://snowstormbot.net/api/v1/me",
    headers={"Authorization": f"Bearer {os.environ['SNOWSTORM_API_KEY']}"},
)
print(response.json())
csharp
using var http = new HttpClient { BaseAddress = new Uri("https://snowstormbot.net/") };
http.DefaultRequestHeaders.Authorization =
    new("Bearer", Environment.GetEnvironmentVariable("SNOWSTORM_API_KEY"));
Console.WriteLine(await http.GetStringAsync("api/v1/me"));

The full list of addresses, with every field explained and a "try it" button, is in the API reference.

What you can do ​

WhatRequest
Your key, limits and usageGET /api/v1/me
Your serversGET /api/v1/guilds
A server's rolesGET /api/v1/guilds/{guildId}/roles
All members of a serverGET /api/v1/guilds/{guildId}/members
One memberGET /api/v1/guilds/{guildId}/members/{userId}
Is this user a member?GET /api/v1/guilds/{guildId}/members/{userId}/membership
Give and take rolesPATCH /api/v1/guilds/{guildId}/members/{userId}/roles
Set or clear a nicknamePUT /api/v1/guilds/{guildId}/members/{userId}/nickname
List API triggered automationsGET /api/v1/guilds/{guildId}/automations
Run an API triggered automationPOST /api/v1/guilds/{guildId}/automations/{automationId}/trigger
List ticket categoriesGET /api/v1/guilds/{guildId}/ticket-categories
List ticketsGET /api/v1/guilds/{guildId}/tickets
One ticket with its historyGET /api/v1/guilds/{guildId}/tickets/{ticketId}
A ticket's messagesGET /api/v1/guilds/{guildId}/tickets/{ticketId}/transcript
Open a ticket for a memberPOST /api/v1/guilds/{guildId}/tickets
Close a ticketPOST /api/v1/guilds/{guildId}/tickets/{ticketId}/close
Reopen a ticketPOST /api/v1/guilds/{guildId}/tickets/{ticketId}/reopen
Check a change that took a whileGET /api/v1/operations/{operationId}

guildId is your Discord server's id and userId is a Discord user's id. In Discord you can copy these by turning on Developer Mode (User Settings, Advanced) and right clicking a server or a person.

Ids are sent as text

Discord ids are very long numbers, too long for some programming languages to store safely. That is why the API always sends and expects them as text, like "123456789012345678", never as a plain number.

Member information ​

Member details come from the bot's own records. The bot keeps them up to date as things happen in your server, and refreshes every member once a week as a safety net. Each member has a cachedAt time that tells you when that record was last refreshed. The API never asks Discord directly when you read, which keeps it fast and keeps your limits generous.

The member list is split into pages so a big server does not arrive in one giant answer. Ask for a page with ?limit=100, and when the answer has a nextAfter value, ask for the next page with ?after= followed by that value. When nextAfter is empty you have everything.

Changing roles and nicknames ​

To change roles, send the role ids to give in add and the role ids to take in remove:

json
{ "add": ["111111111111111111"], "remove": ["222222222222222222"] }

A few rules come from Discord itself:

  • The bot can only give or take roles that sit below its own highest role in your server's role list. GET /roles shows assignable: true for roles it can manage.
  • Roles managed by an integration (bot roles, the booster role) can never be given by hand.
  • Nobody can change the server owner's nickname, not even a bot.

To change a nickname, send { "nickname": "New name" }. Sending null, an empty text or only spaces clears the nickname, so the member's normal name shows again.

A role change through the API counts just like a change made by hand, so automations that react to "Role assigned" or "Role removed" run for it too.

The API checks these rules before anything happens and tells you exactly which role is the problem. All roles in one request are changed together in a single step: either every change goes through, or none of them do, so a member is never left half changed. Giving a role the member already has, or taking one they do not have, is fine.

When a change takes a while ​

Changes are carried out by the bot itself. Usually that takes a second and you get the answer straight away with status succeeded. For a ticket change the answer also has a resourceId: the id of the ticket. If the bot is busy and it takes longer than about 10 seconds, you get 202 Accepted with an operationId instead. Ask GET /api/v1/operations/{operationId} a little later to see whether it succeeded, partial or failed. Operations are kept for 7 days.

An automation trigger can also end as partial: the automation ran, but some steps did not work (for example Discord refused a role, or the member's DMs are closed). You still get 200 OK, with status: "partial", the code automation_incomplete, and the reasons in errorMessage. It is a 200 on purpose: retrying would run the steps that did work a second time. The same problems show on the Problems tab of the Automations page.

Tickets ​

These requests work with support tickets. The Tickets feature has to be switched on in the server, and be part of your plan, for every ticket request, reading included. Otherwise you get feature_disabled. That is the same rule as the Tickets pages on the dashboard.

Reading tickets. GET /tickets lists tickets oldest first, in pages, just like the member list. You can narrow it down with ?status=open (or claimed, closed, deleted), ?categoryId= and ?openerId=. GET /tickets/{ticketId} gives one ticket with the people in it, the answers given when it was opened, and its history. Every step in the history has a via field that says how it happened:

  • user: a person did it in Discord or on the dashboard.
  • system: the bot did it by itself, for example closing an idle ticket.
  • automation: an automation did it. automationName says which one, and triggeredVia says whether the automation started from something in Discord or from the API.
  • api: it was done through this API. actorUserId is then the owner of the API key.

GET /tickets/{ticketId}/transcript gives the messages of the ticket, oldest first and in pages. Messages from bots are not stored. The messages stay available after the ticket's channel is deleted.

Opening a ticket. Send the category id (from GET /ticket-categories) and the member the ticket is for:

json
{
  "categoryId": "4f1c2a9be3d84b0f9a6d2c7e5b8a1f03",
  "openerUserId": "123456789012345678",
  "answers": [{ "question": "What do you need help with?", "answer": "My order did not arrive" }]
}

answers is optional. The answers are shown in the ticket's welcome message, and the first one becomes the ticket's subject. Add "giveOpenerAccess": false for a staff-only ticket: the member is named in it, without a ping, but only your support roles can see it. Tickets show this as staffOnly: true. The same limits apply as when a member opens a ticket themselves: your plan's limit of open tickets and the rate limit from your ticket settings. When it worked, the operation's resourceId is the id of the new ticket.

Closing and reopening. POST /tickets/{ticketId}/close closes a ticket. Closing locks it and keeps it as an archive until the "delete after close" timer removes the channel. POST /tickets/{ticketId}/reopen unlocks a closed ticket again, as long as its channel still exists.

Opening, closing and reopening count against the same change limit as role and nickname changes. Everything you do here is recorded in the ticket's history and in the audit log as done through the developer API, credited to you as the key owner.

API triggered automations ​

An automation is a recipe of actions the bot runs (see Automations). Normally something in Discord starts it, like a member joining. With the API triggered trigger, your own program starts it instead.

  1. On the Automations page, create an automation and pick API triggered (your own app) as the event.
  2. Add the actions you want, for example "DM the user" and "Give roles".
  3. Save it. The editor now shows the automation id and a box that says exactly what your program has to send.
  4. Call POST /api/v1/guilds/{guildId}/automations/{automationId}/trigger.

Your program can send these details, all optional:

json
{
  "userId": "123456789012345678",
  "channelId": "234567890123456789",
  "roleId": "345678901234567890",
  "variables": { "orderId": "A-1001", "tier": "Gold" }
}
  • userId is the member the automation acts on: the one who gets the DM, the roles, the kick or a new ticket, and who fills in {user.*} placeholders. It is also needed for Ask a question, because only that member can press the buttons.
  • channelId is used wherever an action posts in "Channel sent by the API call", and fills in {channel.*}.
  • roleId fills in {role.*} placeholders and role conditions.
  • variables are up to 10 pieces of your own text. To show one in a message, open the placeholder menu in the message editor, pick API, Custom variable, and type the name, for example orderId. The message then shows the value you sent as orderId. Double click the chip later to rename it. You can also branch on a variable with an API variable condition in an If / else action. Names may use letters, numbers and underscores (at most 32 characters). Values can be up to 256 characters.

Safety check

If an automation needs something you did not send (for example it DMs the user but you left out userId), nothing runs at all. You get 422 missing_context with a list of what is missing. GET /automations tells your program up front what each automation needs.

When an API triggered automation opens, closes or reopens a ticket, the ticket's history credits you, the key owner, and says the automation was started through the API.

Text you send in variables can never ping people. Any @everyone, @here or user mention in a value is shown as plain text.

Rate limits ​

A rate limit is the maximum number of requests you may send in a certain time. It protects the bot (and Discord) from being overloaded, so everyone keeps a fast service. Your limits come from your plan and count for your whole account, across all your servers. You can see them on the Developer API page and through GET /api/v1/me.

LimitWhat it means
Requests per minuteYour steady pace. Unused requests build up again over time.
Burst sizeHow many requests you can send in one go before the steady pace applies.
Requests per dayA daily maximum. It resets at midnight UTC.
Changes per minuteA separate, lower limit for changes (roles, nicknames and tickets), because Discord limits those too.
Automation triggersA separate limit per minute and per day for running automations.
Requests at the same timeHow many requests may be waiting for an answer at once.
Members per pageThe biggest limit the member list accepts.

Every answer tells you where you stand with these headers:

HeaderMeaning
RateLimit-LimitThe burst size of the limit that applied.
RateLimit-RemainingHow many requests you can still send right now.
RateLimit-ResetSeconds until you are fully topped up again.
X-RateLimit-Daily-RemainingRequests left today.

When you go over a limit you get 429 Too Many Requests with a Retry-After header. It holds the number of seconds to wait before trying again. Good programs read it and pause.

Rotating your key does not reset your limits.

Errors ​

When something goes wrong you get an error answer in a standard format (called "problem details"). It always has a code your program can check, a title and a detail sentence for humans, and a traceId you can send to support.

json
{
  "status": 422,
  "title": "Role cannot be managed",
  "detail": "The bot cannot give or take 'Admin'. ...",
  "code": "role_not_manageable",
  "roleId": "111111111111111111"
}
CodeWhat happened
invalid_keyThe key is missing, wrong, deleted or expired.
too_many_failed_attemptsToo many requests with a wrong key came from your address. Wait a few minutes.
plan_no_api_accessYour plan does not include the API (anymore).
account_suspendedYour account is suspended.
api_disabledThe API is switched off for maintenance. Try again later.
bot_pausedThe bot is paused, so changes are not possible right now. Reading still works.
rate_limited, daily_quota_exceeded, write_rate_limited, trigger_rate_limited, trigger_quota_exceeded, too_many_concurrent_requestsYou went over a limit. Wait for Retry-After.
guild_not_foundThe bot is not in that server, you do not own it, or the API is switched off for it.
member_not_foundThe user is not a member of the server.
role_not_found, channel_not_foundThe id does not exist in that server.
role_not_manageable, member_not_manageableDiscord does not allow the bot to make this change.
automation_not_found, automation_inactiveThe automation does not exist, is not API triggered, or is switched off.
feature_disabledAutomations or Tickets are switched off in that server, or not part of your plan.
ticket_not_found, ticket_category_not_foundThe ticket or ticket category does not exist in that server.
category_unavailableThe ticket category is switched off.
ticket_limit_reachedThe server reached the most open tickets its plan allows.
ticket_rate_limitedThis member opened too many tickets in a short time (the rate limit from your ticket settings).
ticket_not_open, ticket_not_closedThe ticket is already closed, or is not closed yet so it can't be reopened.
ticket_channel_missingThe ticket's channel was deleted, so it can't be reopened.
thread_parent_missingThe category creates threads but has no channel to create them in. Fix it in the ticket settings.
discord_errorDiscord refused the change, for example because the bot is missing a permission.
missing_contextThe automation needs details you did not send.
automation_incompleteThe automation ran but some steps did not work (status partial, see above).
automation_failedThe automation stopped because of an error. The reason is in detail.
validation_failedSomething in your request is not valid. The detail says what.
internal_errorSomething broke on our side. It is logged automatically.

Keeping a server out of reach ​

The API is on for every server you own by default, because creating a key is already a clear choice. To keep a single server out of reach, open that server's Features page and switch Developer API off. The key then answers as if that server does not exist.

If your plan stops including the API, your key stays but every request is refused with plan_no_api_access until you upgrade again. If you hand a server over to someone else, your key stops reaching it right away.