Ticket bot (Python)

Opens a private support channel for each member who runs /ticket, with a button to close it.

Python 3.13

Environment variables you'll need

KeyWhat it is
DISCORD_TOKENYour bot's token from the Discord Developer Portal. Paste it on the Secrets tab; we store it encrypted.
TICKET_CATEGORY_IDOptional. Category the ticket channels are created in
SUPPORT_ROLE_IDOptional. Role that can see and close every ticket

Create an account to use it

Files

README.md

# Ticket bot

A Python bot built on discord.py. Your bot token is set on the Secrets tab as
DISCORD_TOKEN and reaches the bot as an environment variable.

## Commands

- `/ticket [reason]` opens a private channel only you, the bot and the support role can see. One open ticket per member.
- The Close button (or `/close` inside the ticket) deletes the channel.

## Setup

1. No privileged gateway intents are needed. Leave Message Content, Server Members
   and Presence off in the Discord Developer Portal.
2. Invite the bot with the `bot` and `applications.commands` scopes and these
   permissions: View Channels, Manage Channels, Send Messages, Read Message History.
3. Set these values on the Secrets tab. The ones marked optional can stay empty:

- `TICKET_CATEGORY_ID`: Optional. Category the ticket channels are created in.
- `SUPPORT_ROLE_ID`: Optional. Role that can see and close every ticket.

4. Press Deploy. The console shows a line with "Logged in as" once the bot is online.

main.py

import os

import discord
from discord import app_commands


def env_id(name):
    value = os.environ.get(name, "").strip()
    return int(value) if value.isdigit() else None


CATEGORY_ID = env_id("TICKET_CATEGORY_ID")
SUPPORT_ROLE_ID = env_id("SUPPORT_ROLE_ID")


def ticket_name(user_id):
    return f"ticket-{user_id}"


def can_close(interaction):
    channel = interaction.channel
    member = interaction.user
    if channel.name == ticket_name(member.id):
        return True
    if SUPPORT_ROLE_ID and any(r.id == SUPPORT_ROLE_ID for r in member.roles):
        return True
    return channel.permissions_for(member).manage_channels


async def close_ticket(interaction):
    channel = interaction.channel
    if interaction.guild is None or not channel.name.startswith("ticket-"):
        await interaction.response.send_message(
            "This only works inside a ticket channel.", ephemeral=True)
        return
    if not can_close(interaction):
        await interaction.response.send_message(
            "Only the ticket owner or support staff can close this ticket.", ephemeral=True)
        return
    await interaction.response.send_message("Closing this ticket.")
    await channel.delete(reason=f"Ticket closed by {interaction.user}")


class CloseView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)  # persistent, survives restarts

    @discord.ui.button(label="Close ticket", style=discord.ButtonStyle.danger,
                       custom_id="ticket:close")
    async def close(self, interaction, button):
        await close_ticket(interaction)


class TicketBot(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(CloseView())
        await self.tree.sync()


bot = TicketBot()


@bot.tree.command(name="ticket", description="Open a private support ticket")
@app_commands.describe(reason="What do you need help with?")
@app_commands.guild_only()
async def ticket(interaction: discord.Interaction, reason: str | None = None):
    guild = interaction.guild
    user = interaction.user
    name = ticket_name(user.id)
    existing = discord.utils.get(guild.text_channels, name=name)
    if existing is not None:
        await interaction.response.send_message(
            f"You already have an open ticket: {existing.mention}", ephemeral=True)
        return
    await interaction.response.defer(ephemeral=True, thinking=True)

    overwrites = {
        guild.default_role: discord.PermissionOverwrite(view_channel=False),
        user: discord.PermissionOverwrite(view_channel=True, send_messages=True,
                                          read_message_history=True, attach_files=True),
        guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True,
                                              read_message_history=True,
                                              manage_channels=True),
    }
    support = guild.get_role(SUPPORT_ROLE_ID) if SUPPORT_ROLE_ID else None
    if support is not None:
        overwrites[support] = discord.PermissionOverwrite(
            view_channel=True, send_messages=True, read_message_history=True)
    category = guild.get_channel(CATEGORY_ID) if CATEGORY_ID else None
    if not isinstance(category, discord.CategoryChannel):
        category = None

    channel = await guild.create_text_channel(
        name, overwrites=overwrites, category=category,
        topic=f"Ticket for {user} ({user.id})", reason=f"Ticket opened by {user}")
    text = f"{user.mention} opened a ticket."
    if reason:
        text += f"\nReason: {reason}"
    text += "\nSupport will be with you soon. Press the button or use /close when done."
    await channel.send(text, view=CloseView(),
                       allowed_mentions=discord.AllowedMentions(users=[user]))
    await interaction.followup.send(f"Your ticket is ready: {c

requirements.txt

discord.py