slack-ruby-client: A Tested Ruby Example, Errors and Limits
slack-ruby-client 3.2.0 is the Ruby gem for the Slack Web API. We installed it, posted, read back, reacted and paginated against a real workspace, and recorded the error classes it raised, including the rate-limit error that a rescue of SlackError does not catch.
On this page
slack-ruby-client is the Ruby gem for the Slack Web API. Version 3.2.0 (July 2026) calls every Web API method as a snake_case Ruby method, such as client.chat_postMessage, raises one Ruby class per Slack error, and pages through cursors for you. It no longer has a real-time client: version 3.0.0 removed the RTM API, and there is no Socket Mode client in 3.2.0. We installed it on 4 October 2026 and ran every example below against a free-plan test workspace with a bot token.
Install, and the Ruby version it needs
On macOS the built-in Ruby is 2.6, and the install fails there:
$ /usr/bin/gem install slack-ruby-client
ERROR: Error installing slack-ruby-client:
The last version of hashie (>= 0) to support your Ruby & RubyGems was 5.0.0. Try installing it with `gem install hashie -v 5.0.0` and then running the current command again
hashie requires Ruby version >= 2.7. The current ruby version is 2.6.10.210.
The gem itself declares required_ruby_version >= 2.7. We installed Ruby 4.0.7 with brew install ruby and ran its gem. That pulled in 8 gems: slack-ruby-client 3.2.0, faraday 2.14.4, faraday-net_http, faraday-multipart, faraday-mashify, multipart-post, hashie 5.1.0 and gli.
brew install ruby
/opt/homebrew/opt/ruby/bin/gem install slack-ruby-client
The bot needs a token from an installed Slack app. Ours had chat:write, channels:history and reactions:write for this script. We read the token from an environment variable so it never sits in the code.
Post, read back, react and paginate
require 'slack-ruby-client'
Slack.configure { |c| c.token = ENV.fetch('SLACK_BOT_TOKEN') }
client = Slack::Web::Client.new
channel = ENV.fetch('CHANNEL_ID')
auth = client.auth_test
puts "auth.test: #{auth.user} #{auth.user_id} team=#{auth.team}"
posted = client.chat_postMessage(channel: channel, text: 'Hello from slack-ruby-client')
puts "posted ts=#{posted.ts} class=#{posted.class}"
hist = client.conversations_history(channel: channel, limit: 1)
puts "latest: #{hist.messages.first.text.inspect} (bot_id=#{hist.messages.first.bot_id})"
client.reactions_add(channel: channel, timestamp: posted.ts, name: 'white_check_mark')
puts 'reaction added'
pages = 0
count = 0
client.conversations_history(channel: channel, limit: 2) do |page|
pages += 1
count += page.messages.size
puts "page #{pages}: #{page.messages.size} messages, next_cursor=#{page.response_metadata&.next_cursor.to_s.empty? ? '(none)' : 'set'}"
end
puts "total #{count} messages in #{pages} pages"
begin
client.chat_postMessage(channel: 'C000BADID00', text: 'x')
rescue Slack::Web::Api::Errors::SlackError => e
puts "bad channel: #{e.class} message=#{e.message.inspect} error=#{e.error.inspect}"
end
begin
Slack::Web::Client.new(token: 'xoxb-not-a-real-token').auth_test
rescue Slack::Web::Api::Errors::SlackError => e
puts "bad token: #{e.class} message=#{e.message.inspect}"
end
begin
client.reactions_add(channel: channel, timestamp: posted.ts, name: 'white_check_mark')
rescue Slack::Web::Api::Errors::SlackError => e
puts "same reaction twice: #{e.class} message=#{e.message.inspect}"
end
Output:
auth.test: sglab1004 U0C6NCVVBNG team=Slack
posted ts=1791077037.228259 class=Slack::Messages::Message
latest: "Hello from slack-ruby-client" (bot_id=B0C6JHSGEAW)
reaction added
page 1: 2 messages, next_cursor=set
page 2: 2 messages, next_cursor=set
page 3: 2 messages, next_cursor=set
page 4: 2 messages, next_cursor=set
page 5: 2 messages, next_cursor=set
page 6: 2 messages, next_cursor=set
page 7: 1 messages, next_cursor=(none)
total 13 messages in 7 pages
bad channel: Slack::Web::Api::Errors::ChannelNotFound message="channel_not_found" error="channel_not_found"
bad token: Slack::Web::Api::Errors::InvalidAuth message="invalid_auth"
same reaction twice: Slack::Web::Api::Errors::AlreadyReacted message="already_reacted"
What the run shows:
- • Responses are
Slack::Messages::Messageobjects (a Hashie::Mash), soposted.tsandhist.messages.first.textwork as methods. - • With a block,
conversations_historycalls the API untilnext_cursoris empty and yields each page. Our channel had 13 messages, solimit: 2made 7 calls. Without a block you get one page and handlecursoryourself, as on our Slack API pagination page. - •
e.messageis the bare Slack error code, such aschannel_not_found. Each code has its own class, so you can rescueSlack::Web::Api::Errors::ChannelNotFoundalone.
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Error classes, and the one that slips through
A missing scope raises MissingScope, and the response body tells you which scope to add. We called usergroups_list without usergroups:read:
begin
client.usergroups_list
rescue Slack::Web::Api::Errors::SlackError => e
puts "missing scope: #{e.class} needed=#{e.response.body['needed']} provided=#{e.response.body['provided'].to_s[0,40]}..."
end
missing scope: Slack::Web::Api::Errors::MissingScope needed=usergroups:read provided=chat:write,channels:join,channels:manage...
Rate limits work differently. An HTTP 429 raises Slack::Web::Api::Errors::TooManyRequestsError, and that class is not a SlackError. We printed both class chains:
$ ruby -e "require 'slack-ruby-client'; p Slack::Web::Api::Errors::TooManyRequestsError.ancestors.take(4); p Slack::Web::Api::Errors::ChannelNotFound.ancestors.take(4)"
[Slack::Web::Api::Errors::TooManyRequestsError, Faraday::Error, StandardError, Exception]
[Slack::Web::Api::Errors::ChannelNotFound, Slack::Web::Api::Errors::SlackError, Faraday::Error, StandardError]
So rescue Slack::Web::Api::Errors::SlackError lets a rate limit through. Rescue TooManyRequestsError next to it and wait e.retry_after seconds, or rescue Faraday::Error, which covers both. The only built-in retry is in the cursor pagination: it sleeps retry_after and tries again up to default_max_retries times, which printed as 100 in 3.2.0.
We could not trigger a 429 on purpose. 200 conversations_history calls in a row took 80.2 seconds with no limit hit, and 60 chat_postMessage calls to one channel took 24.1 seconds, also with none. The 429 responses we did get with other tools are on our rate limits page.
Events: verify the signature, no Socket Mode
For the Events API over HTTP, the gem checks Slack's request signature. Slack::Events::Request wraps a Rack request. We signed one body with our app's signing secret, then sent a wrong signature and a 10-minute-old timestamp:
require 'slack-ruby-client'
require 'rack'
require 'openssl'
SECRET = ENV.fetch('SLACK_SIGNING_SECRET')
Slack::Events.configure { |c| c.signing_secret = SECRET }
def request(ts, sig, body)
env = Rack::MockRequest.env_for('/slack/events', method: 'POST', input: body,
'HTTP_X_SLACK_REQUEST_TIMESTAMP' => ts, 'HTTP_X_SLACK_SIGNATURE' => sig)
Slack::Events::Request.new(Rack::Request.new(env))
end
body = '{"type":"url_verification","challenge":"abc"}'
ts = Time.now.to_i.to_s
sig = 'v0=' + OpenSSL::HMAC.hexdigest('SHA256', SECRET, "v0:#{ts}:#{body}")
puts "valid signature: #{request(ts, sig, body).verify!}"
begin
request(ts, 'v0=deadbeef', body).verify!
rescue => e
puts "wrong signature: #{e.class}"
end
old_ts = (Time.now.to_i - 600).to_s
old_sig = 'v0=' + OpenSSL::HMAC.hexdigest('SHA256', SECRET, "v0:#{old_ts}:#{body}")
begin
request(old_ts, old_sig, body).verify!
rescue => e
puts "600-second-old timestamp: #{e.class}"
end
valid signature: true
wrong signature: Slack::Events::Request::InvalidSignature
600-second-old timestamp: Slack::Events::Request::TimestampExpired
Socket Mode is the gap. The gem's lib/slack folder has config, events, logger, messages, utils and web, and nothing that opens a WebSocket. It does have apps_connections_open, the call that returns the Socket Mode URL, but you would have to run the WebSocket loop and acknowledge each envelope yourself. Old blog posts that use Slack::RealTime::Client describe the RTM client, which version 3.0.0 removed. If you need Socket Mode without a public URL, the Python and JavaScript SDKs ship a client; ours is on the Socket Mode in Python page.
Stop Jiggling Your Mouse.
Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.
Related Articles
@slack/web-api v8: A Tested Node.js and TypeScript Example
@slack/web-api 8.2.0 runs on Node 20 or newer and sends requests with fetch. We posted, paginated, caught a Slack error and a rate limit in TypeScript, and found that the v7 agent option for proxies is silently ignored in v8.
Slack API in Go With slack-go/slack: A Tested Example
slack-go/slack v0.29.0 is the Go client most people use for the Slack API. We ran a program that posts, reads back and updates a Block Kit message, a Socket Mode bot that answers a mention, and a loop that hit the rate limit, with the output and error types we got.
Slack Status Change Notification: The Events an App Gets, Timed
Slack does not tell anyone when you change your status. An app can find out within a second through the user_status_changed and user_change events. We changed a status 7 ways and timed each event, including statuses that expired.