sendly 3.30.0 → 3.31.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0aa8097776c931f504cc3f006783ed8232691fc48c35c93b8930437c9f5480b5
4
- data.tar.gz: 74cfe38cea2edefae5a4173dbbae2ff4df2a3364e69de2329191ef95fd26195b
3
+ metadata.gz: 80748f70518bf065b5c3aec3495bbc69ef4641793a57186757ea1f10957de933
4
+ data.tar.gz: b98259199daccc2948950da3a6058b4b66140ff72b4c8a9b1ad5fae6a59a639c
5
5
  SHA512:
6
- metadata.gz: 40d6bb9e8df8757fa585253b39b49e85973f7959f1278b8354c803b293047bb0dbf279dafd5c5550e61d4b6a26ed78ada5295ab686a312dc32f026d46ff7203f
7
- data.tar.gz: b4ee727e07b7fc3fa75326e36ae073c62d50e6d3c98b5905c745444c80d6dd358ab19a5ed36c0853af3c07eba7fbba50d6db7389f469c3555abf15d6960b5742
6
+ metadata.gz: 796976f6e39f8bb3fad45e86b9bdb5256521c46dbe566ace7c14ccb72192ea4902c79965b93cdf757eb23677e254166cbdc0ba2195ef51c0c90d85c7365984c6
7
+ data.tar.gz: 32b41bc79313a7f26d6cd6dac95125ad210168be1dcd8ca9e2389a7d1beb2c4678fa7f32769e4263c102cdf1b94ed68276bb8c84b84d5dc67c992c9238a1f230
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # sendly (Ruby)
2
2
 
3
+ ## 3.31.0
4
+
5
+ ### Patch Changes
6
+
7
+ - **`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:
8
+
9
+ ```ruby
10
+ # Positional (matches our docs)
11
+ client = Sendly::Client.new("sk_live_v1_xxx")
12
+ client = Sendly::Client.new("sk_live_v1_xxx", timeout: 60)
13
+
14
+ # Keyword (existing v3.30.0 signature — unchanged)
15
+ client = Sendly::Client.new(api_key: "sk_live_v1_xxx")
16
+ ```
17
+
18
+ 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.
19
+
20
+ ## 3.30.0
21
+
22
+ ### Minor Changes
23
+
24
+ - `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.
25
+ - **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.
26
+ - `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.
27
+ - 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`).
28
+
29
+ ### Server-side fixes paired with this release
30
+
31
+ - `/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.
32
+ - Endpoint accepts both flat and `{ verification: {...} }` wrapped shapes (matches `/enterprise/provision`).
33
+ - `use_case` validation expanded from 23 entries to the full 43-value Telnyx enum.
34
+
3
35
  ## 3.29.0
4
36
 
5
37
  ### Minor Changes
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sendly (3.30.0)
4
+ sendly (3.31.0)
5
5
  faraday (~> 2.0)
6
6
  faraday-retry (~> 2.0)
7
7
 
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.data.each { |msg| puts "#{msg.id}: #{msg.scheduled_at}" }
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.credits_refunded} credits"
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.batch_id
192
- puts "Queued: #{batch.queued}"
193
- puts "Failed: #{batch.failed}"
194
- puts "Credits used: #{batch.credits_used}"
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 "Total credits needed: #{preview.total_credits}"
210
- puts "Valid: #{preview.valid}, Invalid: #{preview.invalid}"
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.get_credits
270
- puts "Available: #{credits.available_balance} credits"
271
- puts "Reserved: #{credits.reserved_balance} credits"
272
- puts "Total: #{credits.balance} 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
- result = client.account.get_credit_transactions
276
- result.data.each do |tx|
277
- puts "#{tx.type}: #{tx.amount} credits - #{tx.description}"
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
- result = client.account.list_api_keys
282
- result.data.each do |key|
283
- puts "#{key.name}: #{key.prefix}*** (#{key.type})"
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
- new_key = client.account.create_api_key(
288
- name: 'Production Key',
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
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
- # @param api_key [String] Your Sendly API key
29
- # @param base_url [String] API base URL (optional)
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
- # @example
35
- # client = Sendly::Client.new("sk_live_v1_xxx")
36
- # client = Sendly::Client.new("sk_live_v1_xxx", timeout: 60, max_retries: 5)
37
- def initialize(api_key:, base_url: nil, timeout: 30, max_retries: 3, organization_id: nil)
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
@@ -31,25 +31,77 @@ module Sendly
31
31
  @client.delete("/enterprise/workspaces/#{workspace_id}")
32
32
  end
33
33
 
34
- def submit_verification(workspace_id, business_name:, business_type:, ein:, address:, city:, state:, zip:, use_case:, sample_messages:, monthly_volume: nil)
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
- business_name: business_name,
39
- business_type: business_type,
40
- ein: ein,
41
- address: address,
42
- city: city,
43
- state: state,
44
- zip: zip,
45
- use_case: use_case,
46
- sample_messages: sample_messages
47
- }
48
- body[:monthly_volume] = monthly_volume if monthly_volume
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?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "3.30.0"
4
+ VERSION = "3.31.0"
5
5
  end
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.30.0
4
+ version: 3.31.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-01 00:00:00.000000000 Z
11
+ date: 2026-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday