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.

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

each_record pages automatically and stops when the server reports there is no more data. See Backup & Recovery.

Errors

begin
  client.subscribers.create(email: 'invalid')
rescue Broadcast::ValidationError => e     # 422
  # bad input — do not retry
rescue Broadcast::AuthenticationError => e # 401
rescue Broadcast::AuthorizationError => e  # 403 — token lacks permission
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:

Prefer PHP? See the PHP SDK.

Was this page helpful?

Thanks for your feedback!

Thanks for letting us know. We'll work on improving this page.