broadcast-ruby 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -0
- data/Gemfile.lock +3 -2
- data/README.md +272 -17
- data/SDK-COVERAGE.md +422 -0
- data/lib/broadcast/client.rb +36 -136
- data/lib/broadcast/configuration.rb +53 -4
- data/lib/broadcast/connection.rb +278 -0
- data/lib/broadcast/debug_logger.rb +64 -0
- data/lib/broadcast/delivery_method.rb +5 -0
- data/lib/broadcast/errors.rb +27 -1
- data/lib/broadcast/resources/autopilots.rb +100 -0
- data/lib/broadcast/resources/discovery.rb +36 -0
- data/lib/broadcast/resources/migration.rb +75 -0
- data/lib/broadcast/resources/opt_in_forms.rb +12 -0
- data/lib/broadcast/resources/subscribers.rb +24 -0
- data/lib/broadcast/resources/templates.rb +17 -0
- data/lib/broadcast/resources/transactionals.rb +39 -2
- data/lib/broadcast/response.rb +104 -0
- data/lib/broadcast/version.rb +1 -1
- data/lib/broadcast/webhook.rb +34 -0
- data/lib/broadcast.rb +6 -0
- metadata +9 -3
- data/.rubocop.yml +0 -50
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fec10ff364f7b63e728e1da143f2e0ff4445e2466f8a9e1a307370142d4c8666
|
|
4
|
+
data.tar.gz: efce695852b7774e6e40f1f91a93ee2487e8d635a579ab0fe20505a9fe4a9efe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b72667f6f90ecb99ec60d0d9c905f529fbf0f78e30386e78ca93aeb1ac783e2a923750b76a41b73f2da771be67b3bfca012749066614a2ddb99c6cebb093924c
|
|
7
|
+
data.tar.gz: ff0b6ba2de7606b7dbc1fab2c552f6947158cd69ad28ae328d3b23f1d52b8878700802a08d108841e7453b5495398258c70e2e287bbc5ac76e1a908baedab1c1
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,93 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-07-26
|
|
6
|
+
|
|
7
|
+
Catches the gem up to the Broadcast v2.19 API. The API gained a response-warnings
|
|
8
|
+
contract, idempotency keys, and several discovery endpoints since 0.2.0; none of
|
|
9
|
+
them were represented here.
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
- **`host` is now required.** It previously defaulted to `https://sendbroadcast.com`,
|
|
13
|
+
which 301-redirects to `sendbroadcast.net` — and since the client did not follow
|
|
14
|
+
redirects, the default failed every request with
|
|
15
|
+
`APIError: Unexpected response: 301`. Broadcast is self-hosted-first, so there is
|
|
16
|
+
no URL the gem can guess. Pass `host:` explicitly or set `BROADCAST_HOST`.
|
|
17
|
+
A host without a scheme is now rejected at construction rather than at request time.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`client.autopilots`** — the Autopilot API (AI-generated newsletters): CRUD,
|
|
21
|
+
activate / pause / deactivate, trigger_run, and runs. The `autopilot_read` /
|
|
22
|
+
`autopilot_write` token permissions existed since 2026-01-30 but had no
|
|
23
|
+
endpoints behind them until Broadcast v2.19.1. `update` strips a bullet-masked
|
|
24
|
+
`openrouter_api_key` so a fetch-modify-save cannot destroy the stored key.
|
|
25
|
+
- **API warnings.** Successful writes can carry a `warnings` array describing what
|
|
26
|
+
the server ignored (`unrecognized_parameter`, `parameter_ignored`,
|
|
27
|
+
`parameter_overridden`, `double_opt_in_skipped`). Exposed as `result.warnings`
|
|
28
|
+
and controlled by `warnings_mode:` — `:log` (default), `:raise`, or `:ignore`.
|
|
29
|
+
- **`Broadcast::Response`.** All JSON calls now return a Hash subclass carrying
|
|
30
|
+
`#warnings`, `#rate_limit`, `#status`, `#headers`, and `#idempotent_replay?`.
|
|
31
|
+
Existing Hash-based code is unaffected.
|
|
32
|
+
- **Idempotent transactional sends.** `transactionals.create(..., idempotency_key:)`
|
|
33
|
+
sends the `Idempotency-Key` header; `Broadcast::ConflictError` maps 409 (previously
|
|
34
|
+
a generic `APIError`).
|
|
35
|
+
- **Rate-limit awareness.** `result.rate_limit` exposes the `X-RateLimit-*` headers,
|
|
36
|
+
`RateLimitError#retry_after` exposes `Retry-After`, and 429s are now retried
|
|
37
|
+
honouring it, bounded by the new `max_retry_delay` setting (default 30s).
|
|
38
|
+
- **Discovery endpoints:** `client.whoami`, `client.status`, `client.prime`, and
|
|
39
|
+
`client.skill` (plain text).
|
|
40
|
+
- **Migration/export namespace:** `client.migration.*` covers all 19 read-only
|
|
41
|
+
endpoints under `/api/migration/v1`, plus `each_record` for automatic paging and
|
|
42
|
+
`download_file_asset` for binary assets. Requires an admin token.
|
|
43
|
+
- **`Broadcast::Webhook::EVENT_TYPES`** and per-category constants (`EMAIL_EVENTS`,
|
|
44
|
+
`SUBSCRIBER_EVENTS`, `BROADCAST_EVENTS`, `SEQUENCE_EVENTS`, `SYSTEM_EVENTS`).
|
|
45
|
+
- `BROADCAST_HOST` / `BROADCAST_API_TOKEN` environment fallbacks, matching the
|
|
46
|
+
Broadcast CLI's config keys.
|
|
47
|
+
- Redirect handling: same-host GETs follow up to 3 hops. Writes, and any redirect
|
|
48
|
+
that changes host, fail with an error naming the target — every request carries
|
|
49
|
+
the bearer token, so following a cross-host redirect would leak it.
|
|
50
|
+
- Documented attributes added to the API after 0.2.0 — template
|
|
51
|
+
`template_purpose` / `confirmation_text` / `default_confirmation` /
|
|
52
|
+
`confirmation_page_settings`; opt-in form `confirmation_email_template_id` /
|
|
53
|
+
`welcome_email_template_id` / `confirmation_redirect_url` /
|
|
54
|
+
`include_unsubscribe_link_in_confirmation`; admin-only `confirmed_at` on
|
|
55
|
+
subscriber create; and the full subscriber list filter set.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- Debug logging redacted: SMTP passwords and provider API keys are no longer written
|
|
59
|
+
to the log when `debug: true`.
|
|
60
|
+
- 2xx responses other than 200/201 (e.g. 204) no longer raise.
|
|
61
|
+
- A 2xx response with a non-JSON body no longer raises `JSON::ParserError`.
|
|
62
|
+
- Error messages now include ActiveModel-style `errors` hashes, not just `error` strings.
|
|
63
|
+
- Binary responses keep binary encoding instead of being tagged UTF-8.
|
|
64
|
+
- The ActionMailer delivery method no longer wraps `WarningError` in `DeliveryError` —
|
|
65
|
+
the email was sent, so reporting a delivery failure would be wrong.
|
|
66
|
+
- **Packaging:** the gemspec shipped this repo's internal planning documents
|
|
67
|
+
(`TODO.md`, `SDK-TODO.md`, `.api-coverage.yml`) and the committed `.gem`
|
|
68
|
+
artifacts at the repo root to every install. The file list now excludes them,
|
|
69
|
+
along with `.github/` and `pkg/`. `SDK-COVERAGE.md` is kept deliberately —
|
|
70
|
+
"what does this gem support" is a user's question.
|
|
71
|
+
|
|
72
|
+
### Internal
|
|
73
|
+
- HTTP transport extracted from `Client` into `Broadcast::Connection`, and debug
|
|
74
|
+
logging into `Broadcast::DebugLogger`.
|
|
75
|
+
- Test coverage for the gemspec's file list, since a wrong package fails
|
|
76
|
+
silently: nothing breaks, the download is just wrong.
|
|
77
|
+
|
|
78
|
+
### Verification
|
|
79
|
+
- 292 tests, rubocop clean.
|
|
80
|
+
- Coverage against the generated OpenAPI document: 104/104 operations
|
|
81
|
+
(`rake "openapi:coverage[../broadcast-ruby]"` in the `broadcast` repo).
|
|
82
|
+
- The built `.gem` was installed into an isolated `GEM_HOME` and exercised.
|
|
83
|
+
- `rake test_live` run against a real self-hosted instance: 14 tests, 52
|
|
84
|
+
assertions, 0 failures. Covers the discovery endpoints, the `X-RateLimit-*`
|
|
85
|
+
headers, and full create/read/update/delete round-trips for subscribers,
|
|
86
|
+
sequences, segments, templates, and webhook endpoints.
|
|
87
|
+
- Two gaps in that run, both deliberate: the transactional **send** test is
|
|
88
|
+
skipped unless `BROADCAST_TEST_EMAIL` is set, so no mail was delivered; and
|
|
89
|
+
the target was a local development instance over plain HTTP, so TLS and
|
|
90
|
+
real-world latency and rate limiting are still unexercised.
|
|
91
|
+
|
|
5
92
|
## [0.2.0] - 2026-04-28
|
|
6
93
|
|
|
7
94
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
broadcast-ruby (0.
|
|
4
|
+
broadcast-ruby (0.3.0)
|
|
5
5
|
base64
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -155,6 +155,7 @@ GEM
|
|
|
155
155
|
|
|
156
156
|
PLATFORMS
|
|
157
157
|
arm64-darwin-24
|
|
158
|
+
arm64-darwin-25
|
|
158
159
|
x86_64-linux
|
|
159
160
|
|
|
160
161
|
DEPENDENCIES
|
|
@@ -176,7 +177,7 @@ CHECKSUMS
|
|
|
176
177
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
177
178
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
178
179
|
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
|
179
|
-
broadcast-ruby (0.
|
|
180
|
+
broadcast-ruby (0.3.0)
|
|
180
181
|
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
|
181
182
|
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
182
183
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# broadcast-ruby
|
|
2
2
|
|
|
3
|
-
Ruby client for the [Broadcast](https://sendbroadcast.
|
|
3
|
+
Ruby client for the [Broadcast](https://sendbroadcast.net) email platform.
|
|
4
4
|
|
|
5
|
-
Works with
|
|
5
|
+
Works with any Broadcast instance — self-hosted or SaaS.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -37,7 +37,7 @@ require 'broadcast'
|
|
|
37
37
|
|
|
38
38
|
client = Broadcast::Client.new(
|
|
39
39
|
api_token: 'your-token',
|
|
40
|
-
host: 'https://
|
|
40
|
+
host: 'https://mail.example.com' # your Broadcast instance
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
client.send_email(
|
|
@@ -47,26 +47,108 @@ client.send_email(
|
|
|
47
47
|
)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
`host` has no default — every Broadcast instance lives at its own domain, so
|
|
51
|
+
there is no URL the gem could guess correctly. You can supply it through the
|
|
52
|
+
environment instead, using the same variable names as the Broadcast CLI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export BROADCAST_HOST=https://mail.example.com
|
|
56
|
+
export BROADCAST_API_TOKEN=your-token
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
client = Broadcast::Client.new # picks both up from ENV
|
|
61
|
+
```
|
|
62
|
+
|
|
50
63
|
### Options
|
|
51
64
|
|
|
52
65
|
| Option | Default | Description |
|
|
53
66
|
|--------|---------|-------------|
|
|
54
|
-
| `api_token` | *required* | Your Broadcast API token |
|
|
55
|
-
| `host` |
|
|
67
|
+
| `api_token` | *required* | Your Broadcast API token. Falls back to `BROADCAST_API_TOKEN` |
|
|
68
|
+
| `host` | *required* | Broadcast instance URL, scheme included. Falls back to `BROADCAST_HOST` |
|
|
56
69
|
| `timeout` | `30` | Read timeout (seconds) |
|
|
57
70
|
| `open_timeout` | `10` | Connection timeout (seconds) |
|
|
58
|
-
| `retry_attempts` | `3` | Max total attempts (1 initial + 2 retries). Server errors (5xx) and
|
|
71
|
+
| `retry_attempts` | `3` | Max total attempts (1 initial + 2 retries). Server errors (5xx), timeouts, and rate limits (429) are retried; other client errors (4xx) are not |
|
|
59
72
|
| `retry_delay` | `1` | Base delay between retries in seconds (multiplied by attempt number) |
|
|
60
|
-
| `
|
|
73
|
+
| `max_retry_delay` | `30` | Ceiling for a server-supplied `Retry-After`, so a long rate-limit window can't stall the caller |
|
|
74
|
+
| `warnings_mode` | `:log` | How to handle API warnings — `:log`, `:raise`, or `:ignore`. See [API Warnings](#api-warnings) |
|
|
75
|
+
| `debug` | `false` | Log request/response details (credentials are redacted) |
|
|
61
76
|
| `logger` | `nil` | Logger instance for debug output (e.g. `Rails.logger`) |
|
|
62
77
|
| `broadcast_channel_id` | `nil` | Auto-included on every request when set. Required when using an admin/system token (regular tokens are channel-scoped already). Can be overridden per-call or via `client.with_channel(id) { ... }` |
|
|
63
78
|
|
|
64
|
-
All methods return parsed JSON as Ruby Hashes with string keys.
|
|
79
|
+
All methods return parsed JSON as Ruby Hashes with string keys. The returned
|
|
80
|
+
object is a `Broadcast::Response` — a Hash subclass that also carries response
|
|
81
|
+
metadata (`#warnings`, `#rate_limit`, `#status`, `#idempotent_replay?`).
|
|
82
|
+
Anything that worked against a plain Hash still works.
|
|
65
83
|
|
|
66
84
|
> **Note on module naming:** This gem defines a top-level `Broadcast` module. If your application already has a `Broadcast` class or module (e.g. an ActiveRecord model), you may encounter a namespace collision.
|
|
67
85
|
|
|
68
86
|
---
|
|
69
87
|
|
|
88
|
+
## API Warnings
|
|
89
|
+
|
|
90
|
+
Broadcast accepts a write and then tells you what it ignored. A misspelled
|
|
91
|
+
attribute, a parameter that only applies in another mode, a value the server
|
|
92
|
+
overrode — none of these fail the request, they come back as a `warnings` array
|
|
93
|
+
on the successful response.
|
|
94
|
+
|
|
95
|
+
This is the difference between "my custom field isn't saving and I can't work
|
|
96
|
+
out why" and a one-line answer:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
result = client.subscribers.create(email: 'jane@example.com', frist_name: 'Jane')
|
|
100
|
+
|
|
101
|
+
result['id'] # => 42 — the subscriber WAS created
|
|
102
|
+
result.warnings? # => true
|
|
103
|
+
result.warnings.each { |w| puts w }
|
|
104
|
+
# [unrecognized_parameter] subscriber.frist_name: frist_name is not a recognized
|
|
105
|
+
# subscriber attribute and was ignored.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Each warning has `#code`, `#param` (a dot-path, may be nil), and `#message`.
|
|
109
|
+
Values you submitted are never echoed back, so warnings are safe to log.
|
|
110
|
+
|
|
111
|
+
Control what happens via `warnings_mode`:
|
|
112
|
+
|
|
113
|
+
| Mode | Behaviour |
|
|
114
|
+
|------|-----------|
|
|
115
|
+
| `:log` (default) | Written to `logger` at WARN level, if a logger is configured |
|
|
116
|
+
| `:raise` | Raises `Broadcast::WarningError` |
|
|
117
|
+
| `:ignore` | Left on the response for you to inspect |
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Recommended in CI or a test suite: turn silent parameter drops into failures
|
|
121
|
+
client = Broadcast::Client.new(api_token: '...', host: '...', warnings_mode: :raise)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> `:raise` fires **after** the request succeeded. Rescuing `WarningError` does
|
|
125
|
+
> not mean the write was rolled back — the subscriber was still created, the
|
|
126
|
+
> email was still sent.
|
|
127
|
+
|
|
128
|
+
Common codes: `unrecognized_parameter`, `parameter_ignored`,
|
|
129
|
+
`parameter_overridden`, `double_opt_in_skipped`.
|
|
130
|
+
|
|
131
|
+
## Rate Limits
|
|
132
|
+
|
|
133
|
+
Every response carries the current limit state, and 429s are retried
|
|
134
|
+
automatically honouring the server's `Retry-After` (capped at `max_retry_delay`).
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
result = client.subscribers.list
|
|
138
|
+
|
|
139
|
+
result.rate_limit.limit # => 120
|
|
140
|
+
result.rate_limit.remaining # => 118
|
|
141
|
+
result.rate_limit.reset # => 2026-07-26 12:00:00 UTC
|
|
142
|
+
|
|
143
|
+
# Back off before you get throttled
|
|
144
|
+
sleep 1 if result.rate_limit.remaining < 10
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
If the retries are exhausted you get a `Broadcast::RateLimitError`, which
|
|
148
|
+
carries `#retry_after` so you can requeue the job sensibly.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
70
152
|
## Rails ActionMailer Integration
|
|
71
153
|
|
|
72
154
|
In a Rails app, the gem auto-registers a `:broadcast` delivery method. Your existing mailers work unchanged.
|
|
@@ -91,7 +173,7 @@ Then configure production to use Broadcast:
|
|
|
91
173
|
config.action_mailer.delivery_method = :broadcast
|
|
92
174
|
config.action_mailer.broadcast_settings = {
|
|
93
175
|
api_token: Rails.application.credentials.dig(:broadcast, :api_token),
|
|
94
|
-
host: 'https://
|
|
176
|
+
host: 'https://mail.example.com'
|
|
95
177
|
}
|
|
96
178
|
```
|
|
97
179
|
|
|
@@ -133,7 +215,7 @@ Replace in `config/environments/production.rb`:
|
|
|
133
215
|
+ config.action_mailer.delivery_method = :broadcast
|
|
134
216
|
+ config.action_mailer.broadcast_settings = {
|
|
135
217
|
+ api_token: Rails.application.credentials.dig(:broadcast, :api_token),
|
|
136
|
-
+ host: 'https://
|
|
218
|
+
+ host: 'https://mail.example.com'
|
|
137
219
|
+ }
|
|
138
220
|
```
|
|
139
221
|
|
|
@@ -213,6 +295,47 @@ client.transactionals.create(
|
|
|
213
295
|
client.transactionals.get_transactional(42)
|
|
214
296
|
```
|
|
215
297
|
|
|
298
|
+
### Idempotent Sends
|
|
299
|
+
|
|
300
|
+
Pass `idempotency_key:` to make a retry safe. The server stores the response for
|
|
301
|
+
24 hours keyed on (token, key) and replays it instead of sending a second email
|
|
302
|
+
— so a job that times out after the send but before the ack won't double-mail
|
|
303
|
+
your customer.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
result = client.transactionals.create(
|
|
307
|
+
to: 'user@example.com',
|
|
308
|
+
subject: "Receipt for order ##{order.id}",
|
|
309
|
+
body: receipt_html,
|
|
310
|
+
idempotency_key: "receipt-#{order.id}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
result.idempotent_replay? # => true if this replayed a stored response
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Use a key derived from the thing you're emailing about (`"receipt-#{order.id}"`),
|
|
317
|
+
not a random UUID generated per attempt — a fresh UUID on each retry defeats the
|
|
318
|
+
whole mechanism.
|
|
319
|
+
|
|
320
|
+
Two failure modes worth handling separately:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
begin
|
|
324
|
+
client.transactionals.create(to: ..., idempotency_key: key)
|
|
325
|
+
rescue Broadcast::ConflictError
|
|
326
|
+
# 409 — the original request is still in flight. Retry shortly; do not
|
|
327
|
+
# change the key, or you'll send twice.
|
|
328
|
+
retry_job(wait: 5.seconds)
|
|
329
|
+
rescue Broadcast::ValidationError => e
|
|
330
|
+
# 422 with an idempotency key can mean the key was already used with a
|
|
331
|
+
# DIFFERENT payload. Retrying with the same key will never succeed.
|
|
332
|
+
raise
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The fingerprint covers method, full path (including query string), and body.
|
|
337
|
+
Changing any of them while reusing a key is what triggers that 422.
|
|
338
|
+
|
|
216
339
|
### Double Opt-In
|
|
217
340
|
|
|
218
341
|
Pass `double_opt_in: true` to require email confirmation before delivery. The recipient receives a confirmation email; the actual transactional email is held until they confirm. If the recipient is already a confirmed subscriber, `double_opt_in` is ignored and the email sends normally.
|
|
@@ -267,13 +390,22 @@ result['subscribers'] # => [{'email' => '...', 'tags' => [...], ...},
|
|
|
267
390
|
result['pagination']['total'] # => 1500
|
|
268
391
|
result['pagination']['current'] # => 1
|
|
269
392
|
|
|
270
|
-
# Filter by status, tags, dates, or custom data
|
|
393
|
+
# Filter by status, tags, dates, or custom data -- all combinable
|
|
271
394
|
client.subscribers.list(is_active: true)
|
|
272
|
-
client.subscribers.list(
|
|
273
|
-
client.subscribers.list(
|
|
274
|
-
client.subscribers.list(
|
|
395
|
+
client.subscribers.list(source: 'opt_in_form')
|
|
396
|
+
client.subscribers.list(tags: ['newsletter', 'premium']) # AND -- must have all
|
|
397
|
+
client.subscribers.list(email: 'example.com') # partial match, not exact
|
|
398
|
+
client.subscribers.list(confirmation_status: 'unconfirmed')
|
|
399
|
+
client.subscribers.list(created_after: '2026-01-01T00:00:00Z',
|
|
400
|
+
created_before: '2026-02-01T00:00:00Z')
|
|
401
|
+
client.subscribers.list(custom_data: { plan: 'pro' }) # JSONB containment
|
|
275
402
|
```
|
|
276
403
|
|
|
404
|
+
> An unparseable `created_after` / `created_before` is **ignored** by the server
|
|
405
|
+
> rather than rejected — you get every subscriber back, plus a
|
|
406
|
+
> `parameter_ignored` warning. Check `result.warnings` (or run with
|
|
407
|
+
> `warnings_mode: :raise`) if a filtered count looks too high.
|
|
408
|
+
|
|
277
409
|
```ruby
|
|
278
410
|
# Find by email
|
|
279
411
|
subscriber = client.subscribers.find(email: 'jane@example.com')
|
|
@@ -871,6 +1003,20 @@ result = client.webhook_endpoints.create(
|
|
|
871
1003
|
# IMPORTANT: Save the secret from the response -- it is only shown once
|
|
872
1004
|
secret = result['secret']
|
|
873
1005
|
|
|
1006
|
+
# Every valid event type is available as a constant. An unknown event type is
|
|
1007
|
+
# dropped server-side rather than rejected, so subscribe from these.
|
|
1008
|
+
Broadcast::Webhook::EVENT_TYPES # all 32
|
|
1009
|
+
Broadcast::Webhook::EMAIL_EVENTS # email.sent, email.delivered, ...
|
|
1010
|
+
Broadcast::Webhook::SUBSCRIBER_EVENTS # subscriber.created, ...
|
|
1011
|
+
Broadcast::Webhook::BROADCAST_EVENTS # broadcast.sending, broadcast.sent, ...
|
|
1012
|
+
Broadcast::Webhook::SEQUENCE_EVENTS # sequence.subscriber_added, ...
|
|
1013
|
+
Broadcast::Webhook::SYSTEM_EVENTS # message.attempt.exhausted, test.webhook
|
|
1014
|
+
|
|
1015
|
+
client.webhook_endpoints.create(
|
|
1016
|
+
url: 'https://yourapp.com/webhooks/broadcast',
|
|
1017
|
+
event_types: Broadcast::Webhook::EMAIL_EVENTS
|
|
1018
|
+
)
|
|
1019
|
+
|
|
874
1020
|
# Update (url and secret cannot be changed -- create a new endpoint instead)
|
|
875
1021
|
client.webhook_endpoints.update(1, active: false)
|
|
876
1022
|
client.webhook_endpoints.update(1, event_types: ['email.delivered', 'email.opened'])
|
|
@@ -933,6 +1079,106 @@ The signature is computed as `HMAC-SHA256(timestamp + "." + payload, secret)`. T
|
|
|
933
1079
|
|
|
934
1080
|
---
|
|
935
1081
|
|
|
1082
|
+
## Discovery
|
|
1083
|
+
|
|
1084
|
+
Ask the instance what this token can do and whether the channel is ready to
|
|
1085
|
+
send. Useful as a deploy-time smoke check, and as the entry point for agents
|
|
1086
|
+
and CLIs.
|
|
1087
|
+
|
|
1088
|
+
```ruby
|
|
1089
|
+
# Who am I? Token type, permissions, resolved channel.
|
|
1090
|
+
me = client.whoami
|
|
1091
|
+
me['token']['type'] # => 'channel_scoped' or 'admin_cross_channel'
|
|
1092
|
+
me['token']['permissions'] # => { 'subscribers' => ['read', 'write'], ... }
|
|
1093
|
+
me['channel']['name'] # => 'Main'
|
|
1094
|
+
|
|
1095
|
+
# Is this channel actually able to send?
|
|
1096
|
+
status = client.status
|
|
1097
|
+
status['subscribers']['active'] # => 1042
|
|
1098
|
+
status['readiness']['broadcasts'] # => true
|
|
1099
|
+
status['readiness']['sequences'] # => false — no email server configured
|
|
1100
|
+
|
|
1101
|
+
# Full capability manifest: platform version, endpoint list, rate limit, tips.
|
|
1102
|
+
client.prime
|
|
1103
|
+
|
|
1104
|
+
# Plain-text agent skill manifest (Markdown + YAML front matter).
|
|
1105
|
+
# Returns a String, not a Hash.
|
|
1106
|
+
puts client.skill
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
A useful preflight before a send job:
|
|
1110
|
+
|
|
1111
|
+
```ruby
|
|
1112
|
+
raise 'channel not ready to send' unless client.status.dig('readiness', 'broadcasts')
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
## Export & Migration (admin tokens only)
|
|
1118
|
+
|
|
1119
|
+
Read-only endpoints under `/api/migration/v1` for backups and moving a channel
|
|
1120
|
+
between instances. Two constraints differ from the rest of the API:
|
|
1121
|
+
|
|
1122
|
+
- **Admin tokens only.** Channel-scoped tokens are rejected.
|
|
1123
|
+
- **`broadcast_channel_id` is required on every call.** Set it once on the
|
|
1124
|
+
client and the gem attaches it for you.
|
|
1125
|
+
|
|
1126
|
+
```ruby
|
|
1127
|
+
client = Broadcast::Client.new(
|
|
1128
|
+
api_token: ENV['BROADCAST_ADMIN_TOKEN'],
|
|
1129
|
+
host: 'https://mail.example.com',
|
|
1130
|
+
broadcast_channel_id: 1
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
# Size the export first
|
|
1134
|
+
manifest = client.migration.manifest
|
|
1135
|
+
manifest['counts'] # => { 'subscribers' => 5000, 'templates' => 12, ... }
|
|
1136
|
+
manifest['export_format_version']
|
|
1137
|
+
|
|
1138
|
+
# Time-bounded counts (broadcasts, receipts, histories) default to 90 days
|
|
1139
|
+
client.migration.manifest(days_history: 365)
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
Each collection is a paginated list returning `data` and `pagination`:
|
|
1143
|
+
|
|
1144
|
+
```ruby
|
|
1145
|
+
page = client.migration.subscribers(limit: 250, offset: 0)
|
|
1146
|
+
page['data']
|
|
1147
|
+
page['pagination'] # => { 'total' => 5000, 'limit' => 250, 'offset' => 0, 'has_more' => true }
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
`each_record` handles the paging for you, advancing by the page size the server
|
|
1151
|
+
actually granted rather than the one you asked for:
|
|
1152
|
+
|
|
1153
|
+
```ruby
|
|
1154
|
+
CSV.open('subscribers.csv', 'w') do |csv|
|
|
1155
|
+
client.migration.each_record(:subscribers) do |subscriber|
|
|
1156
|
+
csv << [subscriber['email'], subscriber['first_name'], subscriber['created_at']]
|
|
1157
|
+
end
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
# Without a block you get an Enumerator
|
|
1161
|
+
client.migration.each_record(:tags).map { |tag| tag['name'] }
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
Available collections:
|
|
1165
|
+
|
|
1166
|
+
```
|
|
1167
|
+
channels subscribers templates segments sequences email_servers
|
|
1168
|
+
opt_in_forms broadcasts outbound_receipts webhook_endpoints tokens
|
|
1169
|
+
suppressions tags users link_redirects link_clicks
|
|
1170
|
+
subscriber_histories file_assets
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
File assets are downloaded separately and return raw bytes:
|
|
1174
|
+
|
|
1175
|
+
```ruby
|
|
1176
|
+
bytes = client.migration.download_file_asset(7)
|
|
1177
|
+
File.binwrite('logo.png', bytes)
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
---
|
|
1181
|
+
|
|
936
1182
|
## Error Handling
|
|
937
1183
|
|
|
938
1184
|
All API errors inherit from `Broadcast::Error`. Put specific errors before general ones:
|
|
@@ -943,15 +1189,24 @@ begin
|
|
|
943
1189
|
rescue Broadcast::AuthenticationError # 401 -- invalid or expired API token
|
|
944
1190
|
rescue Broadcast::AuthorizationError # 403 -- token lacks the required permission, or admin-only endpoint
|
|
945
1191
|
rescue Broadcast::NotFoundError # 404 -- resource does not exist
|
|
1192
|
+
rescue Broadcast::ConflictError # 409 -- Idempotency-Key request still in flight
|
|
946
1193
|
rescue Broadcast::ValidationError # 422 -- missing or invalid parameters
|
|
947
|
-
rescue Broadcast::RateLimitError # 429 --
|
|
1194
|
+
rescue Broadcast::RateLimitError # 429 -- rate limited; carries #retry_after
|
|
948
1195
|
rescue Broadcast::TimeoutError # connection or read timeout
|
|
949
1196
|
rescue Broadcast::APIError # 5xx or unexpected status codes
|
|
1197
|
+
rescue Broadcast::WarningError # warnings_mode: :raise -- request SUCCEEDED
|
|
1198
|
+
rescue Broadcast::ConfigurationError # missing api_token or host
|
|
950
1199
|
rescue Broadcast::DeliveryError # ActionMailer wrapper (wraps any of the above)
|
|
951
1200
|
end
|
|
952
1201
|
```
|
|
953
1202
|
|
|
954
|
-
Server errors (5xx)
|
|
1203
|
+
Server errors (5xx), timeouts, and rate limits (429) are retried automatically —
|
|
1204
|
+
429s honour the server's `Retry-After`, bounded by `max_retry_delay`. Other
|
|
1205
|
+
client errors (401, 403, 404, 409, 422) are raised immediately.
|
|
1206
|
+
|
|
1207
|
+
`DeliveryError` is only raised from the ActionMailer delivery method and wraps
|
|
1208
|
+
the underlying API error. `WarningError` is deliberately not wrapped: the email
|
|
1209
|
+
was sent, so reporting it as a delivery failure would be wrong.
|
|
955
1210
|
|
|
956
1211
|
---
|
|
957
1212
|
|
|
@@ -978,7 +1233,7 @@ Each token can be scoped to specific resources. The ActionMailer delivery method
|
|
|
978
1233
|
### `Broadcast::AuthenticationError` (401)
|
|
979
1234
|
|
|
980
1235
|
- **Wrong token:** Double-check you copied the full token from your Broadcast dashboard.
|
|
981
|
-
- **Wrong host:**
|
|
1236
|
+
- **Wrong host:** Make sure `host` points at your own Broadcast instance. There is no default.
|
|
982
1237
|
- **Missing permissions:** Your token may not have the required permissions for the resource you're accessing. Check the [permissions table](#api-token-permissions).
|
|
983
1238
|
|
|
984
1239
|
### `Broadcast::AuthorizationError` (403)
|