sendly 3.27.1 → 3.29.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: 68163da427953fe0e6323698c1f535ff209375de0d3ef39636a6baa058ec2e73
4
- data.tar.gz: 51e7b707e397c788fa7d93360ff20970df48bd9c89510c3acc8c55ecf68e3f9a
3
+ metadata.gz: c41be641303c996be30a1408b25aa49607676c0b6a8511ced4a5275e439602d9
4
+ data.tar.gz: 6228171a120073a65d7fe6b52500906785022d4c2d185e2b678b28a2ffc7e9b3
5
5
  SHA512:
6
- metadata.gz: 4885dadab414fb8f67f61eb2bd333294081ccb7c2db7c26be3d6e2ebfa283c6318fae433e23d1de126d87108bc1d0970c680607df6d025ef4fa473eb64a463e8
7
- data.tar.gz: 992233c97e17dcb0af07df5caf61e4031fb1bf378fb653e6dba158ea831e4d5ed18917a4429683daf77cda6783893e78486b01fde6dd7d51303d8feaa8d79951
6
+ metadata.gz: 7c9239c487bf91a790e2b639a724d2fea57b15032524ab4738de185604ca242100416ec43096f7308f820b42ae0eb8134121f597a12052b4fb232e6b5e325b6d
7
+ data.tar.gz: 80ef1fc95098ab7a4ca6bff2afe4e08d82007bae9979a49ce59e4f0fb222ab6353958fd2a7e561cbd4d37323cbafc2781cfab72064633e12da4f967f3f65d163
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # sendly (Ruby)
2
2
 
3
+ ## 3.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `contacts.bulk_mark_valid(ids: ..., list_id: ...)`: clear the invalid flag on many contacts at once (up to 10,000 per call). Escape hatch for when auto-mark misclassifies at scale.
8
+ - Four new list-health webhook event constants in `Sendly::Webhooks`: `EVENT_CONTACT_AUTO_FLAGGED`, `EVENT_CONTACT_MARKED_VALID`, `EVENT_CONTACTS_LOOKUP_COMPLETED`, `EVENT_CONTACTS_BULK_MARKED_VALID`.
9
+ - New `Sendly::Webhooks::ListHealthEventSource` module with frozen constants (`SEND_FAILURE | CARRIER_LOOKUP | USER_ACTION | BULK_MARK_VALID`) for the `source` field on auto-flag and mark-valid webhooks.
10
+ - `Contact` gains `user_marked_valid_at` — when a user manually cleared an auto-flag. Carrier re-checks respect this timestamp and leave the contact clean.
11
+
12
+ ## 3.28.0
13
+
14
+ ### Minor Changes
15
+
16
+ - `contacts.mark_valid(id)`: clear the auto-exclusion flag on a contact.
17
+ - `contacts.check_numbers(list_id: nil, force: false)`: trigger a background carrier lookup.
18
+ - `Contact` gains `line_type`, `carrier_name`, `line_type_checked_at`, `invalid_reason`, `invalidated_at` plus `invalid?` helper.
19
+
3
20
  ## 3.18.1
4
21
 
5
22
  ### Patch Changes
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sendly (3.27.1)
4
+ sendly (3.29.0)
5
5
  faraday (~> 2.0)
6
6
  faraday-retry (~> 2.0)
7
7
 
@@ -3,6 +3,8 @@
3
3
  module Sendly
4
4
  class Contact
5
5
  attr_reader :id, :phone_number, :name, :email, :metadata, :opted_out,
6
+ :line_type, :carrier_name, :line_type_checked_at,
7
+ :invalid_reason, :invalidated_at, :user_marked_valid_at,
6
8
  :created_at, :updated_at, :lists
7
9
 
8
10
  def initialize(data)
@@ -12,6 +14,12 @@ module Sendly
12
14
  @email = data["email"]
13
15
  @metadata = data["metadata"] || {}
14
16
  @opted_out = data["opted_out"] || data["optedOut"] || false
17
+ @line_type = data["line_type"] || data["lineType"]
18
+ @carrier_name = data["carrier_name"] || data["carrierName"]
19
+ @line_type_checked_at = parse_time(data["line_type_checked_at"] || data["lineTypeCheckedAt"])
20
+ @invalid_reason = data["invalid_reason"] || data["invalidReason"]
21
+ @invalidated_at = parse_time(data["invalidated_at"] || data["invalidatedAt"])
22
+ @user_marked_valid_at = parse_time(data["user_marked_valid_at"] || data["userMarkedValidAt"])
15
23
  @created_at = parse_time(data["created_at"] || data["createdAt"])
16
24
  @updated_at = parse_time(data["updated_at"] || data["updatedAt"])
17
25
  @lists = data["lists"]&.map { |l| { id: l["id"], name: l["name"] } }
@@ -21,10 +29,19 @@ module Sendly
21
29
  opted_out
22
30
  end
23
31
 
32
+ def invalid?
33
+ !invalid_reason.nil?
34
+ end
35
+
24
36
  def to_h
25
37
  {
26
38
  id: id, phone_number: phone_number, name: name, email: email,
27
39
  metadata: metadata, opted_out: opted_out,
40
+ line_type: line_type, carrier_name: carrier_name,
41
+ line_type_checked_at: line_type_checked_at&.iso8601,
42
+ invalid_reason: invalid_reason,
43
+ invalidated_at: invalidated_at&.iso8601,
44
+ user_marked_valid_at: user_marked_valid_at&.iso8601,
28
45
  created_at: created_at&.iso8601, updated_at: updated_at&.iso8601,
29
46
  lists: lists
30
47
  }.compact
@@ -186,6 +203,45 @@ module Sendly
186
203
  @client.delete("/contacts/#{id}")
187
204
  end
188
205
 
206
+ # Clear the invalid flag on a contact so future campaigns include it again.
207
+ # Contacts get auto-flagged when a send fails with a terminal bad-number
208
+ # error (landline, invalid number) or when a carrier lookup reports they
209
+ # can't receive SMS.
210
+ def mark_valid(id)
211
+ response = @client.post("/contacts/#{id}/mark-valid", {})
212
+ Contact.new(response)
213
+ end
214
+
215
+ # Clear the invalid flag on many contacts at once — the escape hatch for
216
+ # when auto-flag misclassifies at scale. Pass either an explicit id array
217
+ # (up to 10,000 per call) OR a list_id, not both. Foreign ids silently
218
+ # no-op via the per-organization filter.
219
+ #
220
+ # @param ids [Array<String>, nil] Explicit contact ids to clear
221
+ # @param list_id [String, nil] Clear every flagged member of this list
222
+ # @return [Hash] `{ cleared: Integer }` — number of contacts whose flag was
223
+ # actually cleared. Already-clean contacts and foreign ids don't count.
224
+ def bulk_mark_valid(ids: nil, list_id: nil)
225
+ if ids.nil? && list_id.nil?
226
+ raise ArgumentError, "bulk_mark_valid requires either :ids or :list_id"
227
+ end
228
+ if ids && list_id
229
+ raise ArgumentError, "bulk_mark_valid accepts :ids OR :list_id, not both"
230
+ end
231
+
232
+ body = ids ? { ids: ids } : { listId: list_id }
233
+ response = @client.post("/contacts/bulk-mark-valid", body)
234
+ { cleared: response["cleared"] || 0 }
235
+ end
236
+
237
+ # Trigger a background carrier lookup across your contacts. Landlines
238
+ # and other non-SMS-capable numbers are auto-excluded from future
239
+ # campaigns. The lookup runs asynchronously (1-5 minutes).
240
+ # Options: list_id (scope to a single list), force (re-check already-looked-up)
241
+ def check_numbers(list_id: nil, force: false)
242
+ @client.post("/contacts/lookup", { listId: list_id, force: force })
243
+ end
244
+
189
245
  def import_contacts(contacts, list_id: nil, opted_in_at: nil)
190
246
  body = {
191
247
  contacts: contacts.map { |c|
data/lib/sendly/verify.rb CHANGED
@@ -3,28 +3,25 @@
3
3
  module Sendly
4
4
  class Verification
5
5
  attr_reader :id, :status, :phone, :delivery_status, :attempts, :max_attempts,
6
- :channel, :expires_at, :verified_at, :created_at, :sandbox,
7
- :app_name, :template_id, :profile_id, :metadata
6
+ :expires_at, :verified_at, :created_at, :sandbox,
7
+ :app_name, :template_id, :profile_id
8
8
 
9
9
  STATUSES = %w[pending verified expired failed].freeze
10
- CHANNELS = %w[sms whatsapp email].freeze
11
10
 
12
11
  def initialize(data)
13
12
  @id = data["id"]
14
13
  @status = data["status"]
15
14
  @phone = data["phone"]
16
- @delivery_status = data["deliveryStatus"] || data["delivery_status"]
15
+ @delivery_status = data["delivery_status"]
17
16
  @attempts = data["attempts"] || 0
18
- @max_attempts = data["maxAttempts"] || data["max_attempts"] || 3
19
- @channel = data["channel"] || "sms"
20
- @expires_at = parse_time(data["expiresAt"] || data["expires_at"])
21
- @verified_at = parse_time(data["verifiedAt"] || data["verified_at"])
22
- @created_at = parse_time(data["createdAt"] || data["created_at"])
17
+ @max_attempts = data["max_attempts"] || 3
18
+ @expires_at = parse_time(data["expires_at"])
19
+ @verified_at = parse_time(data["verified_at"])
20
+ @created_at = parse_time(data["created_at"])
23
21
  @sandbox = data["sandbox"] || false
24
- @app_name = data["appName"] || data["app_name"]
25
- @template_id = data["templateId"] || data["template_id"]
26
- @profile_id = data["profileId"] || data["profile_id"]
27
- @metadata = data["metadata"] || {}
22
+ @app_name = data["app_name"]
23
+ @template_id = data["template_id"]
24
+ @profile_id = data["profile_id"]
28
25
  end
29
26
 
30
27
  def pending?
@@ -46,10 +43,10 @@ module Sendly
46
43
  def to_h
47
44
  {
48
45
  id: id, status: status, phone: phone, delivery_status: delivery_status,
49
- attempts: attempts, max_attempts: max_attempts, channel: channel,
46
+ attempts: attempts, max_attempts: max_attempts,
50
47
  expires_at: expires_at&.iso8601, verified_at: verified_at&.iso8601,
51
48
  created_at: created_at&.iso8601, sandbox: sandbox, app_name: app_name,
52
- template_id: template_id, profile_id: profile_id, metadata: metadata
49
+ template_id: template_id, profile_id: profile_id
53
50
  }.compact
54
51
  end
55
52
 
@@ -64,25 +61,32 @@ module Sendly
64
61
  end
65
62
 
66
63
  class SendVerificationResponse
67
- attr_reader :verification, :code
64
+ attr_reader :id, :status, :phone, :expires_at, :sandbox, :sandbox_code, :message
68
65
 
69
66
  def initialize(data)
70
- @verification = Verification.new(data["verification"])
71
- @code = data["code"]
67
+ @id = data["id"]
68
+ @status = data["status"]
69
+ @phone = data["phone"]
70
+ @expires_at = data["expires_at"]
71
+ @sandbox = data["sandbox"] || false
72
+ @sandbox_code = data["sandbox_code"]
73
+ @message = data["message"]
72
74
  end
73
75
  end
74
76
 
75
77
  class CheckVerificationResponse
76
- attr_reader :valid, :status, :verification
78
+ attr_reader :id, :status, :phone, :verified_at, :remaining_attempts
77
79
 
78
80
  def initialize(data)
79
- @valid = data["valid"]
81
+ @id = data["id"]
80
82
  @status = data["status"]
81
- @verification = data["verification"] ? Verification.new(data["verification"]) : nil
83
+ @phone = data["phone"]
84
+ @verified_at = data["verified_at"]
85
+ @remaining_attempts = data["remaining_attempts"]
82
86
  end
83
87
 
84
- def valid?
85
- valid
88
+ def verified?
89
+ status == "verified"
86
90
  end
87
91
  end
88
92
 
@@ -163,18 +167,14 @@ module Sendly
163
167
  @sessions = SessionsResource.new(client)
164
168
  end
165
169
 
166
- def send(phone:, channel: nil, code_length: nil, expires_in: nil, max_attempts: nil,
167
- template_id: nil, profile_id: nil, app_name: nil, locale: nil, metadata: nil)
168
- body = { to: phone }
169
- body[:channel] = channel if channel
170
- body[:codeLength] = code_length if code_length
171
- body[:expiresIn] = expires_in if expires_in
172
- body[:maxAttempts] = max_attempts if max_attempts
173
- body[:templateId] = template_id if template_id
174
- body[:profileId] = profile_id if profile_id
175
- body[:appName] = app_name if app_name
176
- body[:locale] = locale if locale
177
- body[:metadata] = metadata if metadata
170
+ def send(to:, template_id: nil, profile_id: nil, app_name: nil,
171
+ timeout_secs: nil, code_length: nil)
172
+ body = { to: to }
173
+ body[:template_id] = template_id if template_id
174
+ body[:profile_id] = profile_id if profile_id
175
+ body[:app_name] = app_name if app_name
176
+ body[:timeout_secs] = timeout_secs if timeout_secs
177
+ body[:code_length] = code_length if code_length
178
178
 
179
179
  response = @client.post("/verify", body)
180
180
  SendVerificationResponse.new(response)
@@ -195,11 +195,10 @@ module Sendly
195
195
  Verification.new(response)
196
196
  end
197
197
 
198
- def list(limit: nil, status: nil, phone: nil)
198
+ def list(limit: nil, status: nil)
199
199
  params = {}
200
200
  params[:limit] = limit if limit
201
201
  params[:status] = status if status
202
- params[:phone] = phone if phone
203
202
 
204
203
  response = @client.get("/verify", params)
205
204
  verifications = (response["verifications"] || []).map { |v| Verification.new(v) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "3.27.0"
4
+ VERSION = "3.29.0"
5
5
  end
@@ -34,6 +34,40 @@ module Sendly
34
34
  module Webhooks
35
35
  SIGNATURE_TOLERANCE_SECONDS = 300
36
36
 
37
+ # Webhook event type string constants. Use these when subscribing
38
+ # instead of string literals so you catch typos at load time.
39
+ EVENT_MESSAGE_QUEUED = "message.queued"
40
+ EVENT_MESSAGE_SENT = "message.sent"
41
+ EVENT_MESSAGE_DELIVERED = "message.delivered"
42
+ EVENT_MESSAGE_FAILED = "message.failed"
43
+ EVENT_MESSAGE_BOUNCED = "message.bounced"
44
+ EVENT_MESSAGE_RETRYING = "message.retrying"
45
+ EVENT_MESSAGE_RECEIVED = "message.received"
46
+ EVENT_MESSAGE_OPT_OUT = "message.opt_out"
47
+ EVENT_MESSAGE_OPT_IN = "message.opt_in"
48
+ EVENT_VERIFICATION_CREATED = "verification.created"
49
+ EVENT_VERIFICATION_DELIVERED = "verification.delivered"
50
+ EVENT_VERIFICATION_VERIFIED = "verification.verified"
51
+ EVENT_VERIFICATION_EXPIRED = "verification.expired"
52
+ EVENT_VERIFICATION_FAILED = "verification.failed"
53
+ EVENT_VERIFICATION_RESENT = "verification.resent"
54
+ EVENT_VERIFICATION_DELIVERY_FAILED = "verification.delivery_failed"
55
+ EVENT_CONTACT_AUTO_FLAGGED = "contact.auto_flagged"
56
+ EVENT_CONTACT_MARKED_VALID = "contact.marked_valid"
57
+ EVENT_CONTACTS_LOOKUP_COMPLETED = "contacts.lookup_completed"
58
+ EVENT_CONTACTS_BULK_MARKED_VALID = "contacts.bulk_marked_valid"
59
+
60
+ # Source of a list-health event. Frozen enum — new values will be
61
+ # added in minor SDK versions, never removed.
62
+ module ListHealthEventSource
63
+ SEND_FAILURE = "send_failure"
64
+ CARRIER_LOOKUP = "carrier_lookup"
65
+ USER_ACTION = "user_action"
66
+ BULK_MARK_VALID = "bulk_mark_valid"
67
+
68
+ ALL = [SEND_FAILURE, CARRIER_LOOKUP, USER_ACTION, BULK_MARK_VALID].freeze
69
+ end
70
+
37
71
  class << self
38
72
  # Verify webhook signature from Sendly.
39
73
  #
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.27.1
4
+ version: 3.29.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-04-06 00:00:00.000000000 Z
11
+ date: 2026-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday