broadcast-ruby 0.1.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 +7 -0
- data/.rubocop.yml +44 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +105 -0
- data/LICENSE.txt +21 -0
- data/README.md +522 -0
- data/Rakefile +15 -0
- data/lib/broadcast/client.rb +175 -0
- data/lib/broadcast/configuration.rb +31 -0
- data/lib/broadcast/errors.rb +19 -0
- data/lib/broadcast/resources/base.rb +25 -0
- data/lib/broadcast/resources/broadcasts.rb +54 -0
- data/lib/broadcast/resources/segments.rb +28 -0
- data/lib/broadcast/resources/sequences.rb +68 -0
- data/lib/broadcast/resources/subscribers.rb +51 -0
- data/lib/broadcast/resources/templates.rb +27 -0
- data/lib/broadcast/resources/webhook_endpoints.rb +35 -0
- data/lib/broadcast/version.rb +5 -0
- data/lib/broadcast/webhook.rb +51 -0
- data/lib/broadcast.rb +17 -0
- metadata +79 -0
data/README.md
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# broadcast-ruby
|
|
2
|
+
|
|
3
|
+
Ruby client for the [Broadcast](https://sendbroadcast.com) email platform.
|
|
4
|
+
|
|
5
|
+
Works with [sendbroadcast.com](https://sendbroadcast.com) or any self-hosted Broadcast instance.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'broadcast-ruby'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
client = Broadcast::Client.new(
|
|
17
|
+
api_token: 'your-token',
|
|
18
|
+
host: 'https://sendbroadcast.com' # or your self-hosted URL
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
client.send_email(
|
|
22
|
+
to: 'jane@example.com',
|
|
23
|
+
subject: 'Welcome!',
|
|
24
|
+
body: '<h1>Hello Jane</h1><p>Welcome aboard.</p>'
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Options
|
|
29
|
+
|
|
30
|
+
| Option | Default | Description |
|
|
31
|
+
|--------|---------|-------------|
|
|
32
|
+
| `api_token` | *required* | Your Broadcast API token |
|
|
33
|
+
| `host` | `https://sendbroadcast.com` | Broadcast instance URL |
|
|
34
|
+
| `timeout` | `30` | Read timeout (seconds) |
|
|
35
|
+
| `open_timeout` | `10` | Connection timeout (seconds) |
|
|
36
|
+
| `retry_attempts` | `3` | Max total attempts (1 initial + 2 retries). Server errors (5xx) and timeouts are retried; client errors (4xx) are not |
|
|
37
|
+
| `retry_delay` | `1` | Base delay between retries in seconds (multiplied by attempt number) |
|
|
38
|
+
| `debug` | `false` | Log request/response details |
|
|
39
|
+
| `logger` | `nil` | Logger instance for debug output (e.g. `Rails.logger`) |
|
|
40
|
+
|
|
41
|
+
All methods return parsed JSON as Ruby Hashes with string keys.
|
|
42
|
+
|
|
43
|
+
> **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.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Transactional Email
|
|
48
|
+
|
|
49
|
+
Send one-off emails triggered by application events. Transactional emails bypass unsubscribe status (for password resets, receipts, order confirmations, etc.).
|
|
50
|
+
|
|
51
|
+
The sender address is configured at the channel level in your Broadcast instance — there is no `from` parameter.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Send an email
|
|
55
|
+
result = client.send_email(
|
|
56
|
+
to: 'user@example.com',
|
|
57
|
+
subject: 'Your password reset link',
|
|
58
|
+
body: '<p>Click <a href="...">here</a> to reset your password.</p>',
|
|
59
|
+
reply_to: 'support@yourapp.com' # optional
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
result['id'] # => 42
|
|
63
|
+
result['recipient_email'] # => 'user@example.com'
|
|
64
|
+
result['status_url'] # => '/api/v1/transactionals/42.json'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Check delivery status
|
|
69
|
+
email = client.get_email(42)
|
|
70
|
+
email['sent_at'] # => '2026-03-17T08:00:00Z' (nil if not yet sent)
|
|
71
|
+
email['queue_at'] # => '2026-03-17T07:59:58Z'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Parameters for `send_email`:**
|
|
75
|
+
|
|
76
|
+
| Param | Required | Description |
|
|
77
|
+
|-------|----------|-------------|
|
|
78
|
+
| `to` | yes | Recipient email address |
|
|
79
|
+
| `subject` | yes | Email subject line |
|
|
80
|
+
| `body` | yes | Email content (HTML or plain text) |
|
|
81
|
+
| `reply_to` | no | Reply-to address |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Subscribers
|
|
86
|
+
|
|
87
|
+
Manage your contact list. Subscribers can have tags and custom data fields for segmentation and personalization.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# List subscribers (paginated, 250 per page)
|
|
91
|
+
result = client.subscribers.list(page: 1)
|
|
92
|
+
result['subscribers'] # => [{'email' => '...', 'tags' => [...], ...}, ...]
|
|
93
|
+
result['pagination']['total'] # => 1500
|
|
94
|
+
result['pagination']['current'] # => 1
|
|
95
|
+
|
|
96
|
+
# Filter by status, tags, dates, or custom data
|
|
97
|
+
client.subscribers.list(is_active: true)
|
|
98
|
+
client.subscribers.list(tags: ['newsletter', 'premium'])
|
|
99
|
+
client.subscribers.list(created_after: '2026-01-01T00:00:00Z')
|
|
100
|
+
client.subscribers.list(custom_data: { plan: 'pro' })
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# Find by email
|
|
105
|
+
subscriber = client.subscribers.find(email: 'jane@example.com')
|
|
106
|
+
subscriber['email'] # => 'jane@example.com'
|
|
107
|
+
subscriber['first_name'] # => 'Jane'
|
|
108
|
+
subscriber['tags'] # => ['newsletter', 'premium']
|
|
109
|
+
subscriber['custom_data'] # => {'plan' => 'pro'}
|
|
110
|
+
subscriber['is_active'] # => true
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# Create a subscriber
|
|
115
|
+
# Returns the created subscriber as a Hash
|
|
116
|
+
client.subscribers.create(
|
|
117
|
+
email: 'jane@example.com',
|
|
118
|
+
first_name: 'Jane',
|
|
119
|
+
last_name: 'Doe',
|
|
120
|
+
tags: ['newsletter', 'free-tier'],
|
|
121
|
+
custom_data: { plan: 'free', signup_source: 'landing_page' }
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Update a subscriber (identified by email)
|
|
125
|
+
# Returns the updated subscriber as a Hash
|
|
126
|
+
client.subscribers.update('jane@example.com',
|
|
127
|
+
first_name: 'Janet',
|
|
128
|
+
tags: ['newsletter', 'premium'], # replaces all tags
|
|
129
|
+
custom_data: { plan: 'pro' }
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Change a subscriber's email address
|
|
133
|
+
client.subscribers.update('old@example.com', email: 'new@example.com')
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Add tags without replacing existing ones
|
|
138
|
+
client.subscribers.add_tags('jane@example.com', ['vip', 'early-adopter'])
|
|
139
|
+
|
|
140
|
+
# Remove specific tags
|
|
141
|
+
client.subscribers.remove_tags('jane@example.com', ['free-tier'])
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# Deactivate (stop all email delivery, keep record)
|
|
146
|
+
client.subscribers.deactivate('jane@example.com')
|
|
147
|
+
|
|
148
|
+
# Activate (resume email delivery)
|
|
149
|
+
client.subscribers.activate('jane@example.com')
|
|
150
|
+
|
|
151
|
+
# Unsubscribe (marks as unsubscribed AND deactivates)
|
|
152
|
+
client.subscribers.unsubscribe('jane@example.com')
|
|
153
|
+
|
|
154
|
+
# Resubscribe (clears unsubscribed status AND activates)
|
|
155
|
+
client.subscribers.resubscribe('jane@example.com')
|
|
156
|
+
|
|
157
|
+
# Redact — GDPR "right to be forgotten" (irreversible)
|
|
158
|
+
# Removes PII but preserves aggregate campaign statistics
|
|
159
|
+
client.subscribers.redact('jane@example.com')
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Sequences
|
|
165
|
+
|
|
166
|
+
Automated drip campaigns. Add subscribers to a sequence and they flow through the steps automatically.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# List all sequences
|
|
170
|
+
result = client.sequences.list
|
|
171
|
+
result # => [{'id' => 1, 'label' => 'Onboarding', 'active' => true, 'subscribers_count' => 42, ...}, ...]
|
|
172
|
+
|
|
173
|
+
# Get a single sequence
|
|
174
|
+
sequence = client.sequences.get_sequence(1)
|
|
175
|
+
|
|
176
|
+
# Get with steps included
|
|
177
|
+
sequence = client.sequences.get_sequence(1, include_steps: true)
|
|
178
|
+
sequence['steps'] # => [{'id' => 10, 'action' => 'delay', ...}, ...]
|
|
179
|
+
|
|
180
|
+
# Create
|
|
181
|
+
result = client.sequences.create(label: 'Onboarding', active: false)
|
|
182
|
+
result['id'] # => 5
|
|
183
|
+
|
|
184
|
+
# Create with auto-enrollment by tag
|
|
185
|
+
# Subscribers are auto-added when this tag is applied to them
|
|
186
|
+
client.sequences.create(
|
|
187
|
+
label: 'Free Trial Nurture',
|
|
188
|
+
init_tag: 'free-trial',
|
|
189
|
+
active: true
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Create with auto-enrollment by segment
|
|
193
|
+
# Matching subscribers are synced every 15 minutes
|
|
194
|
+
client.sequences.create(
|
|
195
|
+
label: 'Re-engagement',
|
|
196
|
+
init_segment_id: 5,
|
|
197
|
+
active: true
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Update
|
|
201
|
+
client.sequences.update(1, label: 'Updated Onboarding', active: true)
|
|
202
|
+
|
|
203
|
+
# Delete (and all its steps)
|
|
204
|
+
client.sequences.delete(1)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Subscriber Enrollment
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# Add subscriber to sequence (creates subscriber if new)
|
|
211
|
+
client.sequences.add_subscriber(1, email: 'jane@example.com', first_name: 'Jane')
|
|
212
|
+
|
|
213
|
+
# Remove subscriber from sequence
|
|
214
|
+
client.sequences.remove_subscriber(1, email: 'jane@example.com')
|
|
215
|
+
|
|
216
|
+
# List subscribers in a sequence
|
|
217
|
+
# Returns subscriber_sequences objects with enrollment status
|
|
218
|
+
result = client.sequences.list_subscribers(1, page: 1)
|
|
219
|
+
result['subscriber_sequences']
|
|
220
|
+
# => [{'id' => 1, 'status' => 'active', 'started_at' => '...', 'next_trigger_at' => '...',
|
|
221
|
+
# 'subscriber' => {'id' => '123', 'email' => 'jane@example.com'}}, ...]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Steps
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# List steps in a sequence
|
|
228
|
+
client.sequences.list_steps(sequence_id)
|
|
229
|
+
|
|
230
|
+
# Get a single step
|
|
231
|
+
step = client.sequences.get_step(sequence_id, step_id)
|
|
232
|
+
|
|
233
|
+
# Create steps — each step needs an action, label, and parent_id
|
|
234
|
+
# The parent_id links steps into a tree (entry point is the sequence's root step)
|
|
235
|
+
|
|
236
|
+
# Delay step (seconds)
|
|
237
|
+
client.sequences.create_step(sequence_id,
|
|
238
|
+
action: 'delay', label: 'Wait 1 day',
|
|
239
|
+
parent_id: entry_point_id, delay: 86400
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Email step
|
|
243
|
+
client.sequences.create_step(sequence_id,
|
|
244
|
+
action: 'send_email', label: 'Welcome email',
|
|
245
|
+
parent_id: delay_step_id,
|
|
246
|
+
subject: 'Welcome!', body: '<h1>Welcome!</h1>'
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Condition step (branches based on engagement)
|
|
250
|
+
client.sequences.create_step(sequence_id,
|
|
251
|
+
action: 'condition', label: 'Opened welcome email?',
|
|
252
|
+
parent_id: email_step_id,
|
|
253
|
+
condition_setting: 'previous_email_opened'
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Tag step
|
|
257
|
+
client.sequences.create_step(sequence_id,
|
|
258
|
+
action: 'add_tag_to_subscriber', label: 'Tag as engaged',
|
|
259
|
+
parent_id: condition_yes_id, taggify_list: 'engaged,onboarded'
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Update a step
|
|
263
|
+
client.sequences.update_step(sequence_id, step_id, label: 'Updated')
|
|
264
|
+
|
|
265
|
+
# Move a step under a different parent
|
|
266
|
+
client.sequences.move_step(sequence_id, step_id, under_id: new_parent_id)
|
|
267
|
+
|
|
268
|
+
# Delete a step
|
|
269
|
+
client.sequences.delete_step(sequence_id, step_id)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Step actions:** `send_email`, `delay`, `delay_until_time`, `condition`, `move_to_sequence`, `add_tag_to_subscriber`, `remove_tag_from_subscriber`, `deactivate_subscriber`, `make_http_request`
|
|
273
|
+
|
|
274
|
+
**Condition settings:** `any_email_opened`, `previous_email_opened`, `any_email_clicked`, `previous_email_clicked`
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Broadcasts
|
|
279
|
+
|
|
280
|
+
One-time email campaigns sent to your subscriber list or targeted segments.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# List broadcasts
|
|
284
|
+
result = client.broadcasts.list(limit: 10, offset: 0)
|
|
285
|
+
# => [{'id' => 1, 'name' => '...', 'subject' => '...', 'status' => 'draft', ...}, ...]
|
|
286
|
+
|
|
287
|
+
# Get a broadcast
|
|
288
|
+
broadcast = client.broadcasts.get_broadcast(1)
|
|
289
|
+
broadcast['status'] # => 'draft'
|
|
290
|
+
|
|
291
|
+
# Create a broadcast (starts as draft)
|
|
292
|
+
result = client.broadcasts.create(
|
|
293
|
+
subject: 'March Newsletter',
|
|
294
|
+
body: '<h1>What is new</h1><p>Updates...</p>',
|
|
295
|
+
name: 'march-2026-newsletter',
|
|
296
|
+
preheader: 'Product updates and tips',
|
|
297
|
+
reply_to: 'hello@yourapp.com',
|
|
298
|
+
track_opens: true,
|
|
299
|
+
track_clicks: true,
|
|
300
|
+
segment_ids: [1, 3],
|
|
301
|
+
taggify_list: 'newsletter,march-2026'
|
|
302
|
+
)
|
|
303
|
+
result['id'] # => 5
|
|
304
|
+
|
|
305
|
+
# Update (draft or scheduled only)
|
|
306
|
+
client.broadcasts.update(1, subject: 'Updated Subject')
|
|
307
|
+
|
|
308
|
+
# Send immediately (draft or failed only)
|
|
309
|
+
result = client.broadcasts.send_broadcast(1)
|
|
310
|
+
result['status'] # => 'queueing'
|
|
311
|
+
|
|
312
|
+
# Schedule for later
|
|
313
|
+
result = client.broadcasts.schedule(1,
|
|
314
|
+
scheduled_send_at: '2026-03-20T09:00:00Z',
|
|
315
|
+
scheduled_timezone: 'America/Toronto'
|
|
316
|
+
)
|
|
317
|
+
result['status'] # => 'future_scheduled'
|
|
318
|
+
|
|
319
|
+
# Cancel a scheduled broadcast (returns to draft)
|
|
320
|
+
client.broadcasts.cancel_schedule(1)
|
|
321
|
+
|
|
322
|
+
# Delete (draft or scheduled only)
|
|
323
|
+
client.broadcasts.delete(1)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Statistics
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
# Delivery and engagement stats
|
|
330
|
+
stats = client.broadcasts.statistics(1)
|
|
331
|
+
stats['delivery']['sent'] # => 1500
|
|
332
|
+
stats['engagement']['opens']['count'] # => 723
|
|
333
|
+
stats['engagement']['clicks']['count'] # => 184
|
|
334
|
+
stats['issues']['bounces']['count'] # => 12
|
|
335
|
+
stats['issues']['unsubscribes']['count'] # => 3
|
|
336
|
+
|
|
337
|
+
# Timeline stats (for charts)
|
|
338
|
+
client.broadcasts.statistics_timeline(1, timeframe: '24h', metrics: 'opens,clicks')
|
|
339
|
+
# timeframes: 60m, 120m, 3h, 6h, 12h, 18h, 24h, 48h, 72h, 7d, 14d
|
|
340
|
+
|
|
341
|
+
# Per-link click stats
|
|
342
|
+
client.broadcasts.statistics_links(1, sort: 'clicks', order: 'desc')
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Broadcast statuses:** `draft`, `future_scheduled`, `scheduled`, `queueing`, `sending`, `sent`, `failed`, `partial_failure`, `aborted`, `paused`
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Segments
|
|
350
|
+
|
|
351
|
+
Define subscriber groups using rules for targeted broadcasts and sequence enrollment.
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
# List segments
|
|
355
|
+
result = client.segments.list
|
|
356
|
+
result['segments'] # => [{'id' => 1, 'name' => 'Active Users', ...}, ...]
|
|
357
|
+
|
|
358
|
+
# Get segment with matching subscribers (paginated)
|
|
359
|
+
result = client.segments.get_segment(1, page: 1)
|
|
360
|
+
result['segment'] # => {'id' => 1, 'name' => 'Active Users', ...}
|
|
361
|
+
result['subscribers'] # => [{'email' => '...', ...}, ...]
|
|
362
|
+
result['pagination']['total'] # => 150
|
|
363
|
+
|
|
364
|
+
# Create a segment with rules
|
|
365
|
+
# Groups are OR'd together; rules within a group are combined by match_type (all = AND, any = OR)
|
|
366
|
+
result = client.segments.create(
|
|
367
|
+
name: 'Active Gmail Users',
|
|
368
|
+
description: 'Gmail users who opened an email in the last 30 days',
|
|
369
|
+
segment_groups_attributes: [
|
|
370
|
+
{
|
|
371
|
+
match_type: 'all',
|
|
372
|
+
segment_rules_attributes: [
|
|
373
|
+
{ field: 'email', operator: 'contains', value: 'gmail.com',
|
|
374
|
+
rule_type: 'text', value_type: 'string' },
|
|
375
|
+
{ field: 'last_email_opened_at', operator: 'within_last_days', value: '30',
|
|
376
|
+
rule_type: 'date', value_type: 'string' }
|
|
377
|
+
]
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
)
|
|
381
|
+
result['id'] # => 5
|
|
382
|
+
|
|
383
|
+
# Update
|
|
384
|
+
client.segments.update(1, name: 'Updated Name')
|
|
385
|
+
|
|
386
|
+
# Delete
|
|
387
|
+
client.segments.delete(1)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Rule fields:** `email`, `first_name`, `last_name`, `tags`, `is_active`, `created_at`, `last_email_sent_at`, `last_email_opened_at`, `last_email_clicked_at`, `total_emails_sent`, `total_emails_opened`, `total_emails_clicked`, `has_opened_any_email`, `has_clicked_any_email`
|
|
391
|
+
|
|
392
|
+
**Operators by type:**
|
|
393
|
+
- **Text:** `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty`
|
|
394
|
+
- **Number:** `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal`, `less_than_or_equal`
|
|
395
|
+
- **Date:** `equals`, `before`, `after`, `within_last_days`, `not_within_last_days`, `never`, `is_empty`, `is_not_empty`
|
|
396
|
+
- **Boolean:** `is_true`, `is_false`
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Templates
|
|
401
|
+
|
|
402
|
+
Reusable email templates with [Liquid](https://shopify.github.io/liquid/) variable support for personalization (e.g. `{{first_name}}`, `{{email}}`).
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
# List all templates
|
|
406
|
+
result = client.templates.list
|
|
407
|
+
result['data'] # => [{'id' => 1, 'label' => 'Welcome', 'subject' => '...', ...}, ...]
|
|
408
|
+
|
|
409
|
+
# Get a template
|
|
410
|
+
template = client.templates.get_template(1)
|
|
411
|
+
template['label'] # => 'Welcome'
|
|
412
|
+
template['subject'] # => 'Hello {{first_name}}'
|
|
413
|
+
template['body'] # => '<h1>Welcome!</h1>'
|
|
414
|
+
|
|
415
|
+
# Create a template
|
|
416
|
+
# Returns {'id' => number}
|
|
417
|
+
client.templates.create(
|
|
418
|
+
label: 'Monthly Newsletter',
|
|
419
|
+
subject: 'Your {{month}} update',
|
|
420
|
+
body: '<h1>Hello {{first_name}}</h1><p>Here is what happened...</p>',
|
|
421
|
+
preheader: 'Your monthly digest',
|
|
422
|
+
html_body: true
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Update
|
|
426
|
+
client.templates.update(1, subject: 'Updated subject')
|
|
427
|
+
|
|
428
|
+
# Delete
|
|
429
|
+
client.templates.delete(1)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Webhook Endpoints
|
|
435
|
+
|
|
436
|
+
Receive real-time notifications when events occur (email delivered, subscriber created, sequence completed, etc.).
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
# List endpoints
|
|
440
|
+
result = client.webhook_endpoints.list
|
|
441
|
+
result['data'] # => [{'id' => 1, 'url' => '...', 'active' => true, ...}, ...]
|
|
442
|
+
|
|
443
|
+
# Get an endpoint
|
|
444
|
+
endpoint = client.webhook_endpoints.get_endpoint(1)
|
|
445
|
+
|
|
446
|
+
# Create an endpoint
|
|
447
|
+
result = client.webhook_endpoints.create(
|
|
448
|
+
url: 'https://yourapp.com/webhooks/broadcast',
|
|
449
|
+
event_types: [
|
|
450
|
+
'email.sent', 'email.delivered', 'email.opened', 'email.clicked',
|
|
451
|
+
'subscriber.created', 'subscriber.unsubscribed',
|
|
452
|
+
'sequence.subscriber_added', 'sequence.subscriber_completed'
|
|
453
|
+
],
|
|
454
|
+
retries_to_attempt: 6
|
|
455
|
+
)
|
|
456
|
+
# IMPORTANT: Save the secret from the response — it is only shown once
|
|
457
|
+
secret = result['secret']
|
|
458
|
+
|
|
459
|
+
# Update (url and secret cannot be changed — create a new endpoint instead)
|
|
460
|
+
client.webhook_endpoints.update(1, active: false)
|
|
461
|
+
client.webhook_endpoints.update(1, event_types: ['email.delivered', 'email.opened'])
|
|
462
|
+
|
|
463
|
+
# Delete
|
|
464
|
+
client.webhook_endpoints.delete(1)
|
|
465
|
+
|
|
466
|
+
# Send a test webhook
|
|
467
|
+
client.webhook_endpoints.test(1, event_type: 'email.delivered')
|
|
468
|
+
|
|
469
|
+
# View delivery history
|
|
470
|
+
result = client.webhook_endpoints.deliveries(1, limit: 10)
|
|
471
|
+
result['data'] # => [{'id' => 1, 'event_type' => 'email.sent', 'response_status' => 200, ...}, ...]
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Event types:**
|
|
475
|
+
|
|
476
|
+
| Category | Events |
|
|
477
|
+
|----------|--------|
|
|
478
|
+
| Email | `email.sent`, `email.delivered`, `email.delivery_delayed`, `email.opened`, `email.clicked`, `email.bounced`, `email.complained`, `email.failed` |
|
|
479
|
+
| Subscriber | `subscriber.created`, `subscriber.updated`, `subscriber.deleted`, `subscriber.subscribed`, `subscriber.unsubscribed`, `subscriber.bounced`, `subscriber.complained` |
|
|
480
|
+
| Broadcast | `broadcast.scheduled`, `broadcast.queueing`, `broadcast.sending`, `broadcast.sent`, `broadcast.failed`, `broadcast.partial_failure`, `broadcast.aborted`, `broadcast.paused` |
|
|
481
|
+
| Sequence | `sequence.subscriber_added`, `sequence.subscriber_completed`, `sequence.subscriber_moved`, `sequence.subscriber_removed`, `sequence.subscriber_paused`, `sequence.subscriber_resumed`, `sequence.subscriber_error` |
|
|
482
|
+
| System | `message.attempt.exhausted`, `test.webhook` |
|
|
483
|
+
|
|
484
|
+
### Webhook Signature Verification
|
|
485
|
+
|
|
486
|
+
All incoming webhooks are signed with HMAC-SHA256. Verify them in your controller:
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
Broadcast::Webhook.verify(
|
|
490
|
+
request.raw_post, # raw request body
|
|
491
|
+
request.headers['broadcast-webhook-signature'], # v1,<base64 signature>
|
|
492
|
+
request.headers['broadcast-webhook-timestamp'], # unix timestamp
|
|
493
|
+
secret: ENV['BROADCAST_WEBHOOK_SECRET']
|
|
494
|
+
)
|
|
495
|
+
# => true or false
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
The signature is computed as `HMAC-SHA256(timestamp + "." + payload, secret)`. Timestamps older than 5 minutes are rejected to prevent replay attacks.
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Error Handling
|
|
503
|
+
|
|
504
|
+
All API errors inherit from `Broadcast::Error`:
|
|
505
|
+
|
|
506
|
+
```ruby
|
|
507
|
+
begin
|
|
508
|
+
client.send_email(to: 'user@example.com', subject: 'Hi', body: 'Hello')
|
|
509
|
+
rescue Broadcast::AuthenticationError # 401 — invalid or expired API token
|
|
510
|
+
rescue Broadcast::NotFoundError # 404 — resource does not exist
|
|
511
|
+
rescue Broadcast::ValidationError # 422 — missing or invalid parameters
|
|
512
|
+
rescue Broadcast::RateLimitError # 429 — exceeded 120 requests/minute
|
|
513
|
+
rescue Broadcast::TimeoutError # connection or read timeout
|
|
514
|
+
rescue Broadcast::APIError # 5xx or unexpected status codes
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Server errors (5xx) and timeouts are automatically retried with linear backoff. Client errors (401, 404, 422, 429) are raised immediately.
|
|
519
|
+
|
|
520
|
+
## License
|
|
521
|
+
|
|
522
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'minitest/test_task'
|
|
5
|
+
|
|
6
|
+
Minitest::TestTask.create do |t|
|
|
7
|
+
t.test_globs = ['test/test_*.rb']
|
|
8
|
+
t.test_prelude = 'ENV["BROADCAST_SKIP_LIVE"] = "1"'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Minitest::TestTask.create(:test_live) do |t|
|
|
12
|
+
t.test_globs = ['test/test_live.rb']
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
task default: :test
|