Ruby SDK
broadcast-ruby is the official Ruby client for the Broadcast API. It covers every
API endpoint, and it works against any Broadcast instance: there is no hosted-only
behaviour.
The gem is the reference implementation: the PHP SDK and the other clients are built to match its behaviour.
Installation
Add it to your Gemfile:
gem 'broadcast-ruby'
Or install it directly:
gem install broadcast-ruby
For plain Ruby scripts, require it explicitly:
require 'broadcast'
Quick Start
Create a client with your API token and the URL of your instance:
client = Broadcast::Client.new( api_token: ENV['BROADCAST_API_TOKEN'], host: 'https://mail.example.com' ) client.subscribers.create(email: 'ada@example.com', first_name: 'Ada')
Warning
host is required. There is no default. Broadcast is self-hosted first, so
every instance lives at its own domain. A built-in guess would be wrong for almost
everyone, so the gem asks you to be explicit.
You can also set BROADCAST_HOST and BROADCAST_API_TOKEN in the environment.
These are the same names the Agents CLI uses in
~/.config/broadcast/config, so a machine already set up for the CLI needs no extra
configuration.
Create your API token under Settings → API Keys, granting only the permissions your integration needs. See API Authentication for the full permission list.
Configuration
client = Broadcast::Client.new( api_token: '...', host: 'https://mail.example.com', timeout: 30, # read timeout, seconds open_timeout: 10, retry_attempts: 3, retry_delay: 1, # base backoff, multiplied by attempt number max_retry_delay: 30, # ceiling on a server-supplied Retry-After warnings_mode: :log, # :log | :raise | :ignore logger: Rails.logger, broadcast_channel_id: 42 # admin/system tokens only )
Timeouts, rate-limited requests, and server errors are retried with backoff. A validation error (422) is never retried: it is deterministic, so a retry only adds latency.
Responses
Every call returns the parsed JSON body, so result['id'] works as you would
expect. Transport metadata is available alongside it:
result = client.subscribers.create(email: 'ada@example.com') result['id'] # the response body result.status # 201 result.warnings # parsed warnings, if any result.rate_limit&.remaining # requests left in the window result.idempotent_replay? # true if the API replayed a stored response
Warnings
A successful response can still carry warnings: the API accepted your request but
ignored part of it, such as an unrecognised parameter or a filter it could not
parse. A mistyped created_after silently widens a result set rather than failing,
so it is worth checking.
result.warnings.each { |w| Rails.logger.warn(w) }
Set warnings_mode: :raise to turn them into exceptions instead. Note the write has
already happened by then: nothing is rolled back. See
API Response Warnings.
Rate Limits
Every response carries the current limit state, and 429s are retried
automatically, honouring the server’s Retry-After (capped at max_retry_delay).
result = client.subscribers.list result.rate_limit.limit # => 120 result.rate_limit.remaining # => 118 result.rate_limit.reset # => 2026-07-26 12:00:00 UTC sleep 1 if result.rate_limit.remaining < 10
If the retries are exhausted you get a Broadcast::RateLimitError, which carries
#retry_after so you can requeue the job sensibly.
Rails Integration
The gem ships an ActionMailer delivery method, so existing mailers can send through Broadcast without changing a single mailer class:
# config/environments/production.rb config.action_mailer.delivery_method = :broadcast config.action_mailer.broadcast_settings = { api_token: ENV['BROADCAST_API_TOKEN'], host: 'https://mail.example.com' }
Your mailers then work unchanged:
class OrderMailer < ApplicationMailer def receipt(order) @order = order mail(to: order.email, subject: 'Your receipt') end end
This only needs a token with transactional permissions.
Common Tasks
Subscribers
client.subscribers.list(page: 1, is_active: true, tags: ['vip']) client.subscribers.find(email: 'ada@example.com') client.subscribers.create(email: 'ada@example.com', tags: ['vip']) client.subscribers.update('ada@example.com', first_name: 'Ada') client.subscribers.add_tags('ada@example.com', ['beta']) client.subscribers.unsubscribe('ada@example.com')
See Subscribers API for every filter and field.
Transactional email
client.transactionals.create( to: 'ada@example.com', subject: 'Your receipt', body: '<p>Thanks for your order.</p>', idempotency_key: "receipt-#{order.id}" )
Passing an idempotency_key makes a retry safe: the server stores the response for
24 hours and replays it rather than sending a second email. Check
result.idempotent_replay? to tell a replay from a fresh send.
Full details in Transactional Email API.
Broadcasts
broadcast = client.broadcasts.create(subject: 'Weekly update', body: '<p>Hello</p>') client.broadcasts.schedule(broadcast['id'], scheduled_send_at: '2026-08-01T09:00:00Z', scheduled_timezone: 'UTC')
Warning
client.broadcasts.send_broadcast(id) sends immediately to the whole audience.
There is no unsend. Call client.status first: if readiness.broadcasts is
false, the channel has no usable email server or sender identity.
Opt-in forms
forms = client.opt_in_forms.list(page: 1) # up to 250 per page client.opt_in_forms.get_opt_in_form(15) # Replace the view shown after a signup. Safe to repeat; [] restores the default. client.opt_in_forms.update(15, opt_in_post_submission_blocks: [ { block_type: 'heading', text_value: 'One more step: check your email' }, { block_type: 'text', text_value: 'Click the confirmation link we just sent you.' } ])
See Opt-In Forms API for every field.
Suppressions
client.suppressions.check('ada@example.com') # will this address receive mail? client.suppressions.list(page: 1, email: 'example.com') client.suppressions.add('bounced@example.com') client.suppressions.remove('bounced@example.com') client.suppressions.bulk_add(['a@example.com', 'b@example.com']) # up to 10,000 client.suppressions.bulk_remove(['a@example.com'])
check reads across both the channel’s list and the installation-wide global
list, so it answers the question integrations actually ask: will this address
receive mail? Adding an already-suppressed address is a success (200 rather
than 201), so there is no need to check first.
The global list is a separate resource and needs an admin (system) token:
client.global_suppressions.list client.global_suppressions.add('spamtrap@example.com') client.global_suppressions.remove('spamtrap@example.com') client.global_suppressions.bulk_add([...]) client.global_suppressions.bulk_remove([...])
See Suppressions API for the full semantics.
Channel settings and brand kit
client.channel.settings # general, sender, unsubscribe_page, confirmation_page, opt_in_forms client.channel.update_settings(unsubscribe_page: { legal_notice_url: 'https://example.com/impressum' }) client.channel.design # the brand kit client.channel.update_design(colors: { accent: '#ff5500' }, typography: { font: 'georgia' })
Updates are partial: send only what changes, and the response is the whole object after the change. Read result.warnings for keys the server ignored. Changing either one needs the Channel Settings write permission. The brand kit switches (match_brand_kit in unsubscribe_page, confirmation_page and opt_in_forms) are channel settings, not part of the brand kit update. The brand kit logo is the logo_url of an image in the channel’s file library. Reading and changing channel settings, and changing the brand kit, need Broadcast 2.46.0 or later and the next release of this SDK after 0.8.0. See Channel Settings API and Brand Kit API.
Users
Create and manage the people who sign in to Broadcast, and what they can reach. This
needs an admin (system) token with the Users permission, and no channel: a channel
token gets Broadcast::AuthorizationError.
client.users.list(q: 'ada', status: 'active') user = client.users.create(email: 'ada@example.com', first_name: 'Ada', last_name: 'Lovelace', send_password_reset: true) # or password: '...' client.users.get_user(user['id']) # includes system and channel permissions client.users.update(user['id'], last_name: 'King') client.users.deactivate(user['id']) client.users.activate(user['id']) # also clears a lockout client.users.delete(user['id'])
Channel access. A write replaces the user’s whole record on that channel. Pass
exactly one of permissions:, role: or preset_id::
client.users.channel_permissions(user['id']) client.users.set_channel_permissions(user['id'], channel_id, role: 'Editor') client.users.set_channel_permissions(user['id'], channel_id, permissions: { 'subscribers_read' => true }) client.users.bulk_channel_permissions(user['id'], broadcast_channel_ids: [3, 4], preset_id: 12) client.users.remove_channel_permissions(user['id'], channel_id) client.users.system_permissions(user['id']) client.users.update_system_permissions(user['id'], { 'user_management' => true })
What to expect:
- Sudo users are read-only. Changing one, or their permissions, raises
Broadcast::AuthorizationError, and sudo access can never be granted through the API. - Nothing is silently ignored. A request the API will not apply (such as
sudo,active, or permissions inside the user payload) raisesBroadcast::ValidationErrorand changes nothing. Usedeactivateandactivatefor sign-in access. - A bulk apply that applies to no channel raises
Broadcast::ValidationError; otherwise check itsfailedlist. - Passing none, or more than one, of the permission options raises
ArgumentErrorbefore any request is sent.
See Users API for every field and rule, and Users & Permissions for what the roles and permissions mean.
Channel Scoping
Admin (system) tokens can address any channel. Scope a block of calls to one:
client.with_channel(123) do client.email_servers.list end
The previous scope is restored afterwards, including when the block raises. Regular tokens are already channel-scoped and need none of this.
Webhooks
class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token def create payload = request.raw_post unless Broadcast::Webhook.verify(payload, request.headers['X-Broadcast-Signature'], request.headers['X-Broadcast-Timestamp'], secret: ENV['BROADCAST_WEBHOOK_SECRET']) return head :unauthorized end event = JSON.parse(payload) # handle event['type'] head :ok end end
Pass the raw request body. Re-serialising a parsed hash changes the bytes and
verification will fail. Broadcast::Webhook::EVENT_TYPES lists every event name.
See Webhook Endpoints API for creating and managing endpoints.
Export & Migration
Admin (system) tokens can read the migration endpoints, which back instance backups and moves:
client = Broadcast::Client.new(api_token: '...', host: '...', broadcast_channel_id: 42) client.migration.manifest # sizes the export first client.migration.each_record(:subscribers) do |subscriber| csv << subscriber end client.migration.each_record(:unsubscribed_emails) do |row| csv << row # the channel's own suppression list end
each_record pages automatically and stops when the server reports there is no more
data. See Backup & Recovery.
The channel’s own suppression list is a separate collection from suppressions, which is the global list, so export both. It needs Broadcast 2.40.0 or later and version 0.6.0 or later of this SDK.
Errors
begin client.subscribers.create(email: 'invalid') rescue Broadcast::ValidationError => e # 422 # bad input, do not retry rescue Broadcast::AuthenticationError => e # 401: bad token, or it lacks this permission rescue Broadcast::AuthorizationError => e # 403: needs an admin token, or the user is sudo rescue Broadcast::NotFoundError => e # 404 rescue Broadcast::RateLimitError => e # 429 sleep(e.retry_after || 5) rescue Broadcast::APIError => e # 5xx and other transport failures end
Source and Support
The gem is open source and its README documents every method:
- Source: github.com/send-broadcast/broadcast-ruby
- RubyGems: rubygems.org/gems/broadcast-ruby
Prefer PHP? See the PHP SDK.