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 +4 -4
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +1 -1
- data/lib/sendly/contacts_resource.rb +56 -0
- data/lib/sendly/verify.rb +36 -37
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks.rb +34 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c41be641303c996be30a1408b25aa49607676c0b6a8511ced4a5275e439602d9
|
|
4
|
+
data.tar.gz: 6228171a120073a65d7fe6b52500906785022d4c2d185e2b678b28a2ffc7e9b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
-
:
|
|
7
|
-
:app_name, :template_id, :profile_id
|
|
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["
|
|
15
|
+
@delivery_status = data["delivery_status"]
|
|
17
16
|
@attempts = data["attempts"] || 0
|
|
18
|
-
@max_attempts = data["
|
|
19
|
-
@
|
|
20
|
-
@
|
|
21
|
-
@
|
|
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["
|
|
25
|
-
@template_id = data["
|
|
26
|
-
@profile_id = data["
|
|
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,
|
|
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
|
|
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 :
|
|
64
|
+
attr_reader :id, :status, :phone, :expires_at, :sandbox, :sandbox_code, :message
|
|
68
65
|
|
|
69
66
|
def initialize(data)
|
|
70
|
-
@
|
|
71
|
-
@
|
|
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 :
|
|
78
|
+
attr_reader :id, :status, :phone, :verified_at, :remaining_attempts
|
|
77
79
|
|
|
78
80
|
def initialize(data)
|
|
79
|
-
@
|
|
81
|
+
@id = data["id"]
|
|
80
82
|
@status = data["status"]
|
|
81
|
-
@
|
|
83
|
+
@phone = data["phone"]
|
|
84
|
+
@verified_at = data["verified_at"]
|
|
85
|
+
@remaining_attempts = data["remaining_attempts"]
|
|
82
86
|
end
|
|
83
87
|
|
|
84
|
-
def
|
|
85
|
-
|
|
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(
|
|
167
|
-
|
|
168
|
-
body = { to:
|
|
169
|
-
body[:
|
|
170
|
-
body[:
|
|
171
|
-
body[:
|
|
172
|
-
body[:
|
|
173
|
-
body[:
|
|
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
|
|
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) }
|
data/lib/sendly/version.rb
CHANGED
data/lib/sendly/webhooks.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
11
|
+
date: 2026-04-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|