Back to Blog
Guide

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.

Slack Green Team
October 4, 2026
October 4, 2026
4 min read
Share:
slack api
slack developer
ruby

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::Message objects (a Hashie::Mash), so posted.ts and hist.messages.first.text work as methods.
  • • With a block, conversations_history calls the API until next_cursor is empty and yields each page. Our channel had 13 messages, so limit: 2 made 7 calls. Without a block you get one page and handle cursor yourself, as on our Slack API pagination page.
  • • e.message is the bare Slack error code, such as channel_not_found. Each code has its own class, so you can rescue Slack::Web::Api::Errors::ChannelNotFound alone.

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.

Always Active

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

Guide

@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 Green Team
Guide

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 Green Team
Guide

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.

Slack Green Team