Automate Discord support tickets by calling POST /guilds/{guild.id}/channels with a permission_overwrites array to create a private channel visible only to the user and support staff. Send a welcome message via POST /channels/{channel.id}/messages, then DELETE /channels/{channel.id} to close the ticket. The critical detail is setting the deny bitmask correctly on @everyone — one wrong bit makes the channel public.
API Quick Reference
Bot Token
50 requests/second (global)
JSON
Available
Understanding the Discord API
Discord's REST API v10 (base URL https://discord.com/api/v10) handles all server management operations including channel creation. Channels in Discord support granular permission overwrites — per-role or per-user rules that override the server-level defaults. This is the mechanism that makes private ticket channels possible.
A ticket channel is a standard text channel created with a permission_overwrites array that denies the @everyone role from viewing the channel (VIEW_CHANNEL permission bit), while explicitly allowing the specific user and a support role. The bot can create this channel, post a welcome message, and later delete it to close the ticket — all through REST API calls.
The trickiest part is the permission bitmask arithmetic. Discord permissions are 64-bit integers represented as strings. VIEW_CHANNEL is bit 10 (value 1024). Setting it in deny for @everyone and allow for the user and support role creates the private channel. Getting one bit wrong silently makes the channel either invisible to your own staff or visible to everyone. Official docs: https://discord.com/developers/docs/resources/channel
https://discord.com/api/v10Setting Up Discord API Authentication
Bot tokens authenticate all requests with a static credential that does not expire until reset. Use Authorization: Bot <token> in every request header. For ticket systems, the bot needs Manage Channels to create and delete channels, and Manage Roles to set permission overwrites on channels.
- 1Go to https://discord.com/developers/applications and click New Application.
- 2Open the Bot tab. Click Reset Token to generate your token — copy it immediately.
- 3No privileged intents are needed for a slash-command-based ticket system.
- 4Open OAuth2 > URL Generator. Select scopes: bot and applications.commands.
- 5Under Bot Permissions, check: Manage Channels, Manage Roles, Send Messages, Read Message History, Add Reactions.
- 6Copy the generated URL and invite the bot to your server.
- 7Create a category in your server where ticket channels will be created — note its category ID from the channel URL.
- 8Store DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, DISCORD_SUPPORT_ROLE_ID, and DISCORD_TICKET_CATEGORY_ID in environment variables.
1import os2import requests34BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]5BASE_URL = "https://discord.com/api/v10"67headers = {8 "Authorization": f"Bot {BOT_TOKEN}",9 "Content-Type": "application/json"10}1112# Verify credentials13resp = requests.get(f"{BASE_URL}/users/@me", headers=headers)14resp.raise_for_status()15print(f"Bot authenticated: {resp.json()['username']}")Security notes
- •Store the bot token in environment variables — never hardcode it or commit it to version control.
- •Request only the permissions your bot actually needs — not Administrator.
- •Validate the interaction token before processing any slash command to prevent replay attacks.
- •Respond to interactions within 3 seconds using deferReply — do channel creation asynchronously to avoid interaction timeout.
- •Log all ticket creation and deletion events with timestamps and user IDs for audit purposes.
Key endpoints
/guilds/{guild.id}/channelsCreates a new text channel in the guild with specified permission overwrites. Returns the full channel object including the new channel ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | required | Channel name — max 100 characters, lowercase, hyphens instead of spaces |
type | number | required | Channel type: 0 = text channel |
parent_id | string | optional | Category ID to place the ticket channel in — highly recommended for organization |
permission_overwrites | array | required | Array of overwrite objects with id (role or user ID), type (0=role, 1=member), allow (bitmask string), deny (bitmask string) |
Request
1{"name":"ticket-johndoe-0042","type":0,"parent_id":"1234567890","topic":"Support ticket for JohnDoe","permission_overwrites":[{"id":"111111111111","type":0,"allow":"0","deny":"1024"},{"id":"222222222222","type":1,"allow":"1024","deny":"0"},{"id":"333333333333","type":0,"allow":"1024","deny":"0"}]}Response
1{"id":"987654321098","type":0,"name":"ticket-johndoe-0042","guild_id":"555555555555","parent_id":"1234567890","position":42,"permission_overwrites":[{"id":"111111111111","type":0,"allow":"0","deny":"1024"},{"id":"222222222222","type":1,"allow":"1024","deny":"0"}]}/channels/{channel.id}/messagesSends a message to a channel. Use this to post the ticket welcome message with instructions and a close button after creating the channel.
| Parameter | Type | Required | Description |
|---|---|---|---|
content | string | optional | The message text — mention the user to ping them in the new channel |
embeds | array | optional | Array of embed objects for rich ticket info display |
Request
1{"content":"<@222222222222> Your ticket has been created. Support staff will be with you shortly.","embeds":[{"title":"Support Ticket #0042","description":"Please describe your issue in detail.","color":5814783}]}Response
1{"id":"112233445566","channel_id":"987654321098","author":{"id":"bot_id","username":"SupportBot"},"content":"<@222222222222> Your ticket has been created.","timestamp":"2026-05-07T10:00:00.000Z"}/channels/{channel.id}Deletes the channel to close the ticket. Returns the deleted channel object. Optionally, archive the ticket content before deletion by calling GET /channels/{channel.id}/messages first.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel.id | string | required | The snowflake ID of the ticket channel to delete |
Response
1{"id":"987654321098","type":0,"name":"ticket-johndoe-0042","guild_id":"555555555555"}Step-by-step automation
Register a Slash Command to Open Tickets
Why: Slash commands are the cleanest trigger for ticket creation — no MESSAGE_CONTENT privileged intent needed, and Discord validates the interaction before your bot receives it.
Register a /ticket slash command via the application commands API. When a user runs /ticket, Discord sends an INTERACTION_CREATE event to your bot. You must acknowledge the interaction within 3 seconds using a deferred response, then create the channel asynchronously. Use an ephemeral initial response so only the user sees the 'Creating your ticket...' message.
1# Register the slash command globally (takes up to 1 hour to propagate)2curl -X POST \3 -H "Authorization: Bot $DISCORD_BOT_TOKEN" \4 -H "Content-Type: application/json" \5 -d '{"name":"ticket","description":"Open a support ticket","type":1}' \6 "https://discord.com/api/v10/applications/$APP_ID/commands"Pro tip: Use guild-scoped commands during development for instant updates. Switch to global commands (DELETE /guilds/{id}/commands/{cmd.id}, then POST /applications/{id}/commands) only for production rollout.
Expected result: The /ticket command appears in the server's command list within seconds (guild-scoped) or up to 1 hour (global).
Create the Private Ticket Channel with Permission Overwrites
Why: The permission_overwrites array is what makes the channel private — setting it correctly is the single most error-prone step.
POST to /guilds/{guild.id}/channels with three permission overwrite objects: deny VIEW_CHANNEL (bitmask 1024) for @everyone, allow VIEW_CHANNEL for the requesting user (type 1 = member), and allow VIEW_CHANNEL for the support role (type 0 = role). Place the channel in a dedicated category using parent_id to keep the server organized. Use a ticket counter or user ID suffix to make channel names unique.
1curl -X POST \2 -H "Authorization: Bot $DISCORD_BOT_TOKEN" \3 -H "Content-Type: application/json" \4 -d "{5 \"name\": \"ticket-$USER_ID\",6 \"type\": 0,7 \"parent_id\": \"$CATEGORY_ID\",8 \"permission_overwrites\": [9 {\"id\": \"$GUILD_ID\", \"type\": 0, \"allow\": \"0\", \"deny\": \"1024\"},10 {\"id\": \"$USER_ID\", \"type\": 1, \"allow\": \"3072\", \"deny\": \"0\"},11 {\"id\": \"$SUPPORT_ROLE_ID\", \"type\": 0, \"allow\": \"3072\", \"deny\": \"0\"}12 ]13 }" \14 "https://discord.com/api/v10/guilds/$GUILD_ID/channels"Pro tip: The @everyone role's ID is always the same as the guild ID. Always use the guild ID (not a hardcoded @everyone ID) for the deny overwrite.
Expected result: HTTP 201 with the channel object. The new channel appears in the tickets category, visible only to the requesting user and the support role.
Post a Welcome Message to the Ticket Channel
Why: A welcome message confirms to the user that the ticket was created and tells support staff what the issue is.
After creating the channel, POST a message to the new channel's ID. Mention the user (so they receive a notification) and include an embed with ticket details. Optionally include Block Kit-style components like a Close Ticket button using the components array. This message is the first thing both the user and support staff see.
1curl -X POST \2 -H "Authorization: Bot $DISCORD_BOT_TOKEN" \3 -H "Content-Type: application/json" \4 -d "{5 \"content\": \"<@$USER_ID> Thanks for opening a ticket! Support will be with you shortly.\",6 \"embeds\": [{7 \"title\": \"Support Ticket\",8 \"description\": \"Please describe your issue below.\",9 \"color\": 5814783,10 \"footer\": {\"text\": \"Ticket ID: $TICKET_NUMBER\"}11 }]12 }" \13 "https://discord.com/api/v10/channels/$CHANNEL_ID/messages"Pro tip: Add a /close slash command that triggers DELETE /channels/{channel.id}. Optionally, first archive the conversation by fetching all messages (GET /channels/{id}/messages) and posting a transcript to a private log channel.
Expected result: The welcome message appears in the ticket channel. The user receives a notification ping. Support staff see the channel light up in their sidebar.
Close the Ticket by Deleting the Channel
Why: Deleting the channel is the standard close mechanism — it removes the channel from the sidebar for both the user and staff.
Call DELETE /channels/{channel.id} with the bot token. This permanently removes the channel and all its messages. If you need an audit trail, fetch the message history first and save it to a database or log channel before deletion. Confirm deletion by checking the response — a deleted channel is returned as an object (not 204).
1curl -X DELETE \2 -H "Authorization: Bot $DISCORD_BOT_TOKEN" \3 "https://discord.com/api/v10/channels/$CHANNEL_ID"Pro tip: Before deleting, notify the user: post a final message like 'This ticket will be closed in 10 seconds.' Then sleep 10 seconds before DELETE. This prevents users from losing an in-progress response.
Expected result: HTTP 200 with the deleted channel object. The channel disappears from all members' sidebars immediately.
Complete working code
This script implements a complete ticket lifecycle: slash command registration, ticket creation with correct permission overwrites, welcome message posting, and channel deletion on close. It maintains a ticket counter in memory (replace with a database in production) and handles rate limits with backoff.
1import os, time, random, logging, requests23logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")45BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]6GUILD_ID = os.environ["DISCORD_GUILD_ID"]7SUPPORT_ROLE_ID = os.environ["DISCORD_SUPPORT_ROLE_ID"]8CATEGORY_ID = os.environ["DISCORD_TICKET_CATEGORY_ID"]9BASE = "https://discord.com/api/v10"10HEADERS = {"Authorization": f"Bot {BOT_TOKEN}", "Content-Type": "application/json"}1112ticket_counter = 01314def api_call(method, path, **kwargs):15 for attempt in range(5):16 resp = requests.request(method, f"{BASE}{path}", headers=HEADERS, **kwargs)17 if resp.status_code == 429:18 wait = resp.json().get("retry_after", 1) + random.uniform(0, 0.3)19 logging.warning(f"Rate limited, sleeping {wait:.2f}s")20 time.sleep(wait)21 continue22 return resp23 raise RuntimeError("Max retries exceeded")2425def open_ticket(user_id: str) -> dict:26 global ticket_counter27 ticket_counter += 128 num = ticket_counter2930 # Create private channel31 channel_resp = api_call("POST", f"/guilds/{GUILD_ID}/channels", json={32 "name": f"ticket-{num:04d}",33 "type": 0,34 "parent_id": CATEGORY_ID,35 "permission_overwrites": [36 {"id": GUILD_ID, "type": 0, "allow": "0", "deny": "1024"},37 {"id": user_id, "type": 1, "allow": "3072", "deny": "0"},38 {"id": SUPPORT_ROLE_ID, "type": 0, "allow": "3072", "deny": "0"}39 ]40 })41 channel_resp.raise_for_status()42 channel = channel_resp.json()43 channel_id = channel["id"]44 logging.info(f"Created ticket channel {channel['name']} ({channel_id})")4546 # Post welcome message47 msg_resp = api_call("POST", f"/channels/{channel_id}/messages", json={48 "content": f"<@{user_id}> Your ticket has been created. Support will be with you shortly.",49 "embeds": [{50 "title": f"Support Ticket #{num:04d}",51 "description": "Please describe your issue in detail.",52 "color": 5814783,53 "footer": {"text": "Use /close to close when resolved"}54 }]55 })56 msg_resp.raise_for_status()57 logging.info(f"Welcome message sent to {channel_id}")58 return channel5960def close_ticket(channel_id: str) -> None:61 resp = api_call("DELETE", f"/channels/{channel_id}")62 if resp.status_code in (200, 404):63 logging.info(f"Ticket channel {channel_id} closed")64 else:65 resp.raise_for_status()6667if __name__ == "__main__":68 test_user_id = "123456789012345678"69 channel = open_ticket(test_user_id)70 logging.info(f"Ticket opened: {channel['name']}")71 time.sleep(5)72 close_ticket(channel["id"])73Error handling
{"code": 50013, "message": "Missing Permissions"}The bot lacks Manage Channels permission, or the bot cannot see the category specified in parent_id.
Re-invite the bot with Manage Channels and Manage Roles permissions. Verify the bot has VIEW_CHANNEL access to the ticket category. Check that the category is not locked to a role the bot doesn't have.
Do not retry — fix permissions and redeploy.
{"code": 50035, "message": "Invalid Form Body", "errors": {"permission_overwrites": {"0": {"deny": {"_errors": [{"code": "NUMBER_TYPE_COERCE"}]}}}}}Permission bitmasks must be strings (even though they represent integers). Passing numeric 1024 instead of string "1024" causes this error.
Always pass permission bitmask values as strings: "allow": "1024" not "allow": 1024. Check the errors object in the response for the exact field causing the problem.
Do not retry — fix the payload format.
{"message": "You are being rate limited.", "retry_after": 0.5, "global": false}The channel creation endpoint's per-guild rate limit bucket has been exhausted — typically during raids when many users create tickets simultaneously.
Implement a ticket queue: accept the slash command immediately (return an ephemeral ack), enqueue the ticket creation, and process the queue with rate-limit-aware backoff. This prevents multiple simultaneous channel creation requests.
Sleep for retry_after + jitter seconds. Use a queue for bulk operations.
{"code": 10003, "message": "Unknown Channel"}The category_id (parent_id) does not exist or the bot cannot see it, or a close attempt was made on an already-deleted channel.
Verify the category ID is correct (right-click category > Copy Channel ID in Discord). For close operations, handle 404 as a successful close — the channel is already gone.
For create: do not retry, fix the category ID. For delete: treat 404 as success.
Channel appears visible to all server members despite permission overwritesThe @everyone deny overwrite used a wrong ID (not the guild ID), or the bitmask value is incorrect — e.g., deny: "1" instead of deny: "1024".
The @everyone role ID is always identical to the guild ID. Use the guild ID as the overwrite ID with type: 0 (role). Double-check that the deny bitmask is "1024" (VIEW_CHANNEL bit 10). Test by checking the channel permissions in Discord settings after creation.
Delete the malformed channel and recreate with the correct overwrites.
Rate Limits for Discord API
| Scope | Limit | Window |
|---|---|---|
| Global (per bot) | 50 requests | per second |
| Channel creation (per guild) | Per-route bucket — read X-RateLimit-Remaining | Read X-RateLimit-Reset-After |
| Invalid requests | 10,000 requests returning 401/403/429 | per 10 minutes before Cloudflare ban |
1import time, random23def rate_limited_request(method, url, headers, json_body=None, max_retries=5):4 import requests5 for attempt in range(max_retries):6 resp = requests.request(method, url, headers=headers, json=json_body)7 remaining = int(resp.headers.get('X-RateLimit-Remaining', 1))8 if resp.status_code == 429:9 wait = resp.json().get('retry_after', 1) + random.uniform(0, 0.5)10 time.sleep(wait)11 continue12 if remaining == 0:13 reset_after = float(resp.headers.get('X-RateLimit-Reset-After', 1))14 time.sleep(reset_after)15 return resp16 raise RuntimeError('Max retries exceeded')- Implement a ticket queue during peak hours — do not create channels concurrently from multiple users at the same time.
- Add anti-spam: limit each user to one open ticket at a time by checking if a channel named ticket-{userId} already exists.
- Read X-RateLimit-Remaining on every response and pause when it reaches 0.
- Separate ticket creation (REST) from interaction acknowledgment (interactions endpoint is rate-limit exempt) — always ack the interaction first.
- Store open ticket channel IDs in a database mapped to user IDs to prevent duplicate ticket creation and enable proper close logic.
Security checklist
- Store DISCORD_BOT_TOKEN in environment variables — never hardcode or expose in client-side code.
- Validate interaction tokens before processing slash commands — Discord signs interactions with a public key, verify the signature.
- Deny VIEW_CHANNEL for @everyone using the guild ID as the overwrite target — verify permissions after channel creation by checking the overwrites in the response.
- Limit ticket creation to one active ticket per user — check for existing open tickets before creating a new channel.
- Archive ticket transcripts before deletion for a compliance audit trail — store in a private log channel or database.
- Restrict the /close command to the ticket owner and support staff using role checks before calling DELETE /channels.
- Log all ticket opens and closes with user IDs, timestamps, and ticket numbers for accountability.
- Set a maximum concurrent ticket limit per guild to prevent denial-of-service via bulk ticket creation during raids.
Automation use cases
Button-Triggered Tickets
intermediateA persistent message with a 'Create Ticket' button that users click, triggering a button interaction instead of a slash command.
Department Routing
intermediateUsers select a department (Technical, Billing, General) from a select menu, and the bot creates the channel with the appropriate support team role in the overwrites.
Ticket Transcript Archiving
advancedBefore closing, fetch all channel messages, format them as an HTML or text transcript, and post to a private archive channel or upload to S3.
SLA Timer Integration
advancedTrack time from ticket creation to first staff response, and post warnings to a mod channel if a ticket goes unresponded for more than X minutes.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier availableZapier's Discord integration can send messages to channels but does not support creating channels with custom permission overwrites — not suitable for full ticket systems.
- + Easy setup
- + No code required
- + Good for simple notifications
- - Cannot create channels with permission overwrites
- - Not suitable for full ticket lifecycle
- - Expensive at scale
Make (formerly Integromat)
Free tier available (1,000 operations/month)Make has a more powerful Discord module that can make custom API calls, making full ticket channel creation possible with the right scenario setup.
- + Supports custom HTTP requests to Discord API
- + Visual workflow builder
- + More affordable than Zapier
- - Complex setup for permission overwrites
- - Requires understanding of Discord API schema
- - Execution time limits
n8n
Free self-hosted; Cloud from €20/monthn8n's HTTP Request node gives full access to the Discord API, enabling complete ticket automation including channel creation, messaging, and deletion.
- + Full Discord API access via HTTP node
- + Self-hostable
- + No per-execution pricing when self-hosted
- - Requires technical setup
- - No visual Discord-specific UI
- - Self-hosting requires server management
Best practices
- Always use the guild ID (not a hardcoded string) as the @everyone overwrite ID — they are identical by design.
- Pass permission bitmasks as strings, not integers — the Discord API is strict about this and will return a 400 with a cryptic error otherwise.
- Ack the slash command interaction immediately with deferReply (or a 202 Accepted) before creating the channel — channel creation can take a few hundred milliseconds and will miss the 3-second ack deadline.
- Store the ticket channel ID and creator user ID in a database — this enables lookup for /close commands and prevents users from closing others' tickets.
- Test permission overwrites by logging in as a non-support user and verifying the channel is invisible — it's the most common setup mistake.
- Set a ticket category channel to have explicit bot permissions so that inheriting from the parent category doesn't silently block the bot.
- Implement an auto-close mechanism: after 48 hours of inactivity (no messages), send a warning and close after another 24 hours.
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Discord ticket bot using the REST API v10. My bot creates private channels using POST /guilds/{guild.id}/channels with permission_overwrites. Help me: 1) calculate the correct permission bitmasks for VIEW_CHANNEL and SEND_MESSAGES, 2) build the permission_overwrites array that denies @everyone (using guild ID) and allows a specific user and support role, 3) handle the slash command interaction with a 3-second ack, and 4) implement a /close command that archives the chat history before deleting the channel. Use Python with the requests library.
Build a web dashboard for managing a Discord support ticket system. Features: live ticket list showing all open channels with user, creation time, and status; ticket detail view showing the conversation transcript via Discord API; assign ticket to support agent button; close ticket button that calls DELETE /channels; ticket analytics showing average response time and daily ticket volume; and a configuration panel for setting the support role ID and ticket category ID.
Frequently asked questions
Is the Discord API free for bots?
Yes. The Discord REST API is free to use with no per-request pricing. Hosting costs for your bot server are your only expense. There are no paid tiers for API access.
What happens when I hit the rate limit during a ticket creation spike?
You receive HTTP 429 with retry_after in the JSON body (in seconds). Implement a ticket queue: accept the interaction immediately, enqueue the creation, and process the queue with backoff. During raids, concurrent ticket requests will hit the per-guild channel creation bucket quickly — a queue prevents this.
Why is my ticket channel visible to all members despite my permission overwrites?
The most common cause is using the wrong ID for the @everyone deny overwrite. The @everyone role ID is always equal to the guild ID — not a literal string. The second common cause is passing permission bitmasks as integers instead of strings. Use {"deny": "1024"} not {"deny": 1024}.
Can I archive the ticket transcript before deleting the channel?
Yes. Call GET /channels/{channel.id}/messages?limit=100 repeatedly (paginating with before parameter) to fetch all messages, then format them as a log. Post the transcript to a private archive channel or save to a database before calling DELETE /channels/{channel.id}.
Do I need the MESSAGE_CONTENT privileged intent for a ticket bot?
No. A slash command-based ticket bot doesn't read message content — it only creates channels and posts messages. MESSAGE_CONTENT is only required if your bot reads the actual text content of messages it didn't author. Avoid it to stay off the intent-verification track.
How do I prevent users from creating duplicate tickets?
Store a mapping of {user_id: channel_id} in a database when creating tickets. On each /ticket command, check if the user already has an open ticket and redirect them to the existing channel with a message like 'You already have an open ticket in <#channel_id>.' Clear the mapping when the ticket is closed.
Can RapidDev help build a custom Discord ticket system?
Yes. RapidDev has built 600+ apps including full Discord support bots with ticket routing, SLA tracking, and CRM integration. Visit rapidevelopers.com for a free consultation.
Can I use webhooks instead of a bot for ticket systems?
Partially. Discord webhooks can post messages but cannot create channels, set permission overwrites, or receive slash commands. A bot (with a Bot Token and Gateway connection) is required for full ticket lifecycle management.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation