# Ruby SDK

> Install and use broadcast-ruby, the official Ruby client for the Broadcast API: subscribers, broadcasts, sequences, transactional email, and webhooks.

Source: https://sendbroadcast.net/docs/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](https://sendbroadcast.net/docs/php-sdk) and the
other clients are built to match its behaviour.

## Installation

Add it to your Gemfile:

```ruby
gem 'broadcast-ruby'
```

Or install it directly:

```bash
gem install broadcast-ruby
```

For plain Ruby scripts, require it explicitly:

```ruby
require 'broadcast'
```

## Quick Start

Create a client with your API token and the URL of your instance:

```ruby
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](https://sendbroadcast.net/docs/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](https://sendbroadcast.net/docs/api-authentication) for the full
permission list.

## Configuration

```ruby
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:

```ruby
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.

```ruby
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](https://sendbroadcast.net/docs/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`).

```ruby
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:

```ruby
# 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:

```ruby
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

```ruby
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](https://sendbroadcast.net/docs/api-subscribers) for every filter and field.

### Transactional email

```ruby
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](https://sendbroadcast.net/docs/api-transactional-email).

### Broadcasts

```ruby
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

```ruby
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](https://sendbroadcast.net/docs/api-opt-in-forms) for every field.

### Suppressions

```ruby
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:

```ruby
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](https://sendbroadcast.net/docs/api-suppressions) for the full semantics.

### Channel settings and brand kit

```ruby
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](https://sendbroadcast.net/docs/api-channel-settings) and [Brand Kit API](https://sendbroadcast.net/docs/api-channel-design).

### 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`.

```ruby
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:`:

```ruby
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) raises `Broadcast::ValidationError` and changes
  nothing. Use `deactivate` and `activate` for sign-in access.
- A bulk apply that applies to no channel raises `Broadcast::ValidationError`; otherwise check its
  `failed` list.
- Passing none, or more than one, of the permission options raises `ArgumentError`
  before any request is sent.

See [Users API](https://sendbroadcast.net/docs/api-users) for every field and rule, and
[Users & Permissions](https://sendbroadcast.net/docs/users-and-permissions) for what the roles and permissions mean.

## Channel Scoping

Admin (system) tokens can address any channel. Scope a block of calls to one:

```ruby
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

```ruby
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](https://sendbroadcast.net/docs/api-webhook-endpoints) for creating and managing endpoints.

## Export & Migration

Admin (system) tokens can read the migration endpoints, which back instance
backups and moves:

```ruby
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](https://sendbroadcast.net/docs/backup).

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

```ruby
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](https://github.com/send-broadcast/broadcast-ruby)
- **RubyGems:** [rubygems.org/gems/broadcast-ruby](https://rubygems.org/gems/broadcast-ruby)

Prefer PHP? See the [PHP SDK](https://sendbroadcast.net/docs/php-sdk).
