sendara 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/README.md +44 -12
- data/lib/sendara/checkout_result.rb +38 -0
- data/lib/sendara/client.rb +2 -1
- data/lib/sendara/resources/billing.rb +9 -1
- data/lib/sendara/resources/broadcasts.rb +11 -5
- data/lib/sendara/resources/emails.rb +18 -8
- data/lib/sendara/version.rb +1 -1
- data/lib/sendara.rb +1 -0
- data/sendara.gemspec +2 -1
- metadata +19 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93eb9f0dc2bd668914efca68d65e72f614c23ea0d194f376a77aac5ed7327520
|
|
4
|
+
data.tar.gz: 826da708e6ea0519d6f48827315465261c39382d613008a31f5797f9d6e7d2c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c117085ea0d5d9d82f4da39ca466080e04253b1c1ac2c25584b5841fa61951129c5f4d104f95586443b05592a26e3fe53e05cb040f5993a0559042b635342623
|
|
7
|
+
data.tar.gz: f8c4394b30342443ba05adf6505e03e8b42014561795319bf156f6e7243c3252ac59d34a4ef63ec384ed9ed775a49b932f6c1fe0b8df8dfd6d753b1f02d7ef4b
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sendara Ruby
|
|
2
2
|
|
|
3
|
-
Email-first Ruby client for the [Sendara](https://sendara.dev) API: transactional email, broadcasts, contacts, templates, domains, and signed webhooks.
|
|
3
|
+
Email-first Ruby client for the [Sendara](https://sendara.dev) API: transactional email, broadcasts, contacts, templates, domains, and signed webhooks. It uses Ruby's standard HTTP stack and declares the `base64` default gem for Ruby 3.4+ compatibility.
|
|
4
4
|
|
|
5
5
|
Requires Ruby >= 3.0.
|
|
6
6
|
|
|
@@ -55,7 +55,7 @@ The client automatically attaches an `Idempotency-Key` to every write and retrie
|
|
|
55
55
|
|
|
56
56
|
### Sending email
|
|
57
57
|
|
|
58
|
-
`emails.send` is keyword-based. `
|
|
58
|
+
`emails.send` is keyword-based. `to` is always required; `from` is required after domain verification, and inline sends need a subject plus `html`, `text`, or both. Stored templates can provide their own content:
|
|
59
59
|
|
|
60
60
|
```ruby
|
|
61
61
|
client.emails.send(
|
|
@@ -65,7 +65,9 @@ client.emails.send(
|
|
|
65
65
|
html: "<p>Thanks for your order.</p>",
|
|
66
66
|
text: "Thanks for your order.",
|
|
67
67
|
message_type: "transactional",
|
|
68
|
-
metadata: { "order_id" => "ord_123" }
|
|
68
|
+
metadata: { "order_id" => "ord_123" },
|
|
69
|
+
scheduled_at: "2027-01-15T09:00:00Z",
|
|
70
|
+
validate_recipient: true
|
|
69
71
|
)
|
|
70
72
|
```
|
|
71
73
|
|
|
@@ -122,6 +124,19 @@ broadcast = client.broadcasts.create(
|
|
|
122
124
|
client.broadcasts.send(broadcast["id"])
|
|
123
125
|
```
|
|
124
126
|
|
|
127
|
+
Pass `list_ids:` to send to several contact lists at once, and `exclude_list_ids:`
|
|
128
|
+
to subtract a list from that audience:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
client.broadcasts.create(
|
|
132
|
+
from_email: "you@yourdomain.com",
|
|
133
|
+
subject: "What's new in June",
|
|
134
|
+
body_html: "<h1>June</h1>",
|
|
135
|
+
list_ids: %w[list_123 list_456],
|
|
136
|
+
exclude_list_ids: %w[list_789]
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
125
140
|
Schedule for later, or send immediately on create with `send_now: true`:
|
|
126
141
|
|
|
127
142
|
```ruby
|
|
@@ -130,8 +145,8 @@ client.broadcasts.create(
|
|
|
130
145
|
name: "Launch",
|
|
131
146
|
subject: "We're live",
|
|
132
147
|
body_html: "<p>We launched.</p>",
|
|
133
|
-
recipients: ["a@example.com", "b@example.com"],
|
|
134
|
-
scheduled_at: "
|
|
148
|
+
recipients: [{ "email" => "a@example.com" }, { "email" => "b@example.com" }],
|
|
149
|
+
scheduled_at: "2027-01-15T09:00:00Z"
|
|
135
150
|
)
|
|
136
151
|
```
|
|
137
152
|
|
|
@@ -144,15 +159,16 @@ client.broadcasts.cancel("bcast_123")
|
|
|
144
159
|
client.broadcasts.delete("bcast_123")
|
|
145
160
|
```
|
|
146
161
|
|
|
147
|
-
To create
|
|
162
|
+
To create and immediately send an audience in one request, use `bulk_send`.
|
|
163
|
+
It accepts the same content and audience arguments as `create`, but not
|
|
164
|
+
`scheduled_at` or `send_now` because this endpoint always fans out immediately:
|
|
148
165
|
|
|
149
166
|
```ruby
|
|
150
167
|
client.broadcasts.bulk_send(
|
|
151
168
|
from_email: "you@yourdomain.com",
|
|
152
169
|
subject: "Flash sale",
|
|
153
170
|
body_html: "<p>24 hours only.</p>",
|
|
154
|
-
audience_list_id: "list_123"
|
|
155
|
-
send_now: true
|
|
171
|
+
audience_list_id: "list_123"
|
|
156
172
|
)
|
|
157
173
|
```
|
|
158
174
|
|
|
@@ -189,6 +205,22 @@ client.messages.get("msg_123")
|
|
|
189
205
|
client.messages.get(idempotency_key: "order-42")
|
|
190
206
|
```
|
|
191
207
|
|
|
208
|
+
## Billing checkout
|
|
209
|
+
|
|
210
|
+
Checkout accepts `starter`, `pro`, `growth`, or `scale`, with a `month` or
|
|
211
|
+
`year` billing period. It returns a `Sendara::CheckoutResult` for a new hosted
|
|
212
|
+
checkout or an in-place subscription change:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
checkout = client.billing.checkout(plan: :growth, period: :year)
|
|
216
|
+
|
|
217
|
+
if checkout.updated?
|
|
218
|
+
puts "Subscription changed to #{checkout.plan}"
|
|
219
|
+
else
|
|
220
|
+
redirect_to checkout.url
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
192
224
|
## Webhook verification
|
|
193
225
|
|
|
194
226
|
Verify the signature on incoming webhooks with `Sendara::Webhooks.verify`. Pass the **raw request body** (not parsed), the request headers, and your signing secret. On success it returns the parsed event payload; on failure it raises `Sendara::WebhookVerificationError`.
|
|
@@ -202,7 +234,7 @@ event = Sendara::Webhooks.verify(
|
|
|
202
234
|
ENV.fetch("SENDARA_WEBHOOK_SECRET")
|
|
203
235
|
)
|
|
204
236
|
|
|
205
|
-
event["
|
|
237
|
+
event["event_type"] # => "delivered"
|
|
206
238
|
```
|
|
207
239
|
|
|
208
240
|
`verify` checks the `Sendara-Signature` HMAC and rejects requests whose `Sendara-Timestamp` is outside the tolerance window (default 300 seconds). Adjust it if needed:
|
|
@@ -297,9 +329,9 @@ class SendaraWebhooksController < ActionController::API
|
|
|
297
329
|
Rails.application.credentials.sendara_webhook_secret
|
|
298
330
|
)
|
|
299
331
|
|
|
300
|
-
case event["
|
|
301
|
-
when "
|
|
302
|
-
when "
|
|
332
|
+
case event["event_type"]
|
|
333
|
+
when "delivered" then handle_delivered(event)
|
|
334
|
+
when "bounced" then handle_bounced(event)
|
|
303
335
|
end
|
|
304
336
|
|
|
305
337
|
head :ok
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendara
|
|
4
|
+
# Result of POST /v1/billing/checkout. A new subscription has a URL to
|
|
5
|
+
# redirect to; an existing subscription reports the plan changed in place.
|
|
6
|
+
class CheckoutResult
|
|
7
|
+
PLANS = %w[starter pro growth scale].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :url, :plan
|
|
10
|
+
|
|
11
|
+
def self.from_response(response)
|
|
12
|
+
url = response["url"]
|
|
13
|
+
return new(url: url, updated: false) if url.is_a?(String) && !url.empty?
|
|
14
|
+
|
|
15
|
+
plan = response["plan"]
|
|
16
|
+
if response["updated"] == true && PLANS.include?(plan)
|
|
17
|
+
return new(updated: true, plan: plan)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
raise Error, "Billing checkout returned neither a checkout URL nor an updated plan"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(url: nil, updated:, plan: nil)
|
|
24
|
+
@url = url
|
|
25
|
+
@updated = updated
|
|
26
|
+
@plan = plan
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def updated?
|
|
31
|
+
@updated
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def redirect?
|
|
35
|
+
!@url.nil?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/sendara/client.rb
CHANGED
|
@@ -3,14 +3,22 @@
|
|
|
3
3
|
module Sendara
|
|
4
4
|
module Resources
|
|
5
5
|
class Billing < Resource
|
|
6
|
+
PLANS = %w[starter pro growth scale].freeze
|
|
7
|
+
PERIODS = %w[month year].freeze
|
|
8
|
+
|
|
6
9
|
def get
|
|
7
10
|
request(:get, "/v1/billing") || {}
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def checkout(plan: nil, period: nil)
|
|
14
|
+
plan = plan.to_s unless plan.nil?
|
|
15
|
+
period = period.to_s unless period.nil?
|
|
16
|
+
raise ArgumentError, "plan must be starter, pro, growth, or scale" if plan && !PLANS.include?(plan)
|
|
17
|
+
raise ArgumentError, "period must be month or year" if period && !PERIODS.include?(period)
|
|
18
|
+
|
|
11
19
|
body = compact_params("plan" => plan, "period" => period)
|
|
12
20
|
response = request(:post, "/v1/billing/checkout", body: body) || {}
|
|
13
|
-
response
|
|
21
|
+
CheckoutResult.from_response(response)
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def portal
|
|
@@ -16,11 +16,13 @@ module Sendara
|
|
|
16
16
|
|
|
17
17
|
def create(from_email:, name: nil, subject: nil, body_html: nil, body_text: nil,
|
|
18
18
|
template_id: nil, message_type: nil, audience_list_id: nil,
|
|
19
|
+
list_ids: nil, exclude_list_ids: nil,
|
|
19
20
|
recipients: nil, scheduled_at: nil, send_now: nil)
|
|
20
21
|
request(:post, "/v1/broadcasts", body: broadcast_body(
|
|
21
22
|
from_email: from_email, name: name, subject: subject, body_html: body_html,
|
|
22
23
|
body_text: body_text, template_id: template_id, message_type: message_type,
|
|
23
|
-
audience_list_id: audience_list_id,
|
|
24
|
+
audience_list_id: audience_list_id, list_ids: list_ids,
|
|
25
|
+
exclude_list_ids: exclude_list_ids, recipients: recipients,
|
|
24
26
|
scheduled_at: scheduled_at, send_now: send_now
|
|
25
27
|
)) || {}
|
|
26
28
|
end
|
|
@@ -40,19 +42,21 @@ module Sendara
|
|
|
40
42
|
|
|
41
43
|
def bulk_send(from_email:, name: nil, subject: nil, body_html: nil, body_text: nil,
|
|
42
44
|
template_id: nil, message_type: nil, audience_list_id: nil,
|
|
43
|
-
|
|
45
|
+
list_ids: nil, exclude_list_ids: nil, recipients: nil)
|
|
44
46
|
request(:post, "/v1/send/bulk", body: broadcast_body(
|
|
45
47
|
from_email: from_email, name: name, subject: subject, body_html: body_html,
|
|
46
48
|
body_text: body_text, template_id: template_id, message_type: message_type,
|
|
47
|
-
audience_list_id: audience_list_id,
|
|
48
|
-
|
|
49
|
+
audience_list_id: audience_list_id, list_ids: list_ids,
|
|
50
|
+
exclude_list_ids: exclude_list_ids, recipients: recipients,
|
|
51
|
+
scheduled_at: nil, send_now: nil
|
|
49
52
|
)) || {}
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
private
|
|
53
56
|
|
|
54
57
|
def broadcast_body(from_email:, name:, subject:, body_html:, body_text:, template_id:,
|
|
55
|
-
message_type:, audience_list_id:,
|
|
58
|
+
message_type:, audience_list_id:, list_ids:, exclude_list_ids:,
|
|
59
|
+
recipients:, scheduled_at:, send_now:)
|
|
56
60
|
compact_params(
|
|
57
61
|
"name" => name,
|
|
58
62
|
"from_email" => from_email,
|
|
@@ -62,6 +66,8 @@ module Sendara
|
|
|
62
66
|
"template_id" => template_id,
|
|
63
67
|
"message_type" => message_type,
|
|
64
68
|
"audience_list_id" => audience_list_id,
|
|
69
|
+
"list_ids" => list_ids,
|
|
70
|
+
"exclude_list_ids" => exclude_list_ids,
|
|
65
71
|
"recipients" => recipients,
|
|
66
72
|
"scheduled_at" => scheduled_at,
|
|
67
73
|
"send_now" => send_now
|
|
@@ -5,27 +5,37 @@ module Sendara
|
|
|
5
5
|
class Emails < Resource
|
|
6
6
|
def send(to:, from: nil, subject: nil, html: nil, text: nil, message_type: nil,
|
|
7
7
|
template_id: nil, template_vars: nil, metadata: nil, store_payload: nil,
|
|
8
|
-
test_send: nil, idempotency_key: nil)
|
|
8
|
+
test_send: nil, scheduled_at: nil, validate_recipient: nil, idempotency_key: nil)
|
|
9
|
+
raise ArgumentError, "to is required" if to.nil? || to.to_s.empty?
|
|
10
|
+
|
|
9
11
|
meta = metadata.nil? ? {} : metadata.dup
|
|
10
12
|
meta["from_email"] = from unless from.nil?
|
|
11
13
|
|
|
12
14
|
body = {
|
|
13
15
|
"channel" => "email",
|
|
14
16
|
"idempotency_key" => idempotency_key || Client.generate_idempotency_key,
|
|
15
|
-
"destination" => { "email" => to }
|
|
16
|
-
"payload" => {
|
|
17
|
-
"subject" => subject,
|
|
18
|
-
"body_html" => html,
|
|
19
|
-
"body_text" => text
|
|
20
|
-
},
|
|
21
|
-
"metadata" => meta
|
|
17
|
+
"destination" => { "email" => to }
|
|
22
18
|
}
|
|
19
|
+
body["metadata"] = meta unless meta.empty?
|
|
20
|
+
|
|
21
|
+
# Only the fields the caller supplied go on the wire, and "payload" is
|
|
22
|
+
# dropped outright when none were: a template-only send would otherwise
|
|
23
|
+
# ship nulls the server discards when it renders the template over them.
|
|
24
|
+
# Shared by all five SDKs.
|
|
25
|
+
payload = compact_params(
|
|
26
|
+
"subject" => subject,
|
|
27
|
+
"body_html" => html,
|
|
28
|
+
"body_text" => text
|
|
29
|
+
).reject { |_key, value| value.to_s.empty? }
|
|
30
|
+
body["payload"] = payload unless payload.empty?
|
|
23
31
|
|
|
24
32
|
body["message_type"] = message_type unless message_type.nil?
|
|
25
33
|
body["template_id"] = template_id unless template_id.nil?
|
|
26
34
|
body["template_vars"] = template_vars unless template_vars.nil?
|
|
27
35
|
body["store_payload"] = store_payload unless store_payload.nil?
|
|
28
36
|
body["test_send"] = test_send unless test_send.nil?
|
|
37
|
+
body["scheduled_at"] = scheduled_at unless scheduled_at.nil?
|
|
38
|
+
body["validate_recipient"] = validate_recipient unless validate_recipient.nil?
|
|
29
39
|
|
|
30
40
|
send_raw(body)
|
|
31
41
|
end
|
data/lib/sendara/version.rb
CHANGED
data/lib/sendara.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "sendara/version"
|
|
4
4
|
require_relative "sendara/errors"
|
|
5
5
|
require_relative "sendara/message_page"
|
|
6
|
+
require_relative "sendara/checkout_result"
|
|
6
7
|
require_relative "sendara/resource"
|
|
7
8
|
require_relative "sendara/resources/emails"
|
|
8
9
|
require_relative "sendara/resources/broadcasts"
|
data/sendara.gemspec
CHANGED
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.summary = "Sendara — email & marketing API client for Ruby."
|
|
12
12
|
spec.description = "Email-first Ruby client for the Sendara API: transactional email, " \
|
|
13
13
|
"broadcasts, contacts, templates, domains, and signed webhooks. " \
|
|
14
|
-
"Pure stdlib HTTP
|
|
14
|
+
"Pure stdlib HTTP with the Ruby base64 default gem."
|
|
15
15
|
spec.homepage = "https://sendara.dev"
|
|
16
16
|
spec.license = "MIT"
|
|
17
17
|
spec.required_ruby_version = ">= 3.0"
|
|
@@ -26,5 +26,6 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.files = Dir.glob("lib/**/*.rb") + %w[README.md LICENSE sendara.gemspec].select { |f| File.exist?(f) }
|
|
27
27
|
spec.require_paths = ["lib"]
|
|
28
28
|
|
|
29
|
+
spec.add_dependency "base64", ">= 0.1"
|
|
29
30
|
spec.add_development_dependency "rspec", "~> 3"
|
|
30
31
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sendara
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sendara
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.1'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: rspec
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -25,8 +39,8 @@ dependencies:
|
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
40
|
version: '3'
|
|
27
41
|
description: 'Email-first Ruby client for the Sendara API: transactional email, broadcasts,
|
|
28
|
-
contacts, templates, domains, and signed webhooks. Pure stdlib HTTP
|
|
29
|
-
|
|
42
|
+
contacts, templates, domains, and signed webhooks. Pure stdlib HTTP with the Ruby
|
|
43
|
+
base64 default gem.'
|
|
30
44
|
email:
|
|
31
45
|
- support@sendara.dev
|
|
32
46
|
executables: []
|
|
@@ -35,6 +49,7 @@ extra_rdoc_files: []
|
|
|
35
49
|
files:
|
|
36
50
|
- README.md
|
|
37
51
|
- lib/sendara.rb
|
|
52
|
+
- lib/sendara/checkout_result.rb
|
|
38
53
|
- lib/sendara/client.rb
|
|
39
54
|
- lib/sendara/errors.rb
|
|
40
55
|
- lib/sendara/generators/install_generator.rb
|