Find us on social media
TicketsSupportButtons7 min read
Ticket and support bot
Ticket system with buttons, transcript generation, categories, staff roles and auto-close inactive tickets.
Ticket system architecture
A ticket system creates temporary private channels where users can talk to staff. It includes: button panel, channel creation, transcript and automatic closing.
Step 1: Ticket panel with buttons
javascript
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require('discord.js');
// Command to create the panel
async function createTicketPanel(channel) {
const embed = new EmbedBuilder()
.setTitle('Support Center')
.setDescription('Click the appropriate button to open a ticket.')
.setColor(0x5865f2)
.addFields(
{ name: 'General Inquiry', value: 'Questions about the service', inline: true },
{ name: 'Bug Report', value: 'Report a technical issue', inline: true },
{ name: 'Billing', value: 'Payment inquiries', inline: true }
);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_general')
.setLabel('General Inquiry')
.setEmoji('π¬')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId('ticket_bug')
.setLabel('Bug Report')
.setEmoji('π')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId('ticket_billing')
.setLabel('Billing')
.setEmoji('π°')
.setStyle(ButtonStyle.Success)
);
await channel.send({ embeds: [embed], components: [row] });
}Step 2: Create the ticket channel
javascript
client.on('interactionCreate', async interaction => {
if (!interaction.isButton()) return;
if (!interaction.customId.startsWith('ticket_')) return;
const category = interaction.customId.replace('ticket_', '');
const guild = interaction.guild;
// Check if user already has an open ticket
const existing = guild.channels.cache.find(
c => c.topic === `ticket-${interaction.user.id}`
);
if (existing) {
return interaction.reply({
content: `You already have an open ticket: ${existing}`,
ephemeral: true
});
}
// Create channel
const ticketChannel = await guild.channels.create({
name: `ticket-${interaction.user.username}`,
topic: `ticket-${interaction.user.id}`,
parent: process.env.TICKETS_CATEGORY_ID,
permissionOverwrites: [
{ id: guild.id, deny: ['ViewChannel'] },
{ id: interaction.user.id, allow: ['ViewChannel', 'SendMessages'] },
{ id: process.env.STAFF_ROLE_ID, allow: ['ViewChannel', 'SendMessages'] }
]
});
const welcomeEmbed = new EmbedBuilder()
.setTitle(`Ticket - ${category}`)
.setDescription(`Hello ${interaction.user}, a staff member will assist you shortly.\nPlease describe your issue in as much detail as possible.`)
.setColor(0x5865f2)
.setTimestamp();
const closeRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_close')
.setLabel('Close Ticket')
.setEmoji('π')
.setStyle(ButtonStyle.Danger)
);
await ticketChannel.send({ embeds: [welcomeEmbed], components: [closeRow] });
await interaction.reply({ content: `Ticket created: ${ticketChannel}`, ephemeral: true });
});Step 3: Close ticket and generate transcript
javascript
async function closeTicket(channel, closedBy) {
// Generate transcript
const messages = await channel.messages.fetch({ limit: 100 });
const transcript = messages.reverse().map(m =>
`[${m.createdAt.toISOString()}] ${m.author.tag}: ${m.content}`
).join('\n');
// Save transcript
const fs = require('fs');
const filename = `transcript-${channel.name}-${Date.now()}.txt`;
fs.writeFileSync(`./transcripts/${filename}`, transcript);
// Send to log channel
const logChannel = channel.guild.channels.cache.find(c => c.name === 'ticket-logs');
if (logChannel) {
await logChannel.send({
content: `Ticket closed by ${closedBy.tag}`,
files: [`./transcripts/${filename}`]
});
}
// Delete the channel after 5 seconds
await channel.send('Ticket closed. This channel will be deleted in 5 seconds.');
setTimeout(() => channel.delete(), 5000);
}Step 4: Auto-close inactive tickets
javascript
// Check every hour
setInterval(async () => {
const ticketCategory = client.channels.cache.get(process.env.TICKETS_CATEGORY_ID);
if (!ticketCategory) return;
for (const [, channel] of ticketCategory.children.cache) {
const lastMessage = (await channel.messages.fetch({ limit: 1 })).first();
if (!lastMessage) continue;
const hoursSinceLastMessage = (Date.now() - lastMessage.createdTimestamp) / 3600000;
if (hoursSinceLastMessage > 48) {
await channel.send('This ticket will be closed due to inactivity in 1 hour. Send a message to keep it open.');
// Schedule close in 1 hour if no response
}
}
}, 3600000);Recommendations
- Limit to 1-2 open tickets per user
- Store transcripts in a database for later searching
- Add a rating system when closing tickets
- Use threads instead of channels if you prefer less clutter
- Notify staff with a ping when a new ticket is opened
Was this guide helpful?