sendly 3.30.0 → 3.33.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 +74 -0
- data/Gemfile.lock +1 -1
- data/README.md +23 -27
- data/lib/sendly/business_upgrade_resource.rb +404 -0
- data/lib/sendly/client.rb +30 -8
- data/lib/sendly/enterprise.rb +65 -13
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d643b13d3d51b1ce011b394a31a7fb722c26110e12a87d717e407709bdfd687b
|
|
4
|
+
data.tar.gz: e17abd7c0fa512116b06fb02927e9f8b3c41d2e082f214b48657b15fb0e327e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a5808b5f638300369e98c7746a049cb5633e727f70648e16f40486564bdb5920731feb3a7a97ebec1580f92bff8ceddca4c8795a224e0ce1c1873f51d9dd3cc
|
|
7
|
+
data.tar.gz: 1523973847432c7aa50947f6bf46763987d8be1130df68c318fdae466680d18477dece25cfb56200d4f4120cd772f157046b3410cb3284355634bd851efeda3e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# sendly (Ruby)
|
|
2
2
|
|
|
3
|
+
## 3.32.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- New `business_upgrade` resource for the toll-free entity-upgrade ("fork-with-new-number") flow. When a customer forms a new legal entity (e.g. an LLC), this resource lets them reserve a new toll-free number under the new entity, submit it for carrier review, and atomically swap to it on approval — without disrupting outbound SMS during the 1-2 week review window. Mirrors the Node SDK's `businessUpgrade` resource at parity.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
client = Sendly::Client.new("sk_live_v1_xxx")
|
|
11
|
+
|
|
12
|
+
# Validate before submitting (no writes)
|
|
13
|
+
preview = client.business_upgrade.preflight(
|
|
14
|
+
business_name: "Acme Holdings LLC",
|
|
15
|
+
brn: "12-3456789",
|
|
16
|
+
brn_type: "EIN",
|
|
17
|
+
brn_country: "US",
|
|
18
|
+
entity_type: "PRIVATE_PROFIT"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Best-of prefill across all the caller's verified workspaces
|
|
22
|
+
prefill = client.business_upgrade.best_prefill
|
|
23
|
+
|
|
24
|
+
# Submit the upgrade with the IRS letter (multipart upload)
|
|
25
|
+
result = client.business_upgrade.start(
|
|
26
|
+
"ws_abc",
|
|
27
|
+
business_name: "Acme Holdings LLC",
|
|
28
|
+
brn: "12-3456789",
|
|
29
|
+
brn_type: "EIN",
|
|
30
|
+
brn_country: "US",
|
|
31
|
+
entity_type: "PRIVATE_PROFIT",
|
|
32
|
+
ein_doc_path: "./CP-575.pdf"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Status, cancel, resubmit, set old-number disposition
|
|
36
|
+
client.business_upgrade.status("ws_abc")
|
|
37
|
+
client.business_upgrade.cancel("ws_abc")
|
|
38
|
+
client.business_upgrade.resubmit("ws_abc", contact_email: "new@acme.com")
|
|
39
|
+
client.business_upgrade.set_disposition("ws_abc", disposition: "released")
|
|
40
|
+
client.business_upgrade.set_disposition("ws_abc", disposition: "moved", target_workspace_id: "ws_xyz")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Methods: `preflight`, `best_prefill`, `start`, `status`, `cancel`, `resubmit`, `set_disposition`. EIN PDFs can be passed via `ein_doc_path:` (file path) or `ein_doc:` (raw bytes / IO).
|
|
44
|
+
|
|
45
|
+
## 3.31.0
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- **`Sendly::Client.new` now accepts the API key positionally** in addition to as a keyword argument. Every code sample in our docs used positional, so `Sendly::Client.new("sk_live_...")` previously raised `ArgumentError: missing keyword: :api_key`. Both styles now work and produce identical clients:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# Positional (matches our docs)
|
|
53
|
+
client = Sendly::Client.new("sk_live_v1_xxx")
|
|
54
|
+
client = Sendly::Client.new("sk_live_v1_xxx", timeout: 60)
|
|
55
|
+
|
|
56
|
+
# Keyword (existing v3.30.0 signature — unchanged)
|
|
57
|
+
client = Sendly::Client.new(api_key: "sk_live_v1_xxx")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Passing `api_key` both positionally and as a keyword raises `ArgumentError`; passing more than one positional argument also raises. Backward-compatible with all v3.30.0 callers.
|
|
61
|
+
|
|
62
|
+
## 3.30.0
|
|
63
|
+
|
|
64
|
+
### Minor Changes
|
|
65
|
+
|
|
66
|
+
- `enterprise.workspaces.submit_verification(workspace_id, **fields)`: rewritten to match the actual API shape (camelCase keys on the wire, nested `address`/`contact` hashes, `entity_type` + `brn`/`brn_type`/`brn_country` instead of `business_type`/`ein`). The previous shape didn't match the server endpoint — calls were always returning 400.
|
|
67
|
+
- **Partial-update friendly:** for resubmits on existing workspaces, send only the fields you want to change — everything else is filled from the existing record. Hosted page URLs (`/biz/`, `/opt-in/`, `/legal/`) generated during provision are auto-preserved.
|
|
68
|
+
- `enterprise.workspaces.resubmit_verification(workspace_id, **partial_updates)`: convenience alias for resubmits — same as `submit_verification` but reads more naturally for one-field-change use cases.
|
|
69
|
+
- All top-level keys are accepted as snake_case Ruby keyword arguments (`business_name`, `use_case`, `opt_in_workflow`, etc.) and transformed to the camelCase keys the API expects. Nested `address` and `contact` hashes are passed through verbatim and should already use camelCase keys (e.g. `firstName`, `lastName`).
|
|
70
|
+
|
|
71
|
+
### Server-side fixes paired with this release
|
|
72
|
+
|
|
73
|
+
- `/api/v1/enterprise/workspaces/:id/verification/submit` now returns specific missing-field errors (e.g. `"Missing required fields: website"`) instead of listing every required field whether present or not.
|
|
74
|
+
- Endpoint accepts both flat and `{ verification: {...} }` wrapped shapes (matches `/enterprise/provision`).
|
|
75
|
+
- `use_case` validation expanded from 23 entries to the full 43-value Telnyx enum.
|
|
76
|
+
|
|
3
77
|
## 3.29.0
|
|
4
78
|
|
|
5
79
|
### Minor Changes
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -164,16 +164,16 @@ scheduled = client.messages.schedule(
|
|
|
164
164
|
puts scheduled.id
|
|
165
165
|
puts scheduled.scheduled_at
|
|
166
166
|
|
|
167
|
-
# List scheduled messages
|
|
167
|
+
# List scheduled messages (returns a Hash with "data" array)
|
|
168
168
|
result = client.messages.list_scheduled
|
|
169
|
-
result
|
|
169
|
+
result["data"].each { |msg| puts "#{msg['id']}: #{msg['scheduledAt']}" }
|
|
170
170
|
|
|
171
171
|
# Get a specific scheduled message
|
|
172
172
|
msg = client.messages.get_scheduled("sched_xxx")
|
|
173
173
|
|
|
174
174
|
# Cancel a scheduled message (refunds credits)
|
|
175
175
|
result = client.messages.cancel_scheduled("sched_xxx")
|
|
176
|
-
puts "Refunded: #{result
|
|
176
|
+
puts "Refunded: #{result['creditsRefunded']} credits"
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
### Batch Messages
|
|
@@ -188,10 +188,10 @@ batch = client.messages.send_batch(
|
|
|
188
188
|
]
|
|
189
189
|
)
|
|
190
190
|
|
|
191
|
-
puts batch
|
|
192
|
-
puts "Queued: #{batch
|
|
193
|
-
puts "Failed: #{batch
|
|
194
|
-
puts "Credits used: #{batch
|
|
191
|
+
puts batch["batchId"]
|
|
192
|
+
puts "Queued: #{batch['queued']}"
|
|
193
|
+
puts "Failed: #{batch['failed']}"
|
|
194
|
+
puts "Credits used: #{batch['creditsUsed']}"
|
|
195
195
|
|
|
196
196
|
# Get batch status
|
|
197
197
|
status = client.messages.get_batch("batch_xxx")
|
|
@@ -206,8 +206,8 @@ preview = client.messages.preview_batch(
|
|
|
206
206
|
{ to: '+447700900123', text: 'Hello UK!' }
|
|
207
207
|
]
|
|
208
208
|
)
|
|
209
|
-
puts "
|
|
210
|
-
puts "
|
|
209
|
+
puts "Credits needed: #{preview['creditsNeeded']}"
|
|
210
|
+
puts "Will send: #{preview['willSend']}, Blocked: #{preview['blocked']}"
|
|
211
211
|
```
|
|
212
212
|
|
|
213
213
|
### Iterate All Messages
|
|
@@ -266,30 +266,26 @@ account = client.account.get
|
|
|
266
266
|
puts account.email
|
|
267
267
|
|
|
268
268
|
# Check credit balance
|
|
269
|
-
credits = client.account.
|
|
270
|
-
puts "Available: #{credits
|
|
271
|
-
puts "Reserved: #{credits
|
|
272
|
-
puts "Total: #{credits
|
|
269
|
+
credits = client.account.credits
|
|
270
|
+
puts "Available: #{credits['availableBalance']} credits"
|
|
271
|
+
puts "Reserved: #{credits['reservedBalance']} credits"
|
|
272
|
+
puts "Total: #{credits['balance']} credits"
|
|
273
273
|
|
|
274
274
|
# View credit transaction history
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
puts "#{tx
|
|
275
|
+
transactions = client.account.transactions
|
|
276
|
+
transactions.each do |tx|
|
|
277
|
+
puts "#{tx['type']}: #{tx['amount']} credits - #{tx['description']}"
|
|
278
278
|
end
|
|
279
279
|
|
|
280
280
|
# List API keys
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
puts "#{key
|
|
281
|
+
keys = client.account.api_keys
|
|
282
|
+
keys.each do |key|
|
|
283
|
+
puts "#{key['name']}: #{key['prefix']}*** (#{key['type']})"
|
|
284
284
|
end
|
|
285
285
|
|
|
286
286
|
# Create a new API key
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
type: 'live',
|
|
290
|
-
scopes: ['sms:send', 'sms:read']
|
|
291
|
-
)
|
|
292
|
-
puts "New key: #{new_key.key}" # Only shown once!
|
|
287
|
+
result = client.account.create_api_key('Production Key')
|
|
288
|
+
puts "New key: #{result['key']}" # Only shown once!
|
|
293
289
|
|
|
294
290
|
# Revoke an API key
|
|
295
291
|
client.account.revoke_api_key('key_xxx')
|
|
@@ -408,7 +404,7 @@ Three provisioning modes:
|
|
|
408
404
|
### Workspace Management
|
|
409
405
|
|
|
410
406
|
```ruby
|
|
411
|
-
ws = client.enterprise.workspaces.create("Acme Insurance")
|
|
407
|
+
ws = client.enterprise.workspaces.create(name: "Acme Insurance")
|
|
412
408
|
list = client.enterprise.workspaces.list
|
|
413
409
|
detail = client.enterprise.workspaces.get("ws_xxx")
|
|
414
410
|
client.enterprise.workspaces.delete("ws_xxx")
|
|
@@ -430,7 +426,7 @@ client.enterprise.workspaces.revoke_key("ws_xxx", "key_abc")
|
|
|
430
426
|
### Webhooks & Analytics
|
|
431
427
|
|
|
432
428
|
```ruby
|
|
433
|
-
client.enterprise.webhooks.set("https://yourapp.com/webhooks")
|
|
429
|
+
client.enterprise.webhooks.set(url: "https://yourapp.com/webhooks")
|
|
434
430
|
overview = client.enterprise.analytics.overview
|
|
435
431
|
messages = client.enterprise.analytics.messages(period: "30d")
|
|
436
432
|
delivery = client.enterprise.analytics.delivery
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module Sendly
|
|
9
|
+
# Business Upgrade resource — Entity-Upgrade ("fork-with-new-number")
|
|
10
|
+
#
|
|
11
|
+
# Manages the toll-free business entity upgrade flow: when a customer
|
|
12
|
+
# forms a new legal entity (e.g. an LLC), this resource lets them
|
|
13
|
+
# reserve a new toll-free number under the new entity, submit it for
|
|
14
|
+
# carrier review, and atomically swap to it on approval — without
|
|
15
|
+
# disrupting outbound SMS during the 1-2 week review window.
|
|
16
|
+
#
|
|
17
|
+
# @see https://sendly.live/docs/business-upgrade
|
|
18
|
+
#
|
|
19
|
+
# @example Validate before submitting
|
|
20
|
+
# preview = client.business_upgrade.preflight(
|
|
21
|
+
# business_name: "Acme Holdings LLC",
|
|
22
|
+
# brn: "12-3456789",
|
|
23
|
+
# brn_type: "EIN",
|
|
24
|
+
# brn_country: "US",
|
|
25
|
+
# entity_type: "PRIVATE_PROFIT"
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# @example Submit an upgrade with the IRS letter
|
|
29
|
+
# result = client.business_upgrade.start(
|
|
30
|
+
# "ws_abc",
|
|
31
|
+
# business_name: "Acme Holdings LLC",
|
|
32
|
+
# brn: "12-3456789",
|
|
33
|
+
# brn_type: "EIN",
|
|
34
|
+
# brn_country: "US",
|
|
35
|
+
# entity_type: "PRIVATE_PROFIT",
|
|
36
|
+
# ein_doc_path: "./CP-575.pdf"
|
|
37
|
+
# )
|
|
38
|
+
class BusinessUpgradeResource
|
|
39
|
+
# Entity types accepted by the carrier
|
|
40
|
+
ENTITY_TYPES = %w[
|
|
41
|
+
SOLE_PROPRIETOR
|
|
42
|
+
PRIVATE_PROFIT
|
|
43
|
+
PUBLIC_PROFIT
|
|
44
|
+
NON_PROFIT
|
|
45
|
+
GOVERNMENT
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
# Business Registration Number types
|
|
49
|
+
BRN_TYPES = %w[EIN SSN DUNS CRA VAT LEI OTHER].freeze
|
|
50
|
+
|
|
51
|
+
# Disposition options for the old toll-free number after approval
|
|
52
|
+
DISPOSITIONS = %w[moved released].freeze
|
|
53
|
+
|
|
54
|
+
def initialize(client)
|
|
55
|
+
@client = client
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validate a candidate entity upgrade payload before submission.
|
|
59
|
+
# Returns issues + proposed auto-fixes. No writes — purely advisory.
|
|
60
|
+
#
|
|
61
|
+
# Accepts the same fields as +start+. Returns a hash with
|
|
62
|
+
# +verdict+ (+"ready"+, +"warnings"+, +"blocked"+), +issues+, and
|
|
63
|
+
# +proposedFixes+.
|
|
64
|
+
#
|
|
65
|
+
# @param business_name [String, nil]
|
|
66
|
+
# @param brn [String, nil]
|
|
67
|
+
# @param brn_type [String, nil] One of {BRN_TYPES}
|
|
68
|
+
# @param brn_country [String, nil] ISO country code
|
|
69
|
+
# @param entity_type [String, nil] One of {ENTITY_TYPES}
|
|
70
|
+
# @param doing_business_as [String, nil]
|
|
71
|
+
# @param website [String, nil]
|
|
72
|
+
# @param address1 [String, nil]
|
|
73
|
+
# @param address2 [String, nil]
|
|
74
|
+
# @param city [String, nil]
|
|
75
|
+
# @param state [String, nil]
|
|
76
|
+
# @param zip [String, nil]
|
|
77
|
+
# @param address_country [String, nil]
|
|
78
|
+
# @param contact_first_name [String, nil]
|
|
79
|
+
# @param contact_last_name [String, nil]
|
|
80
|
+
# @param contact_email [String, nil]
|
|
81
|
+
# @param contact_phone [String, nil]
|
|
82
|
+
# @param monthly_volume [String, nil]
|
|
83
|
+
# @param use_case [String, nil]
|
|
84
|
+
# @param use_case_summary [String, nil]
|
|
85
|
+
# @param sample_messages [String, nil]
|
|
86
|
+
# @param opt_in_workflow [String, nil]
|
|
87
|
+
# @param privacy_url [String, nil]
|
|
88
|
+
# @param terms_url [String, nil]
|
|
89
|
+
# @param additional_information [String, nil]
|
|
90
|
+
# @param age_gated_content [Boolean, nil]
|
|
91
|
+
# @return [Hash] Preflight report
|
|
92
|
+
def preflight(business_name: nil, brn: nil, brn_type: nil, brn_country: nil,
|
|
93
|
+
entity_type: nil, doing_business_as: nil, website: nil,
|
|
94
|
+
address1: nil, address2: nil, city: nil, state: nil, zip: nil,
|
|
95
|
+
address_country: nil, contact_first_name: nil,
|
|
96
|
+
contact_last_name: nil, contact_email: nil, contact_phone: nil,
|
|
97
|
+
monthly_volume: nil, use_case: nil, use_case_summary: nil,
|
|
98
|
+
sample_messages: nil, opt_in_workflow: nil, privacy_url: nil,
|
|
99
|
+
terms_url: nil, additional_information: nil,
|
|
100
|
+
age_gated_content: nil)
|
|
101
|
+
body = build_upgrade_body(
|
|
102
|
+
business_name: business_name, brn: brn, brn_type: brn_type,
|
|
103
|
+
brn_country: brn_country, entity_type: entity_type,
|
|
104
|
+
doing_business_as: doing_business_as, website: website,
|
|
105
|
+
address1: address1, address2: address2, city: city, state: state,
|
|
106
|
+
zip: zip, address_country: address_country,
|
|
107
|
+
contact_first_name: contact_first_name,
|
|
108
|
+
contact_last_name: contact_last_name, contact_email: contact_email,
|
|
109
|
+
contact_phone: contact_phone, monthly_volume: monthly_volume,
|
|
110
|
+
use_case: use_case, use_case_summary: use_case_summary,
|
|
111
|
+
sample_messages: sample_messages, opt_in_workflow: opt_in_workflow,
|
|
112
|
+
privacy_url: privacy_url, terms_url: terms_url,
|
|
113
|
+
additional_information: additional_information,
|
|
114
|
+
age_gated_content: age_gated_content
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@client.post("/verification/preflight", body)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get a "best-of" prefill across all the caller's verified workspaces.
|
|
121
|
+
# Returns most-recent non-empty values per messaging field. Use this
|
|
122
|
+
# to pre-populate the upgrade form for users whose current workspace
|
|
123
|
+
# has incomplete data.
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash] +{ "prefill" => {...}, "sourceWorkspaceCount" => Integer }+
|
|
126
|
+
def best_prefill
|
|
127
|
+
@client.get("/verification/best-prefill")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Start an entity upgrade for the given workspace. Auto-provisions
|
|
131
|
+
# a new toll-free number + messaging profile and submits to the
|
|
132
|
+
# carrier for review. Returns the pending verification details.
|
|
133
|
+
#
|
|
134
|
+
# The current toll-free number continues sending throughout the
|
|
135
|
+
# 1-2 week carrier review; on approval, an atomic swap promotes
|
|
136
|
+
# the new number.
|
|
137
|
+
#
|
|
138
|
+
# The EIN document (when supplied) is uploaded as multipart form-data
|
|
139
|
+
# under the +einDoc+ field. Provide either +ein_doc_path+ (a path on
|
|
140
|
+
# disk) or +ein_doc+ (raw bytes / IO) — not both.
|
|
141
|
+
#
|
|
142
|
+
# @param workspace_id [String]
|
|
143
|
+
# @param business_name [String]
|
|
144
|
+
# @param brn [String]
|
|
145
|
+
# @param brn_type [String] One of {BRN_TYPES}
|
|
146
|
+
# @param brn_country [String] ISO country code
|
|
147
|
+
# @param entity_type [String] One of {ENTITY_TYPES}
|
|
148
|
+
# @param ein_doc_path [String, nil] Path to the EIN/CP-575 PDF
|
|
149
|
+
# @param ein_doc [String, IO, nil] Raw EIN PDF bytes or IO
|
|
150
|
+
# @param ein_doc_filename [String, nil] Filename (defaults to "ein-doc.pdf")
|
|
151
|
+
# @param ein_doc_content_type [String, nil] MIME type (defaults to "application/pdf")
|
|
152
|
+
# @return [Hash] Upgrade start response
|
|
153
|
+
def start(workspace_id, business_name:, brn:, brn_type:, brn_country:,
|
|
154
|
+
entity_type:, doing_business_as: nil, website: nil,
|
|
155
|
+
address1: nil, address2: nil, city: nil, state: nil, zip: nil,
|
|
156
|
+
address_country: nil, contact_first_name: nil,
|
|
157
|
+
contact_last_name: nil, contact_email: nil, contact_phone: nil,
|
|
158
|
+
monthly_volume: nil, use_case: nil, use_case_summary: nil,
|
|
159
|
+
sample_messages: nil, opt_in_workflow: nil, privacy_url: nil,
|
|
160
|
+
terms_url: nil, additional_information: nil,
|
|
161
|
+
age_gated_content: nil, ein_doc_path: nil, ein_doc: nil,
|
|
162
|
+
ein_doc_filename: nil, ein_doc_content_type: nil)
|
|
163
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
164
|
+
|
|
165
|
+
fields = build_upgrade_body(
|
|
166
|
+
business_name: business_name, brn: brn, brn_type: brn_type,
|
|
167
|
+
brn_country: brn_country, entity_type: entity_type,
|
|
168
|
+
doing_business_as: doing_business_as, website: website,
|
|
169
|
+
address1: address1, address2: address2, city: city, state: state,
|
|
170
|
+
zip: zip, address_country: address_country,
|
|
171
|
+
contact_first_name: contact_first_name,
|
|
172
|
+
contact_last_name: contact_last_name, contact_email: contact_email,
|
|
173
|
+
contact_phone: contact_phone, monthly_volume: monthly_volume,
|
|
174
|
+
use_case: use_case, use_case_summary: use_case_summary,
|
|
175
|
+
sample_messages: sample_messages, opt_in_workflow: opt_in_workflow,
|
|
176
|
+
privacy_url: privacy_url, terms_url: terms_url,
|
|
177
|
+
additional_information: additional_information,
|
|
178
|
+
age_gated_content: age_gated_content
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
post_multipart_with_fields(
|
|
182
|
+
"/workspaces/#{url_escape(workspace_id)}/upgrade",
|
|
183
|
+
fields,
|
|
184
|
+
ein_doc_path: ein_doc_path,
|
|
185
|
+
ein_doc: ein_doc,
|
|
186
|
+
ein_doc_filename: ein_doc_filename,
|
|
187
|
+
ein_doc_content_type: ein_doc_content_type
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check whether the given workspace has a pending entity upgrade.
|
|
192
|
+
# Returns +{ "pending" => nil }+ when no upgrade is in flight.
|
|
193
|
+
#
|
|
194
|
+
# @param workspace_id [String]
|
|
195
|
+
# @return [Hash]
|
|
196
|
+
def status(workspace_id)
|
|
197
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
198
|
+
|
|
199
|
+
@client.get("/workspaces/#{url_escape(workspace_id)}/upgrade/status")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Cancel a pending entity upgrade for the given workspace. Releases
|
|
203
|
+
# the reserved toll-free number, deletes the new messaging profile,
|
|
204
|
+
# and removes the stored EIN document. Idempotent.
|
|
205
|
+
#
|
|
206
|
+
# @param workspace_id [String]
|
|
207
|
+
# @return [Hash]
|
|
208
|
+
def cancel(workspace_id)
|
|
209
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
210
|
+
|
|
211
|
+
@client.post("/workspaces/#{url_escape(workspace_id)}/upgrade/cancel", {})
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Resubmit a rejected (or waiting-for-customer) entity upgrade with
|
|
215
|
+
# updated fields and optionally a new EIN document. All fields are
|
|
216
|
+
# optional — only the ones you pass are sent.
|
|
217
|
+
#
|
|
218
|
+
# @param workspace_id [String]
|
|
219
|
+
# @return [Hash]
|
|
220
|
+
def resubmit(workspace_id, business_name: nil, brn: nil, brn_type: nil,
|
|
221
|
+
brn_country: nil, entity_type: nil, doing_business_as: nil,
|
|
222
|
+
website: nil, address1: nil, address2: nil, city: nil,
|
|
223
|
+
state: nil, zip: nil, address_country: nil,
|
|
224
|
+
contact_first_name: nil, contact_last_name: nil,
|
|
225
|
+
contact_email: nil, contact_phone: nil, monthly_volume: nil,
|
|
226
|
+
use_case: nil, use_case_summary: nil, sample_messages: nil,
|
|
227
|
+
opt_in_workflow: nil, privacy_url: nil, terms_url: nil,
|
|
228
|
+
additional_information: nil, age_gated_content: nil,
|
|
229
|
+
ein_doc_path: nil, ein_doc: nil, ein_doc_filename: nil,
|
|
230
|
+
ein_doc_content_type: nil)
|
|
231
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
232
|
+
|
|
233
|
+
fields = build_upgrade_body(
|
|
234
|
+
business_name: business_name, brn: brn, brn_type: brn_type,
|
|
235
|
+
brn_country: brn_country, entity_type: entity_type,
|
|
236
|
+
doing_business_as: doing_business_as, website: website,
|
|
237
|
+
address1: address1, address2: address2, city: city, state: state,
|
|
238
|
+
zip: zip, address_country: address_country,
|
|
239
|
+
contact_first_name: contact_first_name,
|
|
240
|
+
contact_last_name: contact_last_name, contact_email: contact_email,
|
|
241
|
+
contact_phone: contact_phone, monthly_volume: monthly_volume,
|
|
242
|
+
use_case: use_case, use_case_summary: use_case_summary,
|
|
243
|
+
sample_messages: sample_messages, opt_in_workflow: opt_in_workflow,
|
|
244
|
+
privacy_url: privacy_url, terms_url: terms_url,
|
|
245
|
+
additional_information: additional_information,
|
|
246
|
+
age_gated_content: age_gated_content
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
post_multipart_with_fields(
|
|
250
|
+
"/workspaces/#{url_escape(workspace_id)}/upgrade/resubmit",
|
|
251
|
+
fields,
|
|
252
|
+
ein_doc_path: ein_doc_path,
|
|
253
|
+
ein_doc: ein_doc,
|
|
254
|
+
ein_doc_filename: ein_doc_filename,
|
|
255
|
+
ein_doc_content_type: ein_doc_content_type
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# After a successful entity-upgrade approval, choose what happens to
|
|
260
|
+
# the old toll-free number:
|
|
261
|
+
#
|
|
262
|
+
# - +"moved"+: keep it active under another workspace owned by the
|
|
263
|
+
# same user (requires +target_workspace_id+)
|
|
264
|
+
# - +"released"+: return it to the carrier pool
|
|
265
|
+
#
|
|
266
|
+
# @param workspace_id [String]
|
|
267
|
+
# @param disposition [String] +"moved"+ or +"released"+
|
|
268
|
+
# @param target_workspace_id [String, nil] Required when +disposition+ is +"moved"+
|
|
269
|
+
# @return [Hash]
|
|
270
|
+
def set_disposition(workspace_id, disposition:, target_workspace_id: nil)
|
|
271
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
272
|
+
unless DISPOSITIONS.include?(disposition)
|
|
273
|
+
raise ArgumentError, "disposition must be one of: #{DISPOSITIONS.join(", ")}"
|
|
274
|
+
end
|
|
275
|
+
if disposition == "moved" && (target_workspace_id.nil? || target_workspace_id.empty?)
|
|
276
|
+
raise ArgumentError, "target_workspace_id is required when disposition is 'moved'"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
body = { disposition: disposition }
|
|
280
|
+
body[:targetOrgId] = target_workspace_id if target_workspace_id
|
|
281
|
+
|
|
282
|
+
@client.post("/workspaces/#{url_escape(workspace_id)}/upgrade/disposition", body)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def url_escape(value)
|
|
288
|
+
URI.encode_www_form_component(value)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def build_upgrade_body(business_name:, brn:, brn_type:, brn_country:,
|
|
292
|
+
entity_type:, doing_business_as:, website:,
|
|
293
|
+
address1:, address2:, city:, state:, zip:,
|
|
294
|
+
address_country:, contact_first_name:,
|
|
295
|
+
contact_last_name:, contact_email:, contact_phone:,
|
|
296
|
+
monthly_volume:, use_case:, use_case_summary:,
|
|
297
|
+
sample_messages:, opt_in_workflow:, privacy_url:,
|
|
298
|
+
terms_url:, additional_information:,
|
|
299
|
+
age_gated_content:)
|
|
300
|
+
body = {}
|
|
301
|
+
body[:businessName] = business_name unless business_name.nil?
|
|
302
|
+
body[:brn] = brn unless brn.nil?
|
|
303
|
+
body[:brnType] = brn_type unless brn_type.nil?
|
|
304
|
+
body[:brnCountry] = brn_country unless brn_country.nil?
|
|
305
|
+
body[:entityType] = entity_type unless entity_type.nil?
|
|
306
|
+
body[:doingBusinessAs] = doing_business_as unless doing_business_as.nil?
|
|
307
|
+
body[:website] = website unless website.nil?
|
|
308
|
+
body[:address1] = address1 unless address1.nil?
|
|
309
|
+
body[:address2] = address2 unless address2.nil?
|
|
310
|
+
body[:city] = city unless city.nil?
|
|
311
|
+
body[:state] = state unless state.nil?
|
|
312
|
+
body[:zip] = zip unless zip.nil?
|
|
313
|
+
body[:addressCountry] = address_country unless address_country.nil?
|
|
314
|
+
body[:contactFirstName] = contact_first_name unless contact_first_name.nil?
|
|
315
|
+
body[:contactLastName] = contact_last_name unless contact_last_name.nil?
|
|
316
|
+
body[:contactEmail] = contact_email unless contact_email.nil?
|
|
317
|
+
body[:contactPhone] = contact_phone unless contact_phone.nil?
|
|
318
|
+
body[:monthlyVolume] = monthly_volume unless monthly_volume.nil?
|
|
319
|
+
body[:useCase] = use_case unless use_case.nil?
|
|
320
|
+
body[:useCaseSummary] = use_case_summary unless use_case_summary.nil?
|
|
321
|
+
body[:sampleMessages] = sample_messages unless sample_messages.nil?
|
|
322
|
+
body[:optInWorkflow] = opt_in_workflow unless opt_in_workflow.nil?
|
|
323
|
+
body[:privacyUrl] = privacy_url unless privacy_url.nil?
|
|
324
|
+
body[:termsUrl] = terms_url unless terms_url.nil?
|
|
325
|
+
body[:additionalInformation] = additional_information unless additional_information.nil?
|
|
326
|
+
body[:ageGatedContent] = age_gated_content unless age_gated_content.nil?
|
|
327
|
+
body
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# POST a multipart/form-data request with the given fields plus an
|
|
331
|
+
# optional +einDoc+ file part. Mirrors the JSON-vs-multipart flexibility
|
|
332
|
+
# the server expects: if no EIN doc is supplied and there are no fields,
|
|
333
|
+
# we still send an empty multipart body (the server is configured for
|
|
334
|
+
# +multer+ on these routes).
|
|
335
|
+
def post_multipart_with_fields(path, fields, ein_doc_path:, ein_doc:,
|
|
336
|
+
ein_doc_filename:, ein_doc_content_type:)
|
|
337
|
+
if !ein_doc_path.nil? && !ein_doc.nil?
|
|
338
|
+
raise ArgumentError, "Provide ein_doc_path OR ein_doc, not both"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
file_bytes = nil
|
|
342
|
+
filename = ein_doc_filename || "ein-doc.pdf"
|
|
343
|
+
content_type = ein_doc_content_type || "application/pdf"
|
|
344
|
+
|
|
345
|
+
if ein_doc_path
|
|
346
|
+
raise ArgumentError, "File not found: #{ein_doc_path}" unless File.exist?(ein_doc_path)
|
|
347
|
+
file_bytes = File.binread(ein_doc_path)
|
|
348
|
+
filename = ein_doc_filename || File.basename(ein_doc_path)
|
|
349
|
+
elsif ein_doc
|
|
350
|
+
file_bytes = ein_doc.is_a?(String) ? ein_doc : ein_doc.read
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
boundary = "SendlyRuby#{SecureRandom.hex(16)}"
|
|
354
|
+
body_parts = []
|
|
355
|
+
|
|
356
|
+
fields.each do |k, v|
|
|
357
|
+
next if v.nil?
|
|
358
|
+
body_parts << "--#{boundary}\r\n"
|
|
359
|
+
body_parts << "Content-Disposition: form-data; name=\"#{k}\"\r\n\r\n"
|
|
360
|
+
body_parts << (v == true || v == false ? v.to_s : v.to_s)
|
|
361
|
+
body_parts << "\r\n"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
if file_bytes
|
|
365
|
+
body_parts << "--#{boundary}\r\n"
|
|
366
|
+
body_parts << "Content-Disposition: form-data; name=\"einDoc\"; filename=\"#{filename}\"\r\n"
|
|
367
|
+
body_parts << "Content-Type: #{content_type}\r\n\r\n"
|
|
368
|
+
body_parts << file_bytes
|
|
369
|
+
body_parts << "\r\n"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
body_parts << "--#{boundary}--\r\n"
|
|
373
|
+
|
|
374
|
+
uri = URI.parse("#{@client.base_url}#{path}")
|
|
375
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
376
|
+
http.use_ssl = uri.scheme == "https"
|
|
377
|
+
http.open_timeout = 10
|
|
378
|
+
http.read_timeout = @client.timeout
|
|
379
|
+
|
|
380
|
+
req = Net::HTTP::Post.new(uri)
|
|
381
|
+
req["Authorization"] = "Bearer #{@client.api_key}"
|
|
382
|
+
req["Accept"] = "application/json"
|
|
383
|
+
req["User-Agent"] = "sendly-ruby/#{Sendly::VERSION}"
|
|
384
|
+
req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
|
385
|
+
req["X-Organization-Id"] = @client.organization_id if @client.organization_id
|
|
386
|
+
req.body = body_parts.join
|
|
387
|
+
|
|
388
|
+
begin
|
|
389
|
+
response = http.request(req)
|
|
390
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
391
|
+
raise Sendly::TimeoutError, "Request timed out after #{@client.timeout} seconds"
|
|
392
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError => e
|
|
393
|
+
raise Sendly::NetworkError, "Connection failed: #{e.message}"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
status = response.code.to_i
|
|
397
|
+
body = response.body.nil? || response.body.empty? ? {} : (JSON.parse(response.body) rescue { "message" => response.body })
|
|
398
|
+
|
|
399
|
+
return body if status >= 200 && status < 300
|
|
400
|
+
|
|
401
|
+
raise Sendly::ErrorFactory.from_response(status, body)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
data/lib/sendly/client.rb
CHANGED
|
@@ -23,18 +23,33 @@ module Sendly
|
|
|
23
23
|
# @return [String, nil] Organization ID
|
|
24
24
|
attr_accessor :organization_id
|
|
25
25
|
|
|
26
|
-
# Create a new Sendly client
|
|
26
|
+
# Create a new Sendly client.
|
|
27
27
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
28
|
+
# Two calling conventions are supported (both produce the same client):
|
|
29
|
+
#
|
|
30
|
+
# # Positional (matches Sendly's published code samples and the
|
|
31
|
+
# # idiom of most other Ruby HTTP SDKs):
|
|
32
|
+
# client = Sendly::Client.new("sk_live_v1_xxx")
|
|
33
|
+
# client = Sendly::Client.new("sk_live_v1_xxx", timeout: 60)
|
|
34
|
+
#
|
|
35
|
+
# # Keyword (existing v3.30.0 signature):
|
|
36
|
+
# client = Sendly::Client.new(api_key: "sk_live_v1_xxx")
|
|
37
|
+
#
|
|
38
|
+
# @param api_key [String, nil] Your Sendly API key (also accepted as positional)
|
|
39
|
+
# @param base_url [String, nil] API base URL (optional)
|
|
30
40
|
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
31
41
|
# @param max_retries [Integer] Maximum retry attempts (default: 3)
|
|
32
42
|
# @param organization_id [String, nil] Organization ID (optional)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
def initialize(*args, api_key: nil, base_url: nil, timeout: 30, max_retries: 3, organization_id: nil)
|
|
44
|
+
# Backward-compatible positional API key. Previously this constructor
|
|
45
|
+
# only accepted `api_key:` as a keyword; every code sample in our
|
|
46
|
+
# docs used positional, breaking copy-paste for new users.
|
|
47
|
+
if !args.empty?
|
|
48
|
+
raise ArgumentError, "Sendly::Client.new accepts at most one positional argument (api_key)" if args.length > 1
|
|
49
|
+
raise ArgumentError, "Cannot pass api_key both positionally and as keyword" unless api_key.nil?
|
|
50
|
+
api_key = args.first
|
|
51
|
+
end
|
|
52
|
+
|
|
38
53
|
@api_key = api_key
|
|
39
54
|
@base_url = (base_url || Sendly.base_url).chomp("/")
|
|
40
55
|
@timeout = timeout
|
|
@@ -135,6 +150,13 @@ module Sendly
|
|
|
135
150
|
@enterprise ||= EnterpriseResource.new(self)
|
|
136
151
|
end
|
|
137
152
|
|
|
153
|
+
# Access the Business Upgrade resource (entity-upgrade flow)
|
|
154
|
+
#
|
|
155
|
+
# @return [Sendly::BusinessUpgradeResource]
|
|
156
|
+
def business_upgrade
|
|
157
|
+
@business_upgrade ||= BusinessUpgradeResource.new(self)
|
|
158
|
+
end
|
|
159
|
+
|
|
138
160
|
# Make a GET request
|
|
139
161
|
#
|
|
140
162
|
# @param path [String] API path
|
data/lib/sendly/enterprise.rb
CHANGED
|
@@ -31,25 +31,77 @@ module Sendly
|
|
|
31
31
|
@client.delete("/enterprise/workspaces/#{workspace_id}")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
# Submit (or resubmit) a verification for an enterprise workspace.
|
|
35
|
+
#
|
|
36
|
+
# Partial-update friendly (May 2026): for resubmit on an existing
|
|
37
|
+
# workspace, you only need to send the fields you want to change —
|
|
38
|
+
# everything else is preserved from the existing record. Hosted page
|
|
39
|
+
# URLs (/biz/, /opt-in/, /legal/) generated during provision are
|
|
40
|
+
# auto-preserved.
|
|
41
|
+
#
|
|
42
|
+
# For sole proprietors, leave brn/brn_type/brn_country nil — the
|
|
43
|
+
# server strips them before forwarding to the carrier.
|
|
44
|
+
#
|
|
45
|
+
# Accepts snake_case keyword arguments which are transformed to the
|
|
46
|
+
# camelCase keys the API expects. Nested +address+ and +contact+
|
|
47
|
+
# hashes should already use camelCase keys (e.g. +firstName+,
|
|
48
|
+
# +lastName+) since they are passed through verbatim.
|
|
49
|
+
#
|
|
50
|
+
# Example (full submit):
|
|
51
|
+
#
|
|
52
|
+
# client.enterprise.workspaces.submit_verification(workspace_id,
|
|
53
|
+
# business_name: "Acme LLC",
|
|
54
|
+
# website: "https://acme.com",
|
|
55
|
+
# address: { street: "...", city: "...", state: "California", zip: "90001", country: "US" },
|
|
56
|
+
# contact: { firstName: "...", lastName: "...", email: "...", phone: "+15551234567" },
|
|
57
|
+
# use_case: "Insurance Services",
|
|
58
|
+
# use_case_summary: "...",
|
|
59
|
+
# sample_messages: "...",
|
|
60
|
+
# opt_in_workflow: "...",
|
|
61
|
+
# entity_type: "SOLE_PROPRIETOR")
|
|
62
|
+
#
|
|
63
|
+
# Example (partial-update resubmit, only changing email):
|
|
64
|
+
#
|
|
65
|
+
# client.enterprise.workspaces.submit_verification(workspace_id,
|
|
66
|
+
# contact: { email: "new@email.com" })
|
|
67
|
+
def submit_verification(workspace_id, business_name: nil, doing_business_as: nil, website: nil, entity_type: nil, address: nil, contact: nil, brn: nil, brn_type: nil, brn_country: nil, use_case: nil, use_case_summary: nil, sample_messages: nil, opt_in_workflow: nil, opt_in_image_urls: nil, monthly_volume: nil, additional_information: nil, age_gated_content: nil, isv_reseller: nil, privacy_url: nil, terms_url: nil)
|
|
35
68
|
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
36
69
|
|
|
37
|
-
body = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
body[:
|
|
70
|
+
body = {}
|
|
71
|
+
body[:businessName] = business_name unless business_name.nil?
|
|
72
|
+
body[:doingBusinessAs] = doing_business_as unless doing_business_as.nil?
|
|
73
|
+
body[:website] = website unless website.nil?
|
|
74
|
+
body[:entityType] = entity_type unless entity_type.nil?
|
|
75
|
+
body[:address] = address unless address.nil?
|
|
76
|
+
body[:contact] = contact unless contact.nil?
|
|
77
|
+
body[:brn] = brn unless brn.nil?
|
|
78
|
+
body[:brnType] = brn_type unless brn_type.nil?
|
|
79
|
+
body[:brnCountry] = brn_country unless brn_country.nil?
|
|
80
|
+
body[:useCase] = use_case unless use_case.nil?
|
|
81
|
+
body[:useCaseSummary] = use_case_summary unless use_case_summary.nil?
|
|
82
|
+
body[:sampleMessages] = sample_messages unless sample_messages.nil?
|
|
83
|
+
body[:optInWorkflow] = opt_in_workflow unless opt_in_workflow.nil?
|
|
84
|
+
body[:optInImageUrls] = opt_in_image_urls unless opt_in_image_urls.nil?
|
|
85
|
+
body[:monthlyVolume] = monthly_volume unless monthly_volume.nil?
|
|
86
|
+
body[:additionalInformation] = additional_information unless additional_information.nil?
|
|
87
|
+
body[:ageGatedContent] = age_gated_content unless age_gated_content.nil?
|
|
88
|
+
body[:isvReseller] = isv_reseller unless isv_reseller.nil?
|
|
89
|
+
body[:privacyUrl] = privacy_url unless privacy_url.nil?
|
|
90
|
+
body[:termsUrl] = terms_url unless terms_url.nil?
|
|
49
91
|
|
|
50
92
|
@client.post("/enterprise/workspaces/#{workspace_id}/verification/submit", body)
|
|
51
93
|
end
|
|
52
94
|
|
|
95
|
+
# Convenience alias for resubmits. Identical to +submit_verification+
|
|
96
|
+
# but reads more naturally when you only want to update a few fields
|
|
97
|
+
# after a rejection.
|
|
98
|
+
#
|
|
99
|
+
# client.enterprise.workspaces.resubmit_verification(workspace_id,
|
|
100
|
+
# contact: { email: "new@email.com" })
|
|
101
|
+
def resubmit_verification(workspace_id, **partial_updates)
|
|
102
|
+
submit_verification(workspace_id, **partial_updates)
|
|
103
|
+
end
|
|
104
|
+
|
|
53
105
|
def inherit_verification(workspace_id, source_workspace_id:)
|
|
54
106
|
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
55
107
|
raise ArgumentError, "Source workspace ID is required" if source_workspace_id.nil? || source_workspace_id.empty?
|
data/lib/sendly/version.rb
CHANGED
data/lib/sendly.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sendly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.33.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sendly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- examples/send_sms.rb
|
|
126
126
|
- lib/sendly.rb
|
|
127
127
|
- lib/sendly/account_resource.rb
|
|
128
|
+
- lib/sendly/business_upgrade_resource.rb
|
|
128
129
|
- lib/sendly/campaigns_resource.rb
|
|
129
130
|
- lib/sendly/client.rb
|
|
130
131
|
- lib/sendly/contacts_resource.rb
|