sendara 0.1.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 +48 -15
- 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/resources/messages.rb +16 -7
- 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,24 +159,25 @@ 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
|
|
|
159
175
|
## Messages & pagination
|
|
160
176
|
|
|
161
|
-
Fetch a single page with `messages.page
|
|
177
|
+
Fetch a single page with `messages.page` (aliased as `messages.list`), which returns a `Sendara::MessagePage`:
|
|
162
178
|
|
|
163
179
|
```ruby
|
|
164
|
-
page = client.messages.
|
|
180
|
+
page = client.messages.list(status: "delivered", limit: 50)
|
|
165
181
|
|
|
166
182
|
page.messages.each { |message| puts message["id"] }
|
|
167
183
|
page.has_more? # => true / false
|
|
@@ -182,10 +198,27 @@ Called without a block it returns an `Enumerator`, so the full `Enumerable` API
|
|
|
182
198
|
recent = client.messages.each(channel: "email", limit: 100).first(10)
|
|
183
199
|
```
|
|
184
200
|
|
|
185
|
-
Filters: `channel`, `status`, `from`, `to`, `limit`, `cursor`. Fetch one message by id:
|
|
201
|
+
Filters: `channel`, `status`, `search`, `from`, `to`, `limit`, `cursor`. Fetch one message with its full event timeline, by id or by the idempotency key from the original send:
|
|
186
202
|
|
|
187
203
|
```ruby
|
|
188
204
|
client.messages.get("msg_123")
|
|
205
|
+
client.messages.get(idempotency_key: "order-42")
|
|
206
|
+
```
|
|
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
|
|
189
222
|
```
|
|
190
223
|
|
|
191
224
|
## Webhook verification
|
|
@@ -201,7 +234,7 @@ event = Sendara::Webhooks.verify(
|
|
|
201
234
|
ENV.fetch("SENDARA_WEBHOOK_SECRET")
|
|
202
235
|
)
|
|
203
236
|
|
|
204
|
-
event["
|
|
237
|
+
event["event_type"] # => "delivered"
|
|
205
238
|
```
|
|
206
239
|
|
|
207
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:
|
|
@@ -296,9 +329,9 @@ class SendaraWebhooksController < ActionController::API
|
|
|
296
329
|
Rails.application.credentials.sendara_webhook_secret
|
|
297
330
|
)
|
|
298
331
|
|
|
299
|
-
case event["
|
|
300
|
-
when "
|
|
301
|
-
when "
|
|
332
|
+
case event["event_type"]
|
|
333
|
+
when "delivered" then handle_delivered(event)
|
|
334
|
+
when "bounced" then handle_bounced(event)
|
|
302
335
|
end
|
|
303
336
|
|
|
304
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
|
|
@@ -5,10 +5,11 @@ module Sendara
|
|
|
5
5
|
class Messages < Resource
|
|
6
6
|
include Enumerable
|
|
7
7
|
|
|
8
|
-
def page(channel: nil, status: nil, from: nil, to: nil, limit: nil, cursor: nil)
|
|
8
|
+
def page(channel: nil, status: nil, search: nil, from: nil, to: nil, limit: nil, cursor: nil)
|
|
9
9
|
query = compact_params(
|
|
10
10
|
"channel" => channel,
|
|
11
11
|
"status" => status,
|
|
12
|
+
"search" => search,
|
|
12
13
|
"from" => from,
|
|
13
14
|
"to" => to,
|
|
14
15
|
"limit" => limit,
|
|
@@ -18,15 +19,17 @@ module Sendara
|
|
|
18
19
|
MessagePage.from_response(response)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
alias list page
|
|
23
|
+
|
|
24
|
+
def each(channel: nil, status: nil, search: nil, from: nil, to: nil, limit: nil, cursor: nil, &block)
|
|
22
25
|
unless block_given?
|
|
23
|
-
return enum_for(:each, channel: channel, status: status,
|
|
24
|
-
to: to, limit: limit, cursor: cursor)
|
|
26
|
+
return enum_for(:each, channel: channel, status: status, search: search,
|
|
27
|
+
from: from, to: to, limit: limit, cursor: cursor)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
loop do
|
|
28
|
-
current = page(channel: channel, status: status,
|
|
29
|
-
to: to, limit: limit, cursor: cursor)
|
|
31
|
+
current = page(channel: channel, status: status, search: search,
|
|
32
|
+
from: from, to: to, limit: limit, cursor: cursor)
|
|
30
33
|
current.messages.each(&block)
|
|
31
34
|
break if current.next_cursor.nil?
|
|
32
35
|
|
|
@@ -34,7 +37,13 @@ module Sendara
|
|
|
34
37
|
end
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
def get(id)
|
|
40
|
+
def get(id = nil, idempotency_key: nil)
|
|
41
|
+
if idempotency_key
|
|
42
|
+
return request(:get, "/v1/messages", query: { "idempotency_key" => idempotency_key }) || {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, "get requires an id or idempotency_key" if id.nil?
|
|
46
|
+
|
|
38
47
|
request(:get, "/v1/messages/#{encode(id)}") || {}
|
|
39
48
|
end
|
|
40
49
|
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
|