Automate Slack support tickets using conversations.create to spin up a dedicated channel per request, pins.add to pin the original issue, and chat.postMessage for Block Kit interactive controls. The /support slash command must be acknowledged within 3 seconds — use response_url (valid 30 minutes, max 5 follow-ups) for async processing. conversations.create is Tier 2 (~20 requests/minute).
API Quick Reference
Bot Token (xoxb-)
conversations.create ~20/min; chat.postMessage 1 msg/sec/channel
JSON
Available
Understanding the Slack API for Support Ticket Automation
The Slack Web API is a REST interface accepting POST requests to https://slack.com/api/<method> with a Bot Token in the Authorization header. All responses return HTTP 200 — application errors appear as {"ok": false, "error": "<code>"} in the body. For support ticket systems, the key methods are conversations.create (creates a new channel), conversations.invite (adds the requester and support team), pins.add (pins the original request for reference), and chat.postMessage (posts interactive Block Kit messages with claim/escalate/close buttons).
The biggest gotcha with slash command-based ticket systems is the 3-second acknowledgement deadline. Slack expects your endpoint to respond within 3 seconds of receiving a slash command payload. If you need to do heavy work (channel creation, database writes, multiple API calls), you must respond immediately with a 200 and an ephemeral confirmation, then use the response_url from the payload to post follow-up messages asynchronously. The response_url stays valid for 30 minutes and accepts up to 5 payloads.
For interactive buttons (claim, escalate, close), Slack delivers block_actions payloads to your Interactivity URL — again with a 3-second ack deadline. Using @slack/bolt handles all ack logic automatically, so it is the recommended framework for apps with multiple interaction types. The official documentation is at https://docs.slack.dev/apis/web-api.
https://slack.com/apiSetting Up Slack API Authentication for Support Tickets
Slack uses Bot Tokens (xoxb-) for programmatic workspace actions. The token is issued when you install your app to a workspace via OAuth. It does not expire unless you revoke it or uninstall the app. You also need to configure a Slash Command (with your server's public URL) and optionally an Interactivity Request URL for button callbacks.
- 1Go to api.slack.com/apps and click 'Create New App' → 'From scratch'
- 2Name the app 'SupportBot' and select your workspace
- 3Go to 'OAuth & Permissions' → 'Bot Token Scopes' and add: channels:manage, channels:write, chat:write, pins:write, commands, groups:write (for private channels)
- 4Go to 'Slash Commands' → 'Create New Command', set command to /support, Request URL to https://yourdomain.com/slack/commands
- 5Go to 'Interactivity & Shortcuts' → toggle Interactivity on, set Request URL to https://yourdomain.com/slack/actions
- 6Click 'Install to Workspace' and authorize — copy the 'Bot User OAuth Token' starting with xoxb-
- 7Store the token as SLACK_BOT_TOKEN in your environment variables
- 8Also save SLACK_SIGNING_SECRET from 'Basic Information' → 'App Credentials' for signature verification
1import os2from slack_sdk import WebClient34# Initialize client with bot token from environment5client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])67# Verify authentication8try:9 response = client.auth_test()10 print(f"Connected as bot: {response['bot_id']} in workspace: {response['team']}")11except Exception as e:12 print(f"Auth failed: {e}")Security notes
- •Store SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET in environment variables — never hardcode them
- •Verify the X-Slack-Signature header on every incoming request using HMAC-SHA256 before processing
- •Set token_rotation on production apps if your workspace supports it (OAuth apps)
- •Use private channels (is_private: true) for sensitive support conversations so only invited members can access them
- •Log all ticket creation and closure events with user IDs and timestamps for audit purposes
- •Rotate the bot token if it is accidentally committed to version control
Key endpoints
/conversations.createCreates a new public or private Slack channel. For support tickets, create a private channel named ticket-{id} so only the requester and support team have access.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | required | Channel name — lowercase, no spaces. Use a prefix like ticket- followed by an ID or timestamp. |
is_private | boolean | optional | Set to true to create a private channel. Bot must have groups:write scope for private channels. |
Request
1{"name": "ticket-1042", "is_private": true}Response
1{"ok": true, "channel": {"id": "C0987654321", "name": "ticket-1042", "is_private": true, "created": 1716595200}}/conversations.inviteInvites one or more users to an existing channel. Call this after conversations.create to add the ticket requester and the support team.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID returned by conversations.create. |
users | string | required | Comma-separated user IDs to invite. Use the requester's user_id from the slash command payload. |
Request
1{"channel": "C0987654321", "users": "U0123456789,U9876543210"}Response
1{"ok": true, "channel": {"id": "C0987654321", "name": "ticket-1042"}}/pins.addPins a message to a channel so it appears in the channel's Pins section. Pin the ticket creation message so support agents immediately see the original request.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID where the message lives. |
timestamp | string | required | The ts value of the message to pin — returned by chat.postMessage. |
Request
1{"channel": "C0987654321", "timestamp": "1716595210.000100"}Response
1{"ok": true}/chat.postMessagePosts a message to a channel. Use Block Kit with interactive buttons (Claim, Escalate, Close) so support agents can manage tickets without leaving Slack.
| Parameter | Type | Required | Description |
|---|---|---|---|
channel | string | required | Channel ID or user ID (for DMs) to post to. |
blocks | array | optional | Block Kit layout array. Maximum 50 blocks per message. Use for structured interactive ticket cards. |
text | string | optional | Fallback text shown in notifications when blocks cannot render. Always include alongside blocks. |
Request
1{"channel": "C0987654321", "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": "*Ticket #1042*\n_Requester:_ <@U0123456789>\n_Issue:_ Login page returns 500 error"}}, {"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "Claim"}, "action_id": "ticket_claim", "value": "1042"}, {"type": "button", "text": {"type": "plain_text", "text": "Close"}, "action_id": "ticket_close", "value": "1042", "style": "danger"}]}]}Response
1{"ok": true, "channel": "C0987654321", "ts": "1716595210.000100", "message": {"text": ""}}Step-by-step automation
Acknowledge the Slash Command Within 3 Seconds
Why: Slack cancels the request and shows an error to the user if your endpoint does not respond within 3 seconds — regardless of whether your work is finished.
When Slack POSTs to your /slack/commands endpoint, immediately respond with HTTP 200 and a JSON body like {"response_type": "ephemeral", "text": "Creating your support ticket..."} before doing any database writes or API calls. Capture the response_url from the payload — you have up to 30 minutes and 5 POST requests to that URL for follow-up messages. If using @slack/bolt, call ack() at the top of your handler and it handles the response automatically.
1# Test your slash command endpoint locally2curl -X POST https://yourdomain.com/slack/commands \3 -H 'Content-Type: application/x-www-form-urlencoded' \4 -d 'command=/support&text=Login+page+returns+500&user_id=U0123456789&response_url=https://hooks.slack.com/commands/...'Pro tip: If your background task takes longer than 30 minutes or needs more than 5 follow-up messages, post the ticket details to a database-backed webhook or emit a task queue job instead of relying solely on response_url.
Expected result: Slack receives a 200 response within 3 seconds and displays 'Creating your support ticket...' ephemerally to the user who ran /support.
Create a Dedicated Private Ticket Channel
Why: A dedicated channel isolates the conversation, controls who can see it, and gives the ticket a persistent audit trail in Slack.
Call conversations.create with is_private: true and a name like ticket-{id} where id is a timestamp or auto-increment counter. Private channels require the groups:write scope in addition to channels:manage. After creation, immediately call conversations.invite to add the requester (from user_id in the slash command payload) and a support team user group or individual agents. The bot itself is auto-added when it creates the channel.
1# Create private ticket channel2curl -X POST https://slack.com/api/conversations.create \3 -H 'Authorization: Bearer xoxb-your-token' \4 -H 'Content-Type: application/json; charset=utf-8' \5 --data '{"name": "ticket-1042", "is_private": true}'67# Invite the requester and a support agent8curl -X POST https://slack.com/api/conversations.invite \9 -H 'Authorization: Bearer xoxb-your-token' \10 -H 'Content-Type: application/json; charset=utf-8' \11 --data '{"channel": "C0987654321", "users": "U0123456789,U9876543210"}'Pro tip: Use a monotonically increasing integer from a database (e.g., a tickets table) rather than a timestamp for the ticket ID. This makes ticket-1042 easier to reference in email threads and dashboards than ticket-1716595200.
Expected result: A new private channel named ticket-{id} is created. Only the bot, the requester, and invited support agents can see and join it.
Post a Block Kit Ticket Card and Pin It
Why: Pinning the ticket card ensures support agents immediately see the full issue description when they open the channel without scrolling through history.
Call chat.postMessage with a Block Kit layout showing the ticket number, requester mention, issue description, and interactive buttons (Claim, Escalate, Close). Save the ts value from the response — that is the message timestamp needed for pins.add. Then immediately call pins.add with the channel ID and the ts to pin the card at the top of the channel.
1# Post ticket card with action buttons2curl -X POST https://slack.com/api/chat.postMessage \3 -H 'Authorization: Bearer xoxb-your-token' \4 -H 'Content-Type: application/json; charset=utf-8' \5 --data '{6 "channel": "C0987654321",7 "text": "Support Ticket #1042",8 "blocks": [9 {"type": "section", "text": {"type": "mrkdwn", "text": "*Support Ticket #1042*\n*From:* <@U0123456789>\n*Issue:* Login page returns 500 error"}},10 {"type": "divider"},11 {"type": "actions", "elements": [12 {"type": "button", "text": {"type": "plain_text", "text": "Claim"}, "action_id": "ticket_claim", "value": "1042"},13 {"type": "button", "text": {"type": "plain_text", "text": "Escalate"}, "action_id": "ticket_escalate", "value": "1042", "style": "primary"},14 {"type": "button", "text": {"type": "plain_text", "text": "Close"}, "action_id": "ticket_close", "value": "1042", "style": "danger"}15 ]}16 ]17 }'1819# Pin the message using the ts from the response20curl -X POST https://slack.com/api/pins.add \21 -H 'Authorization: Bearer xoxb-your-token' \22 -H 'Content-Type: application/json; charset=utf-8' \23 --data '{"channel": "C0987654321", "timestamp": "1716595210.000100"}'Pro tip: Save the channel_id and msg_ts to a database keyed by ticket_id. You will need them later for chat.update (when claiming updates the status line in the card) and conversations.archive (when closing the ticket).
Expected result: A formatted ticket card with Claim/Escalate/Close buttons appears in the channel and is immediately pinned. Support agents see the ticket details as the first pinned item.
Handle Button Interactions to Update Ticket Status
Why: Interactive buttons let support agents claim and close tickets without leaving Slack, eliminating context-switching to an external tool.
When an agent clicks Claim, Escalate, or Close, Slack posts a block_actions payload to your Interactivity URL. Extract the action_id and value (ticket_id) from the payload. For Claim, call chat.update to replace the ticket card with an updated version showing 'Claimed by @agent'. For Close, archive the channel with conversations.archive. Always ack() the interaction within 3 seconds before doing API work.
1# Update the ticket card after claiming (replace ts with actual message timestamp)2curl -X POST https://slack.com/api/chat.update \3 -H 'Authorization: Bearer xoxb-your-token' \4 -H 'Content-Type: application/json; charset=utf-8' \5 --data '{6 "channel": "C0987654321",7 "ts": "1716595210.000100",8 "text": "Support Ticket #1042 — Claimed",9 "blocks": [10 {"type": "section", "text": {"type": "mrkdwn", "text": "*Support Ticket #1042*\n*Status:* :green_circle: Claimed by <@U9876543210>"}}11 ]12 }'1314# Archive channel when ticket is closed15curl -X POST https://slack.com/api/conversations.archive \16 -H 'Authorization: Bearer xoxb-your-token' \17 -H 'Content-Type: application/json; charset=utf-8' \18 --data '{"channel": "C0987654321"}'Pro tip: Before archiving, post a summary message with the resolution (e.g., 'Root cause: DNS misconfiguration. Fixed by updating CNAME record.'). Archived channels are searchable and serve as a knowledge base for recurring issues.
Expected result: Clicking Claim updates the pinned card to show the agent's name and claimed status. Clicking Close posts a closure notice and archives the channel so it no longer appears in the sidebar.
Complete working code
This complete script implements a Slack support ticket system: a /support slash command creates a private channel, invites the requester and support team, posts an interactive Block Kit card with Claim/Escalate/Close buttons, and pins it. Claim updates the card status; Close archives the channel. Uses @slack/bolt for automatic ack handling.
1import os2import time3import logging4import threading5from flask import Flask, request, jsonify6from slack_sdk import WebClient7from slack_bolt import App8from slack_bolt.adapter.flask import SlackRequestHandler910logging.basicConfig(level=logging.INFO)11logger = logging.getLogger(__name__)1213bolt_app = App(14 token=os.environ['SLACK_BOT_TOKEN'],15 signing_secret=os.environ['SLACK_SIGNING_SECRET']16)17client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])18SUPPORT_AGENT_ID = os.environ.get('SUPPORT_AGENT_ID', 'U_SUPPORT')19handler = SlackRequestHandler(bolt_app)20flask_app = Flask(__name__)2122def build_ticket_blocks(ticket_id, user_id, description, status='Open'):23 return [24 {25 'type': 'section',26 'text': {'type': 'mrkdwn',27 'text': f'*Support Ticket #{ticket_id}*\n*From:* <@{user_id}>\n*Issue:* {description}\n*Status:* {status}'}28 },29 {'type': 'divider'},30 {31 'type': 'actions',32 'elements': [33 {'type': 'button', 'text': {'type': 'plain_text', 'text': 'Claim'},34 'action_id': 'ticket_claim', 'value': str(ticket_id)},35 {'type': 'button', 'text': {'type': 'plain_text', 'text': 'Escalate'},36 'action_id': 'ticket_escalate', 'value': str(ticket_id), 'style': 'primary'},37 {'type': 'button', 'text': {'type': 'plain_text', 'text': 'Close'},38 'action_id': 'ticket_close', 'value': str(ticket_id), 'style': 'danger'}39 ]40 }41 ]4243@bolt_app.command('/support')44def handle_support_command(ack, command, respond):45 ack() # Ack within 3 seconds46 user_id = command['user_id']47 description = command.get('text') or 'No description provided'48 threading.Thread(49 target=_create_ticket_bg,50 args=(user_id, description, respond)51 ).start()5253def _create_ticket_bg(user_id, description, respond):54 try:55 ticket_id = int(time.time())56 # Create private channel57 channel = client.conversations_create(58 name=f'ticket-{ticket_id}', is_private=True59 )['channel']60 channel_id = channel['id']61 # Invite requester and support62 client.conversations_invite(63 channel=channel_id, users=f'{user_id},{SUPPORT_AGENT_ID}'64 )65 # Post and pin ticket card66 post = client.chat_postMessage(67 channel=channel_id,68 text=f'Support Ticket #{ticket_id}',69 blocks=build_ticket_blocks(ticket_id, user_id, description)70 )71 client.pins_add(channel=channel_id, timestamp=post['ts'])72 respond({'response_type': 'ephemeral',73 'text': f'Ticket #{ticket_id} created! See <#{channel_id}>'})74 logger.info(f'Ticket {ticket_id} created in {channel_id}')75 except Exception as e:76 logger.error(f'Ticket creation failed: {e}')77 respond({'response_type': 'ephemeral', 'text': f'Error: {str(e)}'})7879@bolt_app.action('ticket_claim')80def handle_claim(ack, body, client):81 ack()82 tid = body['actions'][0]['value']83 agent = body['user']['id']84 client.chat_update(85 channel=body['channel']['id'], ts=body['message']['ts'],86 text=f'Ticket #{tid} — Claimed',87 blocks=[{'type': 'section', 'text': {'type': 'mrkdwn',88 'text': f'*Ticket #{tid}* :green_circle: Claimed by <@{agent}>'}}]89 )9091@bolt_app.action('ticket_close')92def handle_close(ack, body, client):93 ack()94 cid = body['channel']['id']95 tid = body['actions'][0]['value']96 agent = body['user']['id']97 client.chat_postMessage(98 channel=cid, text=f':white_check_mark: Ticket #{tid} closed by <@{agent}>.'99 )100 client.conversations_archive(channel=cid)101102@flask_app.route('/slack/events', methods=['POST'])103def slack_events():104 return handler.handle(request)105106if __name__ == '__main__':107 flask_app.run(port=3000)Error handling
{"ok": false, "error": "name_taken"}A channel with that name already exists in the workspace (active or archived). Slack enforces unique channel names.
Append a unique suffix to the channel name — use a database auto-increment ID, a UUID prefix, or a Unix timestamp rather than just the ticket description.
Not retryable with the same name. Generate a new unique name and retry immediately.
{"ok": false, "error": "missing_scope", "needed": "groups:write"}The bot token lacks the groups:write scope required to create private channels. The channels:manage scope covers public channels only.
Go to api.slack.com/apps → your app → OAuth & Permissions → Bot Token Scopes → add groups:write, then reinstall the app to the workspace to get a new token.
Not retryable until scope is added and app reinstalled.
{"ok": false, "error": "ratelimited"}conversations.create is Tier 2 (~20 requests/minute). Generating more than 20 tickets per minute exhausts the bucket.
Implement a queue with a 3-second delay between channel creation calls. Read the Retry-After header for the exact wait time.
Honor the Retry-After header value (in seconds). Exponential backoff starting at 3 seconds for retry logic.
{"ok": false, "error": "channel_not_found"}The channel ID passed to conversations.invite, pins.add, or chat.postMessage does not exist or the bot cannot see it. This can happen if channel creation succeeded but the ID was not saved correctly.
Always use the channel.id value returned in the conversations.create response body, not a hardcoded or cached value. Validate the ID before passing it to downstream calls.
Not retryable. Fetch the current channel list via conversations.list to find the correct ID.
{"ok": false, "error": "already_archived"}An action attempted to post a message or archive a channel that is already archived.
Check channel status before sending messages or re-archiving. Store ticket status in your database and gate API calls on that status.
Not retryable. Update your database to reflect the channel is closed.
Rate Limits for Slack API (Support Tickets)
| Scope | Limit | Window |
|---|---|---|
| conversations.create | ~20 requests | per minute (Tier 2) |
| conversations.invite | ~50 requests | per minute (Tier 3) |
| chat.postMessage | 1 message | per second per channel (Special) |
| pins.add | ~50 requests | per minute (Tier 3) |
| response_url follow-ups | 5 messages | per slash command invocation, valid for 30 minutes |
1import time2from slack_sdk.errors import SlackApiError34def slack_call_with_retry(fn, max_retries=3, **kwargs):5 for attempt in range(max_retries):6 try:7 response = fn(**kwargs)8 if not response.get('ok'):9 raise SlackApiError(response['error'], response)10 return response11 except SlackApiError as e:12 if e.response.get('error') == 'ratelimited':13 retry_after = int(e.response.headers.get('Retry-After', 5))14 time.sleep(retry_after)15 else:16 raise17 raise Exception(f'Failed after {max_retries} retries')- Queue ticket creation requests so no more than 15 channels are created per minute — leave headroom below the 20/min Tier 2 cap
- Use @slack/bolt's built-in retry middleware for production apps rather than manual retry loops
- Store channel IDs and message timestamps in a database immediately after creation — never rely on re-fetching them under load
- For high-volume support teams, spread /support invocations over time with a request queue rather than processing bursts synchronously
Security checklist
- Verify the X-Slack-Signature header on every incoming request using HMAC-SHA256 with your SLACK_SIGNING_SECRET before processing the payload
- Store SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET in environment variables — never commit them to version control
- Use private channels (is_private: true) for support tickets — public channels expose sensitive customer issues to the entire workspace
- Implement scope of least privilege: only request channels:manage, groups:write, chat:write, pins:write, and commands — not admin scopes
- Log all ticket creation, claim, escalation, and closure events with user IDs and timestamps for audit compliance
- Validate that the user triggering /support is a workspace member before creating a channel (check user_id against users.list if needed for anti-abuse)
- Set up Slack's built-in token rotation if your workspace is on Enterprise Grid to minimize blast radius of a leaked token
Automation use cases
Customer Support in Slack Connect
intermediateExtend the ticket system to Slack Connect channels where external customers and your support team collaborate in shared Slack channels, giving customers real-time visibility without needing a separate ticketing tool.
Bug Report Intake from App Feedback Buttons
advancedEmbed a 'Report Bug' button in your web or mobile app that fires a webhook to your support bot, automatically creating a ticket channel pre-filled with the user's session data and error logs.
SLA Timer and Escalation Automation
intermediateAdd a cron job that monitors open tickets and automatically escalates (posts to an escalation channel and reassigns) any ticket that has not been claimed within your SLA window (e.g., 2 hours).
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier (100 tasks/month); Starter from $19.99/monthZapier's Slack integration can trigger a workflow from a form submission (Typeform, Google Forms) or a message reaction, then create a channel and post a message — no code required.
- + No server or hosting required
- + Visual workflow builder with 5,000+ app integrations
- + Fast setup — under 20 minutes for basic ticket routing
- - No support for Block Kit interactive buttons (Claim/Close) without multi-step Zaps
- - Task limits on free and starter plans can be exhausted quickly in high-volume teams
- - Less control over error handling and retry logic
Make
Free tier (1,000 ops/month); Core from $9/monthMake (formerly Integromat) connects form triggers, Slack modules, and database storage into a visual scenario that creates ticket channels and posts formatted messages.
- + More powerful data transformation than Zapier at lower cost
- + Can loop over arrays and handle complex branching logic visually
- + Free tier includes 1,000 operations/month
- - Interactive Block Kit buttons still require a webhook endpoint to handle action callbacks
- - Scenario complexity grows quickly for multi-step ticket workflows
- - Real-time interaction handling (button clicks) is difficult without a custom webhook receiver
n8n
Self-hosted free; Cloud Starter from €20/monthn8n's Slack node supports conversations.create, conversations.invite, and chat.postMessage, making it possible to build the full ticket channel creation flow without writing code.
- + Self-hosted option means no per-task cost and full data control
- + Native Slack node covers conversations.create and chat.postMessage out of the box
- + Webhook triggers handle incoming slash command payloads directly
- - Interactive buttons (block_actions callbacks) require an additional webhook node and custom response logic
- - Self-hosted version requires infrastructure setup and maintenance
- - Cloud version free tier limited to 5 active workflows
Best practices
- Always call ack() or return an HTTP 200 within 3 seconds of receiving a slash command or block_action payload — set up a background task or queue for the actual API work
- Store ticket_id, channel_id, message_ts, user_id, and status in a database immediately after creation — do not rely on re-fetching from Slack under load
- Use is_private: true for all ticket channels — public channels expose sensitive customer information to the entire workspace
- Name channels with a consistent prefix and numeric ID (ticket-1042) rather than free-text descriptions, which can create name collisions and exceed the 80-character limit
- Pin the ticket card on creation so support agents immediately see full context when they join the channel, without needing to scroll through history
- Archive (not delete) closed ticket channels — archived channels remain searchable and serve as a knowledge base for recurring issues
- Limit Block Kit to simple section + actions layouts — avoid deeply nested attachments and keep total message size well under 13,000 characters to avoid the msg_blocks_too_long bug (Bolt issue #2509)
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Slack support ticket system using @slack/bolt v4.7.2. My /support slash command creates a private channel via conversations.create, invites the user, posts a Block Kit card with Claim/Escalate/Close buttons, and pins it. The button handlers call chat.update to update the card status and conversations.archive to close. Issue: [describe your problem]. My current handler code: [paste code]. Slack API response: [paste response]. Which Slack API method, scope, or timing constraint is causing this?
Build a support ticket dashboard UI for a Slack integration. The dashboard should show a table of open tickets with columns: ticket ID, channel name, requester, description (first 100 chars), status (Open/Claimed/Escalated), created timestamp. Each row has a View in Slack button that links to the Slack channel URL (https://slack.com/app_redirect?channel={channel_id}). Add filter tabs: All, Open, Claimed, Escalated, Closed. Style with Tailwind, use React hooks for state, and connect to a REST API at /api/tickets that returns the tickets array.
Frequently asked questions
Why does Slack show 'Hmm, that didn't work' when I run /support?
This error means Slack did not receive a valid HTTP 200 response from your endpoint within 3 seconds. Check that your server is publicly accessible (not localhost), your endpoint URL is correct in the Slash Command config, and your handler calls ack() (or returns a 200 JSON response) immediately before doing any async work. Use Bolt's built-in ack() for the simplest solution.
Can I create the ticket channel without the groups:write scope?
No. conversations.create requires channels:manage for public channels and groups:write for private channels. If you want private ticket channels (recommended), you must add groups:write to your app's Bot Token Scopes and reinstall the app to the workspace to get a new token with the updated scope.
How many tickets can I create per minute?
conversations.create is Tier 2, which allows approximately 20 requests per minute. If you receive a 429 response, read the Retry-After header for the exact wait time in seconds before retrying. For high-volume support teams, queue ticket creation requests with a 3-second gap to stay comfortably under the limit.
What happens when response_url expires or I've sent 5 follow-ups?
After 30 minutes or 5 POSTs to the response_url, further requests return a 410 Gone response. If you need to send more than 5 follow-up messages, switch to chat.postMessage using the channel ID directly. The response_url is only needed for the initial ack window — once the channel is created, use the Slack Web API for all subsequent messages.
Can I edit the Block Kit ticket card after posting it?
Yes. Use chat.update with the channel ID and the ts (timestamp) returned by the original chat.postMessage call. This is how the Claim button updates the card to show 'Claimed by @agent'. Always store the ts in your database at creation time — it is the only way to reference the specific message for updates.
Is the Slack API free to use?
Yes, the Slack Web API is free with no per-request charges. However, the free Slack workspace plan limits channel history visibility to 90 days and caps at 10 app integrations. The Pro plan ($7.25/user/month) removes message history limits. The API itself has no cost beyond your Slack workspace subscription.
Can RapidDev help build a custom Slack ticketing integration?
Yes. RapidDev has built 600+ apps including Slack automations with ticket systems, SLA escalation bots, and Slack Connect support workflows. Book a free consultation at rapidevelopers.com.
Should I use threads instead of dedicated channels for tickets?
Threads work for low-volume teams (fewer than 20 tickets/day) in a single #support channel. Dedicated channels are better when you need: strict privacy between tickets, a clear archive record per issue, or channel-specific integrations (like linking a ticket channel to a Linear or Jira issue). The dedicated channel approach in this guide is recommended for teams running Slack Connect support with external customers.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation