sendly 3.35.0 → 3.37.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.
@@ -0,0 +1,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sendly
4
+ # A business identity registered for carrier review. The status starts
5
+ # +"pending"+ and moves to +"verified"+ (campaigns can be created) or
6
+ # +"failed"+ (see +failure_reasons+).
7
+ class TenDlcBrand
8
+ attr_reader :id, :legal_name, :dba, :entity_type, :ein, :vertical,
9
+ :website, :status, :identity_status, :failure_reasons,
10
+ :created_at, :updated_at
11
+
12
+ def initialize(data)
13
+ @id = data["id"]
14
+ @legal_name = data["legalName"] || data["legal_name"]
15
+ @dba = data["dba"]
16
+ @entity_type = data["entityType"] || data["entity_type"]
17
+ @ein = data["ein"]
18
+ @vertical = data["vertical"]
19
+ @website = data["website"]
20
+ @status = data["status"]
21
+ @identity_status = data["identityStatus"] || data["identity_status"]
22
+ @failure_reasons = data["failureReasons"] || data["failure_reasons"]
23
+ @created_at = data["createdAt"] || data["created_at"]
24
+ @updated_at = data["updatedAt"] || data["updated_at"]
25
+ end
26
+
27
+ def pending?
28
+ status == "pending"
29
+ end
30
+
31
+ def verified?
32
+ status == "verified"
33
+ end
34
+
35
+ def failed?
36
+ status == "failed"
37
+ end
38
+
39
+ def to_h
40
+ {
41
+ id: id, legal_name: legal_name, dba: dba, entity_type: entity_type,
42
+ ein: ein, vertical: vertical, website: website, status: status,
43
+ identity_status: identity_status, failure_reasons: failure_reasons,
44
+ created_at: created_at, updated_at: updated_at
45
+ }.compact
46
+ end
47
+ end
48
+
49
+ # Messaging throughput granted by the carrier network.
50
+ class TenDlcThroughput
51
+ attr_reader :tier, :carriers_ready
52
+
53
+ def initialize(data)
54
+ @tier = data["tier"]
55
+ @carriers_ready = data["carriersReady"] || data["carriers_ready"]
56
+ end
57
+
58
+ def to_h
59
+ { tier: tier, carriers_ready: carriers_ready }.compact
60
+ end
61
+ end
62
+
63
+ # The result of a use-case qualification pre-check. When +qualified+ is
64
+ # false, +reason+ explains why; +throughput+ carries the expected tier
65
+ # when the carrier network reports it.
66
+ class TenDlcQualifyResult
67
+ attr_reader :use_case, :qualified, :reason, :throughput
68
+
69
+ def initialize(data)
70
+ @use_case = data["useCase"] || data["use_case"]
71
+ @qualified = data["qualified"]
72
+ @reason = data["reason"]
73
+ @throughput = data["throughput"] ? TenDlcThroughput.new(data["throughput"]) : nil
74
+ end
75
+
76
+ def qualified?
77
+ qualified == true
78
+ end
79
+
80
+ def to_h
81
+ {
82
+ use_case: use_case, qualified: qualified, reason: reason,
83
+ throughput: throughput&.to_h
84
+ }.compact
85
+ end
86
+ end
87
+
88
+ # A messaging campaign registered for carrier review. The status starts
89
+ # +"pending"+ and moves to +"active"+ (numbers can be assigned) or
90
+ # +"failed"+ (see +failure_reasons+); the carrier network may later mark
91
+ # it +"suspended"+ or +"expired"+.
92
+ class TenDlcCampaign
93
+ attr_reader :id, :brand_id, :use_case, :sub_use_cases, :description,
94
+ :status, :sample_messages, :throughput, :failure_reasons,
95
+ :created_at, :updated_at
96
+
97
+ def initialize(data)
98
+ @id = data["id"]
99
+ @brand_id = data["brandId"] || data["brand_id"]
100
+ @use_case = data["useCase"] || data["use_case"]
101
+ @sub_use_cases = data["subUseCases"] || data["sub_use_cases"] || []
102
+ @description = data["description"]
103
+ @status = data["status"]
104
+ @sample_messages = data["sampleMessages"] || data["sample_messages"] || []
105
+ @throughput = data["throughput"] ? TenDlcThroughput.new(data["throughput"]) : nil
106
+ @failure_reasons = data["failureReasons"] || data["failure_reasons"]
107
+ @created_at = data["createdAt"] || data["created_at"]
108
+ @updated_at = data["updatedAt"] || data["updated_at"]
109
+ end
110
+
111
+ def pending?
112
+ status == "pending"
113
+ end
114
+
115
+ def active?
116
+ status == "active"
117
+ end
118
+
119
+ def failed?
120
+ status == "failed"
121
+ end
122
+
123
+ def to_h
124
+ {
125
+ id: id, brand_id: brand_id, use_case: use_case,
126
+ sub_use_cases: sub_use_cases, description: description,
127
+ status: status, sample_messages: sample_messages,
128
+ throughput: throughput&.to_h, failure_reasons: failure_reasons,
129
+ created_at: created_at, updated_at: updated_at
130
+ }.compact
131
+ end
132
+ end
133
+
134
+ # A phone number assigned to a campaign. The number can send once the
135
+ # status is +"Active"+.
136
+ class TenDlcAssignment
137
+ attr_reader :id, :campaign_id, :phone_number, :status, :assigned_at
138
+
139
+ def initialize(data)
140
+ @id = data["id"]
141
+ @campaign_id = data["campaignId"] || data["campaign_id"]
142
+ @phone_number = data["phoneNumber"] || data["phone_number"]
143
+ @status = data["status"]
144
+ @assigned_at = data["assignedAt"] || data["assigned_at"]
145
+ end
146
+
147
+ def active?
148
+ status == "Active"
149
+ end
150
+
151
+ def to_h
152
+ {
153
+ id: id, campaign_id: campaign_id, phone_number: phone_number,
154
+ status: status, assigned_at: assigned_at
155
+ }.compact
156
+ end
157
+ end
158
+
159
+ # 10DLC resource — register your business for carrier review so you can
160
+ # text from local (10-digit) US numbers. The flow has three steps:
161
+ #
162
+ # 1. Register a brand with {#create_brand}, then poll {#get_brand} until
163
+ # it is verified.
164
+ # 2. Create a campaign under the verified brand with {#create_campaign},
165
+ # then poll {#get_campaign} until it is active. {#qualify} pre-checks
166
+ # a use case before you create the campaign.
167
+ # 3. Attach a number you own with {#assign_number}. Once the assignment
168
+ # is "Active", the number can send.
169
+ #
170
+ # Writes require a live API key with the +tendlc:write+ scope.
171
+ #
172
+ # @example Register a brand and poll until it's verified
173
+ # brand = client.ten_dlc.create_brand(
174
+ # legal_name: "Acme Holdings LLC",
175
+ # ein: "12-3456789",
176
+ # website: "https://acme.example",
177
+ # email: "ops@acme.example"
178
+ # )
179
+ # # ...poll client.ten_dlc.get_brand(brand.id) until brand.verified?
180
+ #
181
+ # @example Pre-check the use case, then create a campaign
182
+ # check = client.ten_dlc.qualify(brand.id, "MIXED")
183
+ # if check.qualified?
184
+ # campaign = client.ten_dlc.create_campaign(
185
+ # brand_id: brand.id,
186
+ # use_case: "MIXED",
187
+ # description: "Order updates and support replies for Acme customers",
188
+ # message_flow: "Customers opt in at checkout on acme.example",
189
+ # sample_messages: ["Your order #123 has shipped!"]
190
+ # )
191
+ # # ...poll client.ten_dlc.get_campaign(campaign.id) until campaign.active?
192
+ # end
193
+ #
194
+ # @example Assign a number you own
195
+ # client.ten_dlc.assign_number(campaign.id, phone_number: "+15551234567")
196
+ class TenDlcResource
197
+ def initialize(client)
198
+ @client = client
199
+ end
200
+
201
+ # List the brands registered for carrier review.
202
+ #
203
+ # @return [Hash] +{ brands: Array<TenDlcBrand> }+
204
+ def list_brands
205
+ response = @client.get("/tendlc/brands")
206
+ brands = (response["data"] || []).map { |b| TenDlcBrand.new(b) }
207
+ { brands: brands }
208
+ end
209
+
210
+ # Register a brand for carrier review — step 1 of enabling local-number
211
+ # texting. Requires a live API key.
212
+ #
213
+ # The brand starts pending. Poll {#get_brand} until it is verified
214
+ # before creating a campaign.
215
+ #
216
+ # @param legal_name [String] Legal business name
217
+ # @param dba [String, nil] "Doing business as" name, if different from the legal name
218
+ # @param ein [String, nil] Business registration number (e.g. EIN)
219
+ # @param entity_type [String, nil] Business entity type (e.g. "PRIVATE_PROFIT",
220
+ # "SOLE_PROPRIETOR"); the API defaults to "PRIVATE_PROFIT"
221
+ # @param vertical [String, nil] Industry vertical
222
+ # @param website [String, nil] Business website URL
223
+ # @param email [String, nil] Business contact email
224
+ # @param phone [String, nil] Business phone number
225
+ # @param mobile_phone [String, nil] Business mobile phone number
226
+ # @param street [String, nil] Street address
227
+ # @param city [String, nil] City
228
+ # @param state [String, nil] State or region
229
+ # @param postal_code [String, nil] Postal code
230
+ # @param country [String, nil] ISO 3166-1 alpha-2 country code; the API defaults to "US"
231
+ # @param verification_id [String, nil] Existing Sendly verification to prefill
232
+ # business details from
233
+ # @return [TenDlcBrand]
234
+ def create_brand(legal_name:, dba: nil, ein: nil, entity_type: nil,
235
+ vertical: nil, website: nil, email: nil, phone: nil,
236
+ mobile_phone: nil, street: nil, city: nil, state: nil,
237
+ postal_code: nil, country: nil, verification_id: nil)
238
+ raise ValidationError, "legal_name is required" if legal_name.nil? || legal_name.to_s.empty?
239
+
240
+ body = { legalName: legal_name }
241
+ body[:dba] = dba if dba
242
+ body[:ein] = ein if ein
243
+ body[:entityType] = entity_type if entity_type
244
+ body[:vertical] = vertical if vertical
245
+ body[:website] = website if website
246
+ body[:email] = email if email
247
+ body[:phone] = phone if phone
248
+ body[:mobilePhone] = mobile_phone if mobile_phone
249
+ body[:street] = street if street
250
+ body[:city] = city if city
251
+ body[:state] = state if state
252
+ body[:postalCode] = postal_code if postal_code
253
+ body[:country] = country if country
254
+ body[:verificationId] = verification_id if verification_id
255
+
256
+ response = @client.post("/tendlc/brands", body)
257
+ TenDlcBrand.new(response["data"] || {})
258
+ end
259
+
260
+ # Fetch one brand. Also refreshes its carrier-review status, so polling
261
+ # this method shows progress (pending -> verified/failed).
262
+ #
263
+ # @param id [String] Brand identifier
264
+ # @return [TenDlcBrand]
265
+ def get_brand(id)
266
+ raise ValidationError, "Brand ID is required" if id.nil? || id.to_s.empty?
267
+
268
+ encoded_id = URI.encode_www_form_component(id)
269
+ response = @client.get("/tendlc/brands/#{encoded_id}")
270
+ TenDlcBrand.new(response["data"] || {})
271
+ end
272
+
273
+ # Pre-check whether a use case qualifies for a brand on the carrier
274
+ # network before creating a campaign.
275
+ #
276
+ # @param brand_id [String] Brand identifier
277
+ # @param use_case [String] Use-case code (e.g. "MIXED", "MARKETING",
278
+ # "ACCOUNT_NOTIFICATION", "2FA")
279
+ # @return [TenDlcQualifyResult]
280
+ def qualify(brand_id, use_case)
281
+ raise ValidationError, "Brand ID is required" if brand_id.nil? || brand_id.to_s.empty?
282
+ raise ValidationError, "use_case is required" if use_case.nil? || use_case.to_s.empty?
283
+
284
+ encoded_id = URI.encode_www_form_component(brand_id)
285
+ encoded_use_case = URI.encode_www_form_component(use_case)
286
+ response = @client.get("/tendlc/brands/#{encoded_id}/qualify/#{encoded_use_case}")
287
+ TenDlcQualifyResult.new(response["data"] || {})
288
+ end
289
+
290
+ # List your messaging campaigns.
291
+ #
292
+ # @return [Hash] +{ campaigns: Array<TenDlcCampaign> }+
293
+ def list_campaigns
294
+ response = @client.get("/tendlc/campaigns")
295
+ campaigns = (response["data"] || []).map { |c| TenDlcCampaign.new(c) }
296
+ { campaigns: campaigns }
297
+ end
298
+
299
+ # Create a messaging campaign under a verified brand and submit it for
300
+ # carrier review. Requires a live API key.
301
+ #
302
+ # The campaign starts pending. Poll {#get_campaign} until it is active
303
+ # before assigning numbers.
304
+ #
305
+ # @param brand_id [String] The verified brand to create the campaign under
306
+ # @param use_case [String] Primary use-case code (e.g. "MIXED", "MARKETING")
307
+ # @param description [String] What the campaign sends and why
308
+ # @param message_flow [String] How recipients opt in to receive messages
309
+ # @param sample_messages [Array<String>] Example messages the campaign sends
310
+ # (the first 5 are used)
311
+ # @param sub_use_cases [Array<String>, nil] Sub-use-case codes
312
+ # @param opt_in_keywords [String, nil] Comma-separated keywords that opt a recipient in
313
+ # @param opt_out_keywords [String, nil] Comma-separated keywords that opt a recipient out
314
+ # @param help_keywords [String, nil] Comma-separated keywords that request help
315
+ # @param opt_in_message [String, nil] Auto-reply sent on opt-in
316
+ # @param opt_out_message [String, nil] Auto-reply sent on opt-out
317
+ # @param help_message [String, nil] Auto-reply sent on a help request
318
+ # @param embedded_link [Boolean, nil] Whether messages may contain links;
319
+ # the API defaults to true
320
+ # @param embedded_phone [Boolean, nil] Whether messages may contain phone
321
+ # numbers; the API defaults to false
322
+ # @return [TenDlcCampaign]
323
+ def create_campaign(brand_id:, use_case:, description:, message_flow:,
324
+ sample_messages:, sub_use_cases: nil,
325
+ opt_in_keywords: nil, opt_out_keywords: nil,
326
+ help_keywords: nil, opt_in_message: nil,
327
+ opt_out_message: nil, help_message: nil,
328
+ embedded_link: nil, embedded_phone: nil)
329
+ raise ValidationError, "brand_id is required" if brand_id.nil? || brand_id.to_s.empty?
330
+ raise ValidationError, "use_case is required" if use_case.nil? || use_case.to_s.empty?
331
+ raise ValidationError, "description is required" if description.nil? || description.to_s.empty?
332
+ raise ValidationError, "message_flow is required" if message_flow.nil? || message_flow.to_s.empty?
333
+ raise ValidationError, "sample_messages is required" if sample_messages.nil? || sample_messages.empty?
334
+
335
+ body = {
336
+ brandId: brand_id,
337
+ useCase: use_case,
338
+ description: description,
339
+ messageFlow: message_flow,
340
+ sampleMessages: sample_messages
341
+ }
342
+ body[:subUseCases] = sub_use_cases if sub_use_cases
343
+ body[:optInKeywords] = opt_in_keywords if opt_in_keywords
344
+ body[:optOutKeywords] = opt_out_keywords if opt_out_keywords
345
+ body[:helpKeywords] = help_keywords if help_keywords
346
+ body[:optInMessage] = opt_in_message if opt_in_message
347
+ body[:optOutMessage] = opt_out_message if opt_out_message
348
+ body[:helpMessage] = help_message if help_message
349
+ body[:embeddedLink] = embedded_link unless embedded_link.nil?
350
+ body[:embeddedPhone] = embedded_phone unless embedded_phone.nil?
351
+
352
+ response = @client.post("/tendlc/campaigns", body)
353
+ TenDlcCampaign.new(response["data"] || {})
354
+ end
355
+
356
+ # Fetch one campaign. Also refreshes its carrier-review status, so
357
+ # polling this method shows progress (pending -> active) including
358
+ # throughput once carriers approve.
359
+ #
360
+ # @param id [String] Campaign identifier
361
+ # @return [TenDlcCampaign]
362
+ def get_campaign(id)
363
+ raise ValidationError, "Campaign ID is required" if id.nil? || id.to_s.empty?
364
+
365
+ encoded_id = URI.encode_www_form_component(id)
366
+ response = @client.get("/tendlc/campaigns/#{encoded_id}")
367
+ TenDlcCampaign.new(response["data"] || {})
368
+ end
369
+
370
+ # Assign a phone number you own to an active (carrier-approved)
371
+ # campaign, making the number sendable. Requires a live API key.
372
+ #
373
+ # Idempotent — re-assigning the same number to the same campaign
374
+ # returns the existing assignment.
375
+ #
376
+ # @param campaign_id [String] Campaign identifier
377
+ # @param phone_number [String] E.164 number the workspace already owns
378
+ # @return [TenDlcAssignment] The number can send once its status is "Active"
379
+ def assign_number(campaign_id, phone_number:)
380
+ raise ValidationError, "Campaign ID is required" if campaign_id.nil? || campaign_id.to_s.empty?
381
+ raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
382
+
383
+ encoded_id = URI.encode_www_form_component(campaign_id)
384
+ response = @client.post("/tendlc/campaigns/#{encoded_id}/assign", { phoneNumber: phone_number })
385
+ TenDlcAssignment.new(response["data"] || {})
386
+ end
387
+
388
+ # List your number-to-campaign assignments.
389
+ #
390
+ # @return [Hash] +{ assignments: Array<TenDlcAssignment> }+
391
+ def list_assignments
392
+ response = @client.get("/tendlc/assignments")
393
+ assignments = (response["data"] || []).map { |a| TenDlcAssignment.new(a) }
394
+ { assignments: assignments }
395
+ end
396
+ end
397
+ end
data/lib/sendly/types.rb CHANGED
@@ -208,6 +208,85 @@ module Sendly
208
208
  end
209
209
  end
210
210
 
211
+ # Represents the result of sending a group MMS to 2-8 recipients.
212
+ #
213
+ # Unlike {Message}, +to+ is an array of recipients and the response carries a
214
+ # +group_message_id+ identifying the shared conversation. The raw parsed
215
+ # response is preserved on +#raw+ so callers can read any field the server
216
+ # adds.
217
+ class GroupMessage
218
+ # @return [String] Message id — matches the id in delivery webhooks
219
+ attr_reader :id
220
+
221
+ # @return [String] Delivery status ("sent" on a live send, "delivered" when simulated)
222
+ attr_reader :status
223
+
224
+ # @return [Array<String>] The recipients the group message was sent to
225
+ attr_reader :to
226
+
227
+ # @return [String, nil] Identifier for the group conversation (present on live sends)
228
+ attr_reader :group_message_id
229
+
230
+ # @return [Boolean] True when the send was simulated and nothing reached the carrier
231
+ attr_reader :simulated
232
+
233
+ # @return [String, nil] Human-readable note, present on simulated sends
234
+ attr_reader :message
235
+
236
+ # @return [Hash] The raw parsed response
237
+ attr_reader :raw
238
+
239
+ def initialize(data)
240
+ @raw = data
241
+ @id = data["id"]
242
+ @status = data["status"]
243
+ @to = data["to"] || []
244
+ @group_message_id = data["group_message_id"] || data["groupMessageId"]
245
+ @simulated = data["simulated"] || false
246
+ @message = data["message"]
247
+ end
248
+
249
+ # @return [Boolean] Whether the send was simulated
250
+ def simulated?
251
+ simulated
252
+ end
253
+
254
+ def to_h
255
+ {
256
+ id: id, status: status, to: to,
257
+ group_message_id: group_message_id,
258
+ simulated: simulated, message: message
259
+ }.compact
260
+ end
261
+ end
262
+
263
+ # Represents the result of an AI message enhancement.
264
+ class EnhancedMessage
265
+ # @return [String] The rewritten message, capped at 160 characters (one SMS
266
+ # segment). Falls back to the original text when AI is unavailable.
267
+ attr_reader :enhanced
268
+
269
+ # @return [String] Short explanation of what changed (empty on the fallback path)
270
+ attr_reader :explanation
271
+
272
+ # @return [String, nil] The model that produced the enhancement, when available
273
+ attr_reader :model
274
+
275
+ # @return [Hash] The raw parsed response
276
+ attr_reader :raw
277
+
278
+ def initialize(data)
279
+ @raw = data
280
+ @enhanced = data["enhanced"]
281
+ @explanation = data["explanation"] || ""
282
+ @model = data["model"]
283
+ end
284
+
285
+ def to_h
286
+ { enhanced: enhanced, explanation: explanation, model: model }.compact
287
+ end
288
+ end
289
+
211
290
  # ============================================================================
212
291
  # Media
213
292
  # ============================================================================
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "3.35.0"
4
+ VERSION = "3.37.0"
5
5
  end
data/lib/sendly.rb CHANGED
@@ -23,6 +23,8 @@ require_relative "sendly/rules_resource"
23
23
  require_relative "sendly/enterprise"
24
24
  require_relative "sendly/business_upgrade_resource"
25
25
  require_relative "sendly/numbers_resource"
26
+ require_relative "sendly/tendlc_resource"
27
+ require_relative "sendly/links_resource"
26
28
 
27
29
  # Sendly Ruby SDK
28
30
  #
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.35.0
4
+ version: 3.37.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-06-26 00:00:00.000000000 Z
11
+ date: 2026-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -134,11 +134,13 @@ files:
134
134
  - lib/sendly/enterprise.rb
135
135
  - lib/sendly/errors.rb
136
136
  - lib/sendly/labels_resource.rb
137
+ - lib/sendly/links_resource.rb
137
138
  - lib/sendly/media.rb
138
139
  - lib/sendly/messages.rb
139
140
  - lib/sendly/numbers_resource.rb
140
141
  - lib/sendly/rules_resource.rb
141
142
  - lib/sendly/templates_resource.rb
143
+ - lib/sendly/tendlc_resource.rb
142
144
  - lib/sendly/types.rb
143
145
  - lib/sendly/verify.rb
144
146
  - lib/sendly/version.rb