How to Make a Discord Ticket Bot in Python (discord.py)
A ticket bot puts a button in a channel: whoever clicks it gets a private channel that only they and your team can see, and it's deleted when closed. It's what nearly every community Discord server uses for support, reports or staff applications. Here you build it yourself in Python, in a single file, with no ads and no paid features.
What this bot does
An admin types /ticket-panel in the channel they want and the bot posts a message with the “Open ticket” button. When someone clicks it, the bot creates a ticket-name channel inside the Tickets category, visible only to that person, your team's role and the bot itself. Inside there's a “Close ticket” button that deletes the channel after 5 seconds.
Each person can only have one ticket open: if they click again, the bot shows them the one they already have. And the buttons keep working even if the bot restarts, which is exactly what breaks in most of the examples going around.
Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.
1. Set up the bot and your Discord server
Create the application and get the token
Go to discord.com/developers/applications, “New Application”; in the “Bot” tab, “Reset Token” and copy it. If this is your first bot, the Python Discord bot guide explains it more slowly.
Invite it with just the permissions it needs
In “OAuth2” → “URL Generator” tick “bot” and “applications.commands” and, under permissions, “Manage Channels”, “View Channels”, “Send Messages”, “Embed Links”, “Attach Files” and “Read Message History”. Without “Manage Channels” it can't create or delete the ticket channels.
Create the team role
On your Discord server, create a role called Support (or change the name in the code) and give it to whoever will handle tickets. Admins see every channel anyway. So the role gets pinged on every new ticket, turn on “Allow anyone to @mention this role” in its settings.
2. The code
Three files in one folder: requirements.txt with the libraries, .env with the token (no quotes) and app.py with the bot. The category name and the role name are set at the top of app.py.
discord.py
python-dotenvDISCORD_TOKEN=paste-your-token-hereimport asyncio
import os
import discord
from discord import app_commands
from dotenv import load_dotenv
load_dotenv() # reads the token from the .env file
CATEGORY = "Tickets" # the category they open in (created automatically)
STAFF_ROLE = "Support" # the role that handles tickets: create it on your server
class Close(discord.ui.View):
def __init__(self):
super().__init__(timeout=None) # never expires: keeps working after the bot restarts
@discord.ui.button(label="Close ticket", emoji="🔒", style=discord.ButtonStyle.danger, custom_id="ticket:close")
async def close(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message("The ticket closes in 5 seconds.")
await asyncio.sleep(5)
await interaction.channel.delete(reason=f"Ticket closed by {interaction.user}")
class Open(discord.ui.View):
def __init__(self):
super().__init__(timeout=None)
@discord.ui.button(label="Open ticket", emoji="🎫", style=discord.ButtonStyle.primary, custom_id="ticket:open")
async def open(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.defer(ephemeral=True, thinking=True)
guild = interaction.guild
category = discord.utils.get(guild.categories, name=CATEGORY) or await guild.create_category(CATEGORY)
# One per person: the channel topic stores the id of whoever opened it.
for channel in category.text_channels:
if channel.topic == str(interaction.user.id):
await interaction.followup.send(f"You already have one open: {channel.mention}", ephemeral=True)
return
overwrites = {
guild.default_role: discord.PermissionOverwrite(view_channel=False),
interaction.user: discord.PermissionOverwrite(view_channel=True, send_messages=True, attach_files=True),
guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True),
}
staff = discord.utils.get(guild.roles, name=STAFF_ROLE)
if staff:
overwrites[staff] = discord.PermissionOverwrite(view_channel=True, send_messages=True, attach_files=True)
channel = await guild.create_text_channel(
f"ticket-{interaction.user.name}", category=category, overwrites=overwrites, topic=str(interaction.user.id)
)
ping = staff.mention if staff else ""
await channel.send(f"{interaction.user.mention} {ping} Tell us what's going on. Once it's sorted, click the button.", view=Close())
await interaction.followup.send(f"Ticket opened: {channel.mention}", ephemeral=True)
class Bot(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
self.add_view(Open()) # listens again to the buttons already posted
self.add_view(Close())
await self.tree.sync() # publishes /ticket-panel
bot = Bot()
@bot.tree.command(name="ticket-panel", description="Posts the button to open tickets in this channel")
@app_commands.default_permissions(manage_guild=True)
@app_commands.guild_only()
async def ticket_panel(interaction: discord.Interaction):
embed = discord.Embed(
title="Need help?",
description="Click the button and a private channel between you and the team will open.",
color=0x5B4AE8,
)
await interaction.channel.send(embed=embed, view=Open())
await interaction.response.send_message("Done: the ticket panel is posted.", ephemeral=True)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
bot.run(os.environ["DISCORD_TOKEN"])3. Try it
With Python 3.10 or newer, in the folder: “pip install -r requirements.txt” and “python app.py”. When it says “Logged in as…”, go to the channel where you want the button (for example, #support) and type /ticket-panel. Only people who can manage the server see the command; the first time it can take a few minutes to show up.
Click “Open ticket” with an account that has no role: a new channel should appear in the Tickets category that this account can see and the rest can't. If you get a permissions error (Missing Permissions in the console), the bot is missing “Manage Channels” or one of the permissions it gives itself in the channel.
Taking it further
Several ticket types: instead of one button, add several (Support, Reports, Staff application) each with its own custom_id, or a dropdown menu (discord.ui.Select), and use a different category or role for each.
Saving the conversation before deleting: go through the messages with channel.history(oldest_first=True), write them to a .txt and send it to a log channel with discord.File. To read the text of the messages, the bot needs the “Message Content” intent: turn it on in the portal's “Bot” tab and add intents.message_content = True in the code.
Only the team can close: in close(), check whether interaction.user has the role before deleting the channel and, if not, reply with a message only they can see (ephemeral=True).
4. Keep it online all the time (for free)
A ticket bot has to be connected when someone clicks the button: if it runs on your computer and you turn it off, nobody can open tickets. To keep it online all the time it has to be hosted on a server.
On NapNodes: create the free bot, switch it to Python in “Settings” → “Language” (it starts out on Node.js), upload the three files in “Files” (or set your GitHub repository, without the .env) and click “Start”. The libraries in requirements.txt install themselves at startup and app.py is the startup file that comes set.
The free bot has 256 MB, more than enough for this bot even if your Discord server is big, and you only have to renew it with one click every week. It doesn't need any open port: it's the bot that connects to Discord.
Frequently asked questions
How do I make a ticket system on Discord?
With a bot that posts a button and, when clicked, creates a private channel between that person and your team. With discord.py it's about 90 lines in a single file, and you can keep it online for free.
Why does my bot's button say the interaction failed?
Because the bot is offline or, after restarting, doesn't know what to do with that button. Views need timeout=None, a fixed custom_id on each button, and to be registered with add_view at startup.
What permissions does a ticket bot need?
“Manage Channels” to create and delete the channels, plus “View Channels”, “Send Messages”, “Embed Links”, “Attach Files” and “Read Message History” to write in them.
Can I save the ticket conversation before closing it?
Yes: go through channel.history(), write it to a .txt and send it to a log channel. To read the text of the messages, turn on the “Message Content” intent in the Developer Portal.
Shall we set it up for you?
Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.
Keep reading
- Discord Role Bot: Buttons So Everyone Picks Their Own Roles (Python and JavaScript)
Make your own Discord self-role bot in Python or JavaScript: a message with buttons to add and remove roles, no database and no reaction roles needed.
- Discord Verification Bot in Python: a Button and a Question Before Joining
Build your own Discord verification bot with discord.py: a “Verify me” button, an anti-bot question and a role that unlocks the server. Full code, free.
- Discord Welcome Bot: Join Message and Auto Role (Python and JavaScript)
Build a Discord bot that greets new members and gives them a role automatically, in Python or JavaScript: the missing intent, role order and rules screening.