sendly 4.2.0 → 4.3.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 +80 -0
- data/Gemfile.lock +1 -1
- data/README.md +742 -148
- data/examples/list_messages.rb +1 -1
- data/examples/send_sms.rb +6 -2
- data/lib/sendly/account_resource.rb +16 -4
- data/lib/sendly/business_upgrade_resource.rb +3 -2
- data/lib/sendly/calls_resource.rb +19 -8
- data/lib/sendly/campaigns_resource.rb +59 -8
- data/lib/sendly/client.rb +38 -19
- data/lib/sendly/conversations_resource.rb +17 -5
- data/lib/sendly/drafts_resource.rb +2 -1
- data/lib/sendly/enterprise.rb +21 -10
- data/lib/sendly/errors.rb +11 -2
- data/lib/sendly/messages.rb +31 -11
- data/lib/sendly/types.rb +165 -26
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks_resource.rb +30 -7
- data/lib/sendly/whatsapp_resource.rb +595 -44
- metadata +2 -2
|
@@ -4,16 +4,38 @@ module Sendly
|
|
|
4
4
|
# A newly started WhatsApp signup. Hand +connect_url+ to a human — they
|
|
5
5
|
# open it in a browser and log in with Facebook to link their WhatsApp
|
|
6
6
|
# Business Account. Poll {WhatsAppSignupResource#get} with +id+ until the
|
|
7
|
-
# status is +"active"+.
|
|
7
|
+
# status is +"active"+. +STATUSES+ keeps +"expired"+ for compatibility;
|
|
8
|
+
# the API does not send it.
|
|
9
|
+
#
|
|
10
|
+
# A number added to an already-connected WhatsApp Business Account (a
|
|
11
|
+
# +business_account_id+ passed to {WhatsAppSignupResource#create}) has no
|
|
12
|
+
# +connect_url+: its status is +"verifying"+ while Meta sends the number a
|
|
13
|
+
# code, and +phone_number+, +business_account_id+, +verification_method+
|
|
14
|
+
# (+"sms"+ or +"voice"+) and +verification_attempts_remaining+ are set.
|
|
15
|
+
# +updated_at+ is when the session last changed. The status can also be
|
|
16
|
+
# +"failed"+, with the reason in +failure_reasons+ (for example
|
|
17
|
+
# +["verification_start_failed"]+), when a concurrent request for the same
|
|
18
|
+
# number failed the session before this call returned it. Submit the code
|
|
19
|
+
# with {WhatsAppSignupResource#verify}. These readers are nil on a
|
|
20
|
+
# Facebook signup.
|
|
8
21
|
class WhatsAppSignupSession
|
|
9
|
-
attr_reader :id, :connect_url, :status
|
|
22
|
+
attr_reader :id, :connect_url, :status, :phone_number, :business_account_id,
|
|
23
|
+
:failure_reasons, :updated_at, :verification_method,
|
|
24
|
+
:verification_attempts_remaining
|
|
10
25
|
|
|
11
|
-
STATUSES = %w[initiated registering active failed expired].freeze
|
|
26
|
+
STATUSES = %w[initiated registering verifying active failed expired].freeze
|
|
12
27
|
|
|
13
28
|
def initialize(data)
|
|
14
29
|
@id = data["id"]
|
|
15
30
|
@connect_url = data["connectUrl"] || data["connect_url"]
|
|
16
31
|
@status = data["status"]
|
|
32
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
33
|
+
@business_account_id = data["businessAccountId"] || data["business_account_id"]
|
|
34
|
+
@failure_reasons = data["failureReasons"] || data["failure_reasons"]
|
|
35
|
+
@updated_at = data["updatedAt"] || data["updated_at"]
|
|
36
|
+
@verification_method = data["verificationMethod"] || data["verification_method"]
|
|
37
|
+
@verification_attempts_remaining =
|
|
38
|
+
data["verificationAttemptsRemaining"] || data["verification_attempts_remaining"]
|
|
17
39
|
end
|
|
18
40
|
|
|
19
41
|
def active?
|
|
@@ -24,19 +46,60 @@ module Sendly
|
|
|
24
46
|
status == "failed"
|
|
25
47
|
end
|
|
26
48
|
|
|
49
|
+
def verifying?
|
|
50
|
+
status == "verifying"
|
|
51
|
+
end
|
|
52
|
+
|
|
27
53
|
def to_h
|
|
28
|
-
{
|
|
54
|
+
{
|
|
55
|
+
id: id, connect_url: connect_url, status: status, phone_number: phone_number,
|
|
56
|
+
business_account_id: business_account_id, failure_reasons: failure_reasons,
|
|
57
|
+
updated_at: updated_at, verification_method: verification_method,
|
|
58
|
+
verification_attempts_remaining: verification_attempts_remaining
|
|
59
|
+
}.compact
|
|
29
60
|
end
|
|
30
61
|
end
|
|
31
62
|
|
|
32
|
-
# The status of a WhatsApp signup. +
|
|
33
|
-
#
|
|
34
|
-
#
|
|
63
|
+
# The status of a WhatsApp signup. +status+ is +"initiated"+,
|
|
64
|
+
# +"registering"+ (WhatsApp is activating the number; activation usually
|
|
65
|
+
# takes a few minutes but can take hours, and a session that hasn't
|
|
66
|
+
# finished about 6 hours after it began fails with +registration_timeout+
|
|
67
|
+
# and the fee is refunded), +"verifying"+ (a number being added to an
|
|
68
|
+
# already-connected account, waiting for its code), +"active"+ or
|
|
69
|
+
# +"failed"+. The API does not send +"expired"+,
|
|
70
|
+
# so {#expired?} is never true; it stays for compatibility.
|
|
71
|
+
# +business_account_id+ is set only while the status is +"verifying"+ or
|
|
72
|
+
# +"active"+ (from the start for a number added by code); it is nil while
|
|
73
|
+
# +"initiated"+ or +"registering"+ and after a failure.
|
|
74
|
+
# +failure_reasons+ is set when the status is +"failed"+, with one code:
|
|
75
|
+
# +setup_fee_payment_failed+, +signup_abandoned+, +meta_exchange_failed+,
|
|
76
|
+
# +registration_failed+, +waba_already_connected+, +waba_mismatch+ (the
|
|
77
|
+
# WhatsApp Business Account chosen in the Facebook step doesn't hold the
|
|
78
|
+
# verified number), +registration_timeout+ (activation hadn't finished
|
|
79
|
+
# about 6 hours after the session began), +phone_number_mismatch+,
|
|
80
|
+
# +verification_start_failed+ (WhatsApp couldn't start verifying an added
|
|
81
|
+
# number), +verification_failed+ (too many wrong codes) or
|
|
82
|
+
# +verification_expired+ (a verifying session left untouched for an hour).
|
|
83
|
+
# If the connection fails, the $19 fee is refunded automatically.
|
|
84
|
+
#
|
|
85
|
+
# While the status is +"verifying"+, +verification_method+ (+"sms"+ or
|
|
86
|
+
# +"voice"+) and +verification_attempts_remaining+ are set, and on
|
|
87
|
+
# {WhatsAppSignupResource#get} +verification_code+ is the code once Meta's
|
|
88
|
+
# text has arrived on the number (nil until then). Until a code has been
|
|
89
|
+
# submitted, it is the newest code that has arrived since the signup
|
|
90
|
+
# started, so after a resend it still shows the earlier code until the new
|
|
91
|
+
# one arrives. Once WhatsApp has checked a code, only a code that arrived
|
|
92
|
+
# after the last submission or resend is returned. A submission answered
|
|
93
|
+
# with 502 +whatsapp_verification_unavailable+ is not counted, so the same
|
|
94
|
+
# unchecked code can come back, and submitting it again with
|
|
95
|
+
# {WhatsAppSignupResource#verify} is safe. All three are nil in any other
|
|
96
|
+
# status.
|
|
35
97
|
class WhatsAppSignup
|
|
36
98
|
attr_reader :id, :status, :phone_number, :business_account_id,
|
|
37
|
-
:failure_reasons, :updated_at
|
|
99
|
+
:failure_reasons, :updated_at, :verification_method,
|
|
100
|
+
:verification_attempts_remaining, :verification_code
|
|
38
101
|
|
|
39
|
-
STATUSES = %w[initiated registering active failed expired].freeze
|
|
102
|
+
STATUSES = %w[initiated registering verifying active failed expired].freeze
|
|
40
103
|
|
|
41
104
|
def initialize(data)
|
|
42
105
|
@id = data["id"]
|
|
@@ -45,6 +108,10 @@ module Sendly
|
|
|
45
108
|
@business_account_id = data["businessAccountId"] || data["business_account_id"]
|
|
46
109
|
@failure_reasons = data["failureReasons"] || data["failure_reasons"]
|
|
47
110
|
@updated_at = data["updatedAt"] || data["updated_at"]
|
|
111
|
+
@verification_method = data["verificationMethod"] || data["verification_method"]
|
|
112
|
+
@verification_attempts_remaining =
|
|
113
|
+
data["verificationAttemptsRemaining"] || data["verification_attempts_remaining"]
|
|
114
|
+
@verification_code = data["verificationCode"] || data["verification_code"]
|
|
48
115
|
end
|
|
49
116
|
|
|
50
117
|
def active?
|
|
@@ -55,6 +122,10 @@ module Sendly
|
|
|
55
122
|
status == "failed"
|
|
56
123
|
end
|
|
57
124
|
|
|
125
|
+
def verifying?
|
|
126
|
+
status == "verifying"
|
|
127
|
+
end
|
|
128
|
+
|
|
58
129
|
def expired?
|
|
59
130
|
status == "expired"
|
|
60
131
|
end
|
|
@@ -63,7 +134,10 @@ module Sendly
|
|
|
63
134
|
{
|
|
64
135
|
id: id, status: status, phone_number: phone_number,
|
|
65
136
|
business_account_id: business_account_id,
|
|
66
|
-
failure_reasons: failure_reasons, updated_at: updated_at
|
|
137
|
+
failure_reasons: failure_reasons, updated_at: updated_at,
|
|
138
|
+
verification_method: verification_method,
|
|
139
|
+
verification_attempts_remaining: verification_attempts_remaining,
|
|
140
|
+
verification_code: verification_code
|
|
67
141
|
}.compact
|
|
68
142
|
end
|
|
69
143
|
end
|
|
@@ -71,10 +145,20 @@ module Sendly
|
|
|
71
145
|
# A number connected (or connecting) to WhatsApp. +display_name+ is the
|
|
72
146
|
# name recipients see — chosen during the connect flow and reviewed by
|
|
73
147
|
# Meta; nil until set. +quality_rating+ is Meta's rating (e.g. "GREEN"),
|
|
74
|
-
# nil before the first rating.
|
|
148
|
+
# nil before the first rating. +business_account_id+ is the WhatsApp
|
|
149
|
+
# Business Account the number belongs to (pass it to
|
|
150
|
+
# {WhatsAppSignupResource#create} to add another number to it) and
|
|
151
|
+
# +business_name+ that account's business name; both are nil while the
|
|
152
|
+
# sender is +"pending"+. +calling_enabled+ says whether WhatsApp calling
|
|
153
|
+
# is on (see {WhatsAppSendersResource#set_calling}), and
|
|
154
|
+
# +outbound_calling_allowed+ is false for every +1 number (the US, Canada
|
|
155
|
+
# and the rest of the North American numbering plan) and for +20 (Egypt),
|
|
156
|
+
# +84 (Vietnam) and +234 (Nigeria) numbers, where Meta forbids
|
|
157
|
+
# business-initiated calls.
|
|
75
158
|
class WhatsAppSender
|
|
76
159
|
attr_reader :phone_number, :display_name, :status, :quality_rating,
|
|
77
|
-
:created_at
|
|
160
|
+
:created_at, :business_account_id, :business_name,
|
|
161
|
+
:calling_enabled, :outbound_calling_allowed
|
|
78
162
|
|
|
79
163
|
STATUSES = %w[pending active suspended].freeze
|
|
80
164
|
|
|
@@ -84,6 +168,19 @@ module Sendly
|
|
|
84
168
|
@status = data["status"]
|
|
85
169
|
@quality_rating = data["qualityRating"] || data["quality_rating"]
|
|
86
170
|
@created_at = data["createdAt"] || data["created_at"]
|
|
171
|
+
@business_account_id = data["businessAccountId"] || data["business_account_id"]
|
|
172
|
+
@business_name = data["businessName"] || data["business_name"]
|
|
173
|
+
@calling_enabled = data.key?("callingEnabled") ? data["callingEnabled"] : data["calling_enabled"]
|
|
174
|
+
@outbound_calling_allowed =
|
|
175
|
+
data.key?("outboundCallingAllowed") ? data["outboundCallingAllowed"] : data["outbound_calling_allowed"]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def calling_enabled?
|
|
179
|
+
calling_enabled == true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def outbound_calling_allowed?
|
|
183
|
+
outbound_calling_allowed == true
|
|
87
184
|
end
|
|
88
185
|
|
|
89
186
|
def pending?
|
|
@@ -101,15 +198,19 @@ module Sendly
|
|
|
101
198
|
def to_h
|
|
102
199
|
{
|
|
103
200
|
phone_number: phone_number, display_name: display_name,
|
|
104
|
-
status: status, quality_rating: quality_rating, created_at: created_at
|
|
201
|
+
status: status, quality_rating: quality_rating, created_at: created_at,
|
|
202
|
+
business_account_id: business_account_id, business_name: business_name,
|
|
203
|
+
calling_enabled: calling_enabled, outbound_calling_allowed: outbound_calling_allowed
|
|
105
204
|
}.compact
|
|
106
205
|
end
|
|
107
206
|
end
|
|
108
207
|
|
|
109
208
|
# A sender's WhatsApp business profile — what recipients see when they
|
|
110
209
|
# open the sender's details in WhatsApp. Unset fields are nil.
|
|
111
|
-
# +profile_photo_url+
|
|
112
|
-
# {WhatsAppSendersResource#update_profile}
|
|
210
|
+
# +profile_photo_url+ cannot be set through
|
|
211
|
+
# {WhatsAppSendersResource#update_profile}; upload the photo with
|
|
212
|
+
# {WhatsAppSendersResource#upload_profile_photo} and remove it with
|
|
213
|
+
# {WhatsAppSendersResource#delete_profile_photo}.
|
|
113
214
|
class WhatsAppSenderProfile
|
|
114
215
|
attr_reader :phone_number, :display_name, :profile_photo_url, :category,
|
|
115
216
|
:about, :description, :email, :website, :address
|
|
@@ -136,11 +237,78 @@ module Sendly
|
|
|
136
237
|
end
|
|
137
238
|
end
|
|
138
239
|
|
|
240
|
+
# A command shown to a customer who types "/" in a chat with the sender.
|
|
241
|
+
# +command+ is the name without the leading slash.
|
|
242
|
+
class WhatsAppCommand
|
|
243
|
+
attr_reader :command, :description
|
|
244
|
+
|
|
245
|
+
def initialize(data)
|
|
246
|
+
@command = data["command"]
|
|
247
|
+
@description = data["description"]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def to_h
|
|
251
|
+
{ command: command, description: description }.compact
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# A sender's conversational components. +ice_breakers+ are the tappable
|
|
256
|
+
# suggestions shown when someone opens a chat with the business for the
|
|
257
|
+
# first time; +commands+ ({WhatsAppCommand}) are shown when the customer
|
|
258
|
+
# types "/". Both are empty lists when none are set.
|
|
259
|
+
class WhatsAppConversationalComponents
|
|
260
|
+
attr_reader :phone_number, :ice_breakers, :commands
|
|
261
|
+
|
|
262
|
+
def initialize(data)
|
|
263
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
264
|
+
@ice_breakers = data["iceBreakers"] || data["ice_breakers"] || []
|
|
265
|
+
@commands = (data["commands"] || []).map { |c| WhatsAppCommand.new(c) }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def to_h
|
|
269
|
+
{
|
|
270
|
+
phone_number: phone_number, ice_breakers: ice_breakers,
|
|
271
|
+
commands: commands.map(&:to_h)
|
|
272
|
+
}.compact
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Whether WhatsApp calling is on for a sender. +outbound_calling_allowed+
|
|
277
|
+
# is false for every +1 number (the US, Canada and the rest of the North
|
|
278
|
+
# American numbering plan) and for +20 (Egypt), +84 (Vietnam) and +234
|
|
279
|
+
# (Nigeria) numbers, where Meta forbids business-initiated calls.
|
|
280
|
+
class WhatsAppCallingSettings
|
|
281
|
+
attr_reader :phone_number, :calling_enabled, :outbound_calling_allowed
|
|
282
|
+
|
|
283
|
+
def initialize(data)
|
|
284
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
285
|
+
@calling_enabled = data.key?("callingEnabled") ? data["callingEnabled"] : data["calling_enabled"]
|
|
286
|
+
@outbound_calling_allowed =
|
|
287
|
+
data.key?("outboundCallingAllowed") ? data["outboundCallingAllowed"] : data["outbound_calling_allowed"]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def calling_enabled?
|
|
291
|
+
calling_enabled == true
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def outbound_calling_allowed?
|
|
295
|
+
outbound_calling_allowed == true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def to_h
|
|
299
|
+
{
|
|
300
|
+
phone_number: phone_number, calling_enabled: calling_enabled,
|
|
301
|
+
outbound_calling_allowed: outbound_calling_allowed
|
|
302
|
+
}.compact
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
139
306
|
# A WhatsApp message template. Meta reviews every template (usually
|
|
140
307
|
# 24-48h) and may reclassify its category — the category on the record
|
|
141
308
|
# is what drives per-message pricing. +rejection_reason+ is set when the
|
|
142
309
|
# status is +"REJECTED"+; +warnings+ carries non-blocking submission
|
|
143
|
-
# warnings on create responses.
|
|
310
|
+
# warnings on create responses. +STATUSES+ lists the common review
|
|
311
|
+
# statuses; Meta may report others, which come through in uppercase.
|
|
144
312
|
class WhatsAppTemplate
|
|
145
313
|
attr_reader :id, :name, :language, :category, :status, :quality_rating,
|
|
146
314
|
:rejection_reason, :created_at, :updated_at, :warnings
|
|
@@ -203,7 +371,11 @@ module Sendly
|
|
|
203
371
|
|
|
204
372
|
# Whether a 24-hour customer-service window is open between one of your
|
|
205
373
|
# WhatsApp senders and a recipient. +expires_at+ is when the window
|
|
206
|
-
# closes (ISO 8601)
|
|
374
|
+
# closes (ISO 8601). After it closes this is the past expiry, with +open+
|
|
375
|
+
# false. It is nil when Sendly has no window on record for the pair; a
|
|
376
|
+
# free-form send may still go through then if WhatsApp reports an open
|
|
377
|
+
# window, and otherwise fails with +whatsapp_window_closed+. The API
|
|
378
|
+
# returns exactly +{ open, expiresAt }+.
|
|
207
379
|
class WhatsAppWindow
|
|
208
380
|
attr_reader :open, :expires_at
|
|
209
381
|
|
|
@@ -249,8 +421,17 @@ module Sendly
|
|
|
249
421
|
end
|
|
250
422
|
|
|
251
423
|
# A sent WhatsApp message. Unlike {Message}, +segments+ is always 1
|
|
252
|
-
# (WhatsApp has no segment concept), +text+ is
|
|
253
|
-
#
|
|
424
|
+
# (WhatsApp has no segment concept), +text+ is the caption for a media
|
|
425
|
+
# send (pass it as +text+ with +media_urls+) and nil for template sends
|
|
426
|
+
# and media sent without a caption, and +whatsapp+ carries the
|
|
427
|
+
# channel-specific details. +credits_used+: free-form text or media
|
|
428
|
+
# inside the 24-hour window costs 1 credit each for the first 1,000 per
|
|
429
|
+
# sending number per calendar month (UTC), then the destination's utility
|
|
430
|
+
# template price; countries without a listed price use the default
|
|
431
|
+
# utility price of 12 credits. Templates are priced by category and
|
|
432
|
+
# destination country; countries without a listed price use 33
|
|
433
|
+
# (marketing), 12 (utility) and 12 (authentication) credits. A failed
|
|
434
|
+
# send gives its slot back.
|
|
254
435
|
class WhatsAppMessage
|
|
255
436
|
attr_reader :id, :channel, :message_format, :to, :from, :text, :status,
|
|
256
437
|
:segments, :credits_used, :whatsapp, :created_at, :metadata
|
|
@@ -289,7 +470,8 @@ module Sendly
|
|
|
289
470
|
end
|
|
290
471
|
|
|
291
472
|
# Connect numbers to WhatsApp. Starting a signup returns a +connect_url+
|
|
292
|
-
# a human must complete in a browser.
|
|
473
|
+
# a human must complete in a browser. A number added to an account that
|
|
474
|
+
# is already connected is verified with a code instead ({#verify}).
|
|
293
475
|
class WhatsAppSignupResource
|
|
294
476
|
def initialize(client)
|
|
295
477
|
@client = client
|
|
@@ -305,23 +487,179 @@ module Sendly
|
|
|
305
487
|
#
|
|
306
488
|
# Calling again for a number with an in-flight signup returns the
|
|
307
489
|
# existing signup (same +connect_url+) without charging again. Requires
|
|
308
|
-
# a live API key
|
|
490
|
+
# a live API key with the +whatsapp:write+ scope (a test key gets 403
|
|
491
|
+
# +whatsapp_requires_live_key+) and, in a team workspace, an owner or
|
|
492
|
+
# admin (+settings:write+). After the Facebook step the signup stays
|
|
493
|
+
# +"registering"+ while WhatsApp activates the number. Activation usually
|
|
494
|
+
# takes a few minutes but can take hours. If it hasn't finished about 6
|
|
495
|
+
# hours after the session began, the session fails with
|
|
496
|
+
# +registration_timeout+ and the fee is refunded. If the connection
|
|
497
|
+
# fails, the $19 fee is refunded automatically; once the number has
|
|
498
|
+
# connected there is no refund, and a later disconnect gets nothing
|
|
499
|
+
# back.
|
|
500
|
+
#
|
|
501
|
+
# To add a number to a WhatsApp Business Account this workspace has
|
|
502
|
+
# already connected, pass its +business_account_id+ (as shown on
|
|
503
|
+
# {WhatsAppSender#business_account_id}). There is no Facebook step and no
|
|
504
|
+
# +connect_url+: the same $19 fee is charged, Meta sends the number a
|
|
505
|
+
# 6-digit code by text or voice call, and the session is +"verifying"+
|
|
506
|
+
# until you submit the code with {#verify}. Calling again for a number
|
|
507
|
+
# that is verifying returns the same session without charging again or
|
|
508
|
+
# sending a second code; a verifying session older than 3 hours is
|
|
509
|
+
# expired (and refunded) and a new one is started.
|
|
309
510
|
#
|
|
310
511
|
# @param phone_number [String] The number to connect, in E.164 format.
|
|
311
512
|
# Must be an active number in your workspace.
|
|
513
|
+
# @param business_account_id [String, nil] The WhatsApp Business Account
|
|
514
|
+
# to add the number to. It must be connected in this workspace with at
|
|
515
|
+
# least one active number.
|
|
516
|
+
# @param verification_method [String, nil] How Meta sends the code:
|
|
517
|
+
# +"sms"+ (the default) or +"voice"+. Only with +business_account_id+.
|
|
518
|
+
# @param display_name [String, nil] The business name WhatsApp shows for
|
|
519
|
+
# the number (at most 512 characters). Defaults to the account's
|
|
520
|
+
# existing sender display name, then its business name. Only with
|
|
521
|
+
# +business_account_id+.
|
|
312
522
|
# @return [WhatsAppSignupSession]
|
|
523
|
+
# @raise [Sendly::ServerError] +whatsapp_unavailable+ (503) while
|
|
524
|
+
# WhatsApp connections are unavailable. Nothing is charged. Only
|
|
525
|
+
# signup returns it, with +retryAfter+ 3600 in the body
|
|
526
|
+
# (+e.response_body["retryAfter"]+) and a +Retry-After: 3600+ header.
|
|
527
|
+
# The client retries it like any 5xx before raising.
|
|
528
|
+
# @raise [Sendly::RateLimitError] +whatsapp_signup_limit_reached+ (429)
|
|
529
|
+
# after 5 failed, charged signups in 24 hours. Not retried; try again
|
|
530
|
+
# the next day.
|
|
531
|
+
# @raise [Sendly::NotFoundError] +whatsapp_business_account_not_found+
|
|
532
|
+
# (404) when +business_account_id+ isn't connected in this workspace
|
|
533
|
+
# with an active number.
|
|
534
|
+
# @raise [Sendly::ValidationError] +display_name_required+ (400) when no
|
|
535
|
+
# display name is given and the account has none to reuse, and
|
|
536
|
+
# +whatsapp_verification_start_failed+ (422) when WhatsApp refused to
|
|
537
|
+
# verify the number: final, the session failed and the fee is refunded.
|
|
538
|
+
# @raise [Sendly::APIError] 409 +whatsapp_signup_in_progress+ (a Facebook
|
|
539
|
+
# connection for the number is in flight; +e.response_body["id"]+ is
|
|
540
|
+
# that session), 409 +whatsapp_verification_in_progress+ (a Facebook
|
|
541
|
+
# signup for a number that is being added by code, with that session in
|
|
542
|
+
# +e.response_body["id"]+; or, with +business_account_id+, the number is
|
|
543
|
+
# still being added, so try again in a moment) or 409
|
|
544
|
+
# +whatsapp_already_enabled+.
|
|
545
|
+
# @raise [Sendly::InsufficientCreditsError] 402 +payment_method_required+
|
|
546
|
+
# or +payment_failed+ when the $19 fee can't be charged.
|
|
547
|
+
# @raise [Sendly::ServerError] +whatsapp_verification_start_failed+ (502)
|
|
548
|
+
# when WhatsApp couldn't start verifying the number: the session failed
|
|
549
|
+
# and its fee is refunded, so start again. With +business_account_id+,
|
|
550
|
+
# a 5xx is raised at once and never retried by the client, because
|
|
551
|
+
# each retry would start a new charged session (a timeout or dropped
|
|
552
|
+
# connection is never retried either). Without it the client retries a
|
|
553
|
+
# 5xx before raising, as before.
|
|
313
554
|
#
|
|
314
555
|
# @example
|
|
315
556
|
# signup = client.whatsapp.signup.create(phone_number: "+15559876543")
|
|
316
557
|
# puts "Open #{signup.connect_url} and log in with Facebook"
|
|
317
|
-
|
|
558
|
+
#
|
|
559
|
+
# @example Add a number to an account you already connected
|
|
560
|
+
# signup = client.whatsapp.signup.create(
|
|
561
|
+
# phone_number: "+14155550124",
|
|
562
|
+
# business_account_id: "102290129340398"
|
|
563
|
+
# )
|
|
564
|
+
# puts signup.status # "verifying"
|
|
565
|
+
def create(phone_number:, business_account_id: nil, verification_method: nil, display_name: nil)
|
|
318
566
|
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
567
|
+
if !business_account_id.nil? && business_account_id.to_s.strip.empty?
|
|
568
|
+
raise ValidationError, "business_account_id must be a non-empty string"
|
|
569
|
+
end
|
|
319
570
|
|
|
320
|
-
|
|
571
|
+
body = { phoneNumber: phone_number }
|
|
572
|
+
body[:businessAccountId] = business_account_id.to_s unless business_account_id.nil?
|
|
573
|
+
body[:verificationMethod] = verification_method unless verification_method.nil?
|
|
574
|
+
body[:displayName] = display_name unless display_name.nil?
|
|
575
|
+
|
|
576
|
+
by_code = !business_account_id.nil? && !business_account_id.to_s.empty?
|
|
577
|
+
response = @client.post("/whatsapp/signup", body, retry_server_errors: !by_code)
|
|
321
578
|
WhatsAppSignupSession.new(response)
|
|
322
579
|
end
|
|
323
580
|
|
|
324
|
-
#
|
|
581
|
+
# Submit the 6-digit code Meta sent to a number being added to a
|
|
582
|
+
# connected account (a +"verifying"+ signup). Spaces and dashes are
|
|
583
|
+
# ignored. On success the number is connected and the
|
|
584
|
+
# +whatsapp_account.connected+ webhook fires. A signup that is already
|
|
585
|
+
# +"active"+ comes back unchanged. The code can also be read from
|
|
586
|
+
# {WhatsAppSignup#verification_code} once Meta's text arrives on the
|
|
587
|
+
# number. Requires a live API key with the +whatsapp:write+ scope and, in
|
|
588
|
+
# a team workspace, an owner or admin (+settings:write+).
|
|
589
|
+
#
|
|
590
|
+
# @param id [String] The signup's id
|
|
591
|
+
# @param code [String] The 6-digit code
|
|
592
|
+
# @return [WhatsAppSignup] The signup, +"active"+ on success
|
|
593
|
+
# @raise [Sendly::ValidationError] +invalid_verification_code+ (400) when
|
|
594
|
+
# the code isn't 6 digits, and +whatsapp_verification_code_invalid+
|
|
595
|
+
# (422) for a wrong code, with the attempts left in
|
|
596
|
+
# +e.response_body["attemptsRemaining"]+
|
|
597
|
+
# @raise [Sendly::APIError] 409 +whatsapp_verification_failed+ after 5
|
|
598
|
+
# wrong codes (the session failed, the fee is refunded and
|
|
599
|
+
# +whatsapp_account.failed+ fires), 409 +whatsapp_verification_busy+
|
|
600
|
+
# while another code for the number is being checked (try again), or
|
|
601
|
+
# 409 +signup_not_active+ when the signup isn't waiting for a code or is
|
|
602
|
+
# older than 3 hours
|
|
603
|
+
# @raise [Sendly::NotFoundError] +signup_not_found+ (404)
|
|
604
|
+
# @raise [Sendly::ServerError] 502 +whatsapp_verification_unavailable+
|
|
605
|
+
# (WhatsApp couldn't check the code; the attempt isn't counted) or 502
|
|
606
|
+
# +whatsapp_activation_pending+ (the code was accepted but connecting
|
|
607
|
+
# the number didn't finish; check back shortly with {#get}). A 5xx, a
|
|
608
|
+
# timeout or a dropped connection is raised at once and never retried
|
|
609
|
+
# by the client: each submission
|
|
610
|
+
# uses one of the 5 attempts, and after +whatsapp_activation_pending+
|
|
611
|
+
# WhatsApp has already accepted the code.
|
|
612
|
+
#
|
|
613
|
+
# @example
|
|
614
|
+
# signup = client.whatsapp.signup.verify(signup.id, code: "482913")
|
|
615
|
+
# puts signup.status # "active"
|
|
616
|
+
def verify(id, code:)
|
|
617
|
+
raise ValidationError, "id is required" if id.nil? || id.to_s.empty?
|
|
618
|
+
raise ValidationError, "code is required" if code.nil? || code.to_s.empty?
|
|
619
|
+
|
|
620
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
621
|
+
response = @client.post("/whatsapp/signup/#{encoded_id}/verify", { code: code }, retry_server_errors: false)
|
|
622
|
+
WhatsAppSignup.new(response)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Ask Meta to send a new code to a number being added to a connected
|
|
626
|
+
# account. Codes can be requested at most every 30 seconds, counted from
|
|
627
|
+
# the signup's last change (a code submission included). A signup that
|
|
628
|
+
# is already +"active"+ comes back unchanged. Requires a live API key with
|
|
629
|
+
# the +whatsapp:write+ scope and, in a team workspace, an owner or admin
|
|
630
|
+
# (+settings:write+).
|
|
631
|
+
#
|
|
632
|
+
# @param id [String] The signup's id
|
|
633
|
+
# @param verification_method [String, nil] +"sms"+ (the default) or
|
|
634
|
+
# +"voice"+
|
|
635
|
+
# @return [WhatsAppSignup]
|
|
636
|
+
# @raise [Sendly::RateLimitError] +whatsapp_verification_resend_too_soon+
|
|
637
|
+
# (429), raised at once with the seconds to wait in +e.retry_after+
|
|
638
|
+
# @raise [Sendly::ValidationError] +whatsapp_verification_resend_failed+
|
|
639
|
+
# (422) when WhatsApp wouldn't send another code yet
|
|
640
|
+
# @raise [Sendly::APIError] 409 +signup_not_active+ when the signup isn't
|
|
641
|
+
# waiting for a code or is older than 3 hours
|
|
642
|
+
# @raise [Sendly::NotFoundError] +signup_not_found+ (404)
|
|
643
|
+
# @raise [Sendly::ServerError] +whatsapp_verification_resend_failed+
|
|
644
|
+
# (502) when WhatsApp couldn't be reached. The client retries a 5xx
|
|
645
|
+
# before raising.
|
|
646
|
+
#
|
|
647
|
+
# @example
|
|
648
|
+
# client.whatsapp.signup.resend(signup.id, verification_method: "voice")
|
|
649
|
+
def resend(id, verification_method: nil)
|
|
650
|
+
raise ValidationError, "id is required" if id.nil? || id.to_s.empty?
|
|
651
|
+
|
|
652
|
+
body = {}
|
|
653
|
+
body[:verificationMethod] = verification_method unless verification_method.nil?
|
|
654
|
+
|
|
655
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
656
|
+
response = @client.post("/whatsapp/signup/#{encoded_id}/resend", body)
|
|
657
|
+
WhatsAppSignup.new(response)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Get the status of a WhatsApp signup. Needs the +whatsapp:read+ scope;
|
|
661
|
+
# test keys work. While a number is +"verifying"+ the signup carries
|
|
662
|
+
# {WhatsAppSignup#verification_code} once Meta's text has arrived.
|
|
325
663
|
#
|
|
326
664
|
# @param id [String] The signup's id
|
|
327
665
|
# @return [WhatsAppSignup]
|
|
@@ -340,7 +678,8 @@ module Sendly
|
|
|
340
678
|
end
|
|
341
679
|
|
|
342
680
|
# List the numbers connected (or connecting) to WhatsApp, and manage
|
|
343
|
-
# their business profiles
|
|
681
|
+
# their business profiles, profile photos, conversational components and
|
|
682
|
+
# WhatsApp calling.
|
|
344
683
|
class WhatsAppSendersResource
|
|
345
684
|
def initialize(client)
|
|
346
685
|
@client = client
|
|
@@ -348,7 +687,8 @@ module Sendly
|
|
|
348
687
|
|
|
349
688
|
# List your WhatsApp senders, newest first. An empty list means no
|
|
350
689
|
# number is connected yet — start one with
|
|
351
|
-
# {WhatsAppSignupResource#create}.
|
|
690
|
+
# {WhatsAppSignupResource#create}. Needs the +whatsapp:read+ scope; test
|
|
691
|
+
# keys work.
|
|
352
692
|
#
|
|
353
693
|
# @return [Hash] +{ senders: Array<WhatsAppSender> }+
|
|
354
694
|
#
|
|
@@ -367,7 +707,8 @@ module Sendly
|
|
|
367
707
|
# The business profile is what recipients see when they open the
|
|
368
708
|
# sender's details in WhatsApp: display name, photo, category, about
|
|
369
709
|
# line, description, and contact details. The sender must be +"active"+
|
|
370
|
-
# (connected to WhatsApp).
|
|
710
|
+
# (connected to WhatsApp). Needs the +whatsapp:read+ scope; test keys
|
|
711
|
+
# work.
|
|
371
712
|
#
|
|
372
713
|
# @param phone_number [String] Your WhatsApp-connected sending number,
|
|
373
714
|
# in E.164 format
|
|
@@ -391,8 +732,10 @@ module Sendly
|
|
|
391
732
|
# Pass only the fields to change; omitted fields keep their current
|
|
392
733
|
# value. +about+ is limited to 139 characters and +description+ to 512.
|
|
393
734
|
# +display_name+ changes are reviewed by Meta before they take effect.
|
|
394
|
-
# The profile photo cannot be set through this method
|
|
395
|
-
#
|
|
735
|
+
# The profile photo cannot be set through this method; use
|
|
736
|
+
# {#upload_profile_photo}. Requires a live
|
|
737
|
+
# API key with the +whatsapp:write+ scope and, in a team workspace, an
|
|
738
|
+
# owner or admin (+settings:write+).
|
|
396
739
|
#
|
|
397
740
|
# @param phone_number [String] Your WhatsApp-connected sending number,
|
|
398
741
|
# in E.164 format
|
|
@@ -430,6 +773,166 @@ module Sendly
|
|
|
430
773
|
response = @client.patch("/whatsapp/senders/#{encoded_number}/profile", body)
|
|
431
774
|
WhatsAppSenderProfile.new(response)
|
|
432
775
|
end
|
|
776
|
+
|
|
777
|
+
# Upload a sender's WhatsApp profile photo, replacing the current one.
|
|
778
|
+
#
|
|
779
|
+
# The photo must be a JPEG or PNG (checked by its bytes, not by
|
|
780
|
+
# +content_type+) of at most 5 MB. WhatsApp wants it square and at least
|
|
781
|
+
# 192 pixels wide (640 recommended). Free. Requires a live API key with
|
|
782
|
+
# the +whatsapp:write+ scope and, in a team workspace, an owner or admin
|
|
783
|
+
# (+settings:write+).
|
|
784
|
+
#
|
|
785
|
+
# @param phone_number [String] Your WhatsApp-connected sending number,
|
|
786
|
+
# in E.164 format
|
|
787
|
+
# @param file [String, IO] A file path or an IO holding the image
|
|
788
|
+
# @param content_type [String] MIME type sent with the file
|
|
789
|
+
# @param filename [String] Name sent with the file
|
|
790
|
+
# @return [WhatsAppSenderProfile] The profile with its new photo
|
|
791
|
+
# @raise [Sendly::ValidationError] +whatsapp_profile_photo_invalid+ (400)
|
|
792
|
+
# when the file isn't a JPEG or PNG, or +file_required+ (400)
|
|
793
|
+
# @raise [Sendly::APIError] +whatsapp_profile_photo_too_large+ with
|
|
794
|
+
# +status_code+ 413 when the photo is over 5 MB
|
|
795
|
+
# @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
|
|
796
|
+
# @raise [Sendly::ServerError] +whatsapp_profile_update_failed+ (502)
|
|
797
|
+
# when WhatsApp refused the image or couldn't be reached. A 5xx, a
|
|
798
|
+
# timeout or a dropped connection is raised at once and never retried
|
|
799
|
+
# by the client; check the image is
|
|
800
|
+
# square and at least 192 pixels wide before trying again.
|
|
801
|
+
#
|
|
802
|
+
# @example
|
|
803
|
+
# client.whatsapp.senders.upload_profile_photo("+14155550123", "logo.png",
|
|
804
|
+
# content_type: "image/png", filename: "logo.png")
|
|
805
|
+
def upload_profile_photo(phone_number, file, content_type: "image/jpeg", filename: "profile.jpg")
|
|
806
|
+
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
807
|
+
raise ValidationError, "file is required" if file.nil?
|
|
808
|
+
|
|
809
|
+
encoded_number = URI.encode_www_form_component(phone_number)
|
|
810
|
+
response = @client.post_multipart("/whatsapp/senders/#{encoded_number}/profile/photo", file,
|
|
811
|
+
content_type: content_type, filename: filename,
|
|
812
|
+
retry_server_errors: false)
|
|
813
|
+
WhatsAppSenderProfile.new(response)
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# Remove a sender's WhatsApp profile photo. Requires a live API key with
|
|
817
|
+
# the +whatsapp:write+ scope and, in a team workspace, an owner or admin
|
|
818
|
+
# (+settings:write+).
|
|
819
|
+
#
|
|
820
|
+
# @param phone_number [String] Your WhatsApp-connected sending number,
|
|
821
|
+
# in E.164 format
|
|
822
|
+
# @return [WhatsAppSenderProfile] The profile, normally with
|
|
823
|
+
# +profile_photo_url+ nil
|
|
824
|
+
# @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
|
|
825
|
+
# @raise [Sendly::ServerError] +whatsapp_profile_update_failed+ (502).
|
|
826
|
+
# The client retries a 5xx before raising.
|
|
827
|
+
#
|
|
828
|
+
# @example
|
|
829
|
+
# client.whatsapp.senders.delete_profile_photo("+14155550123")
|
|
830
|
+
def delete_profile_photo(phone_number)
|
|
831
|
+
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
832
|
+
|
|
833
|
+
encoded_number = URI.encode_www_form_component(phone_number)
|
|
834
|
+
response = @client.delete("/whatsapp/senders/#{encoded_number}/profile/photo")
|
|
835
|
+
WhatsAppSenderProfile.new(response)
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Get a sender's conversational components: the ice breakers shown when
|
|
839
|
+
# someone opens a chat with the business for the first time, and the
|
|
840
|
+
# commands shown when the customer types "/". Needs the +whatsapp:read+
|
|
841
|
+
# scope; test keys work.
|
|
842
|
+
#
|
|
843
|
+
# @param phone_number [String] Your WhatsApp-connected sending number,
|
|
844
|
+
# in E.164 format
|
|
845
|
+
# @return [WhatsAppConversationalComponents]
|
|
846
|
+
# @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
|
|
847
|
+
# @raise [Sendly::ServerError]
|
|
848
|
+
# +whatsapp_conversational_components_fetch_failed+ (502). The client
|
|
849
|
+
# retries a 5xx before raising.
|
|
850
|
+
#
|
|
851
|
+
# @example
|
|
852
|
+
# components = client.whatsapp.senders.get_conversational_components("+14155550123")
|
|
853
|
+
# components.commands.each { |c| puts "/#{c.command}: #{c.description}" }
|
|
854
|
+
def get_conversational_components(phone_number)
|
|
855
|
+
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
856
|
+
|
|
857
|
+
encoded_number = URI.encode_www_form_component(phone_number)
|
|
858
|
+
response = @client.get("/whatsapp/senders/#{encoded_number}/conversational_components")
|
|
859
|
+
WhatsAppConversationalComponents.new(response)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Replace a sender's ice breakers, commands, or both.
|
|
863
|
+
#
|
|
864
|
+
# Each list you pass replaces the stored one, and +[]+ clears it; a list
|
|
865
|
+
# you leave out is kept. Ice breakers: at most 4, each 1-80 characters,
|
|
866
|
+
# no duplicates. Commands: at most 30, each a hash with +:command+
|
|
867
|
+
# (letters, digits and underscores, 1-32 characters; a leading "/" is
|
|
868
|
+
# dropped) and +:description+ (1-256 characters), no duplicate commands.
|
|
869
|
+
# The {WhatsAppCommand} objects a read returns can be passed back. Free.
|
|
870
|
+
# Requires a live API key with the +whatsapp:write+ scope and, in a team
|
|
871
|
+
# workspace, an owner or admin (+settings:write+).
|
|
872
|
+
#
|
|
873
|
+
# @param phone_number [String] Your WhatsApp-connected sending number,
|
|
874
|
+
# in E.164 format
|
|
875
|
+
# @param ice_breakers [Array<String>, nil] The new ice breakers
|
|
876
|
+
# @param commands [Array<Hash, WhatsAppCommand>, nil] The new commands
|
|
877
|
+
# @return [WhatsAppConversationalComponents] The stored components
|
|
878
|
+
# @raise [Sendly::ValidationError] If neither list is given, or
|
|
879
|
+
# +invalid_request+ (400) with a message naming the problem
|
|
880
|
+
# @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
|
|
881
|
+
# @raise [Sendly::ServerError]
|
|
882
|
+
# +whatsapp_conversational_components_update_failed+ (502). The client
|
|
883
|
+
# retries a 5xx before raising.
|
|
884
|
+
#
|
|
885
|
+
# @example
|
|
886
|
+
# client.whatsapp.senders.update_conversational_components("+14155550123",
|
|
887
|
+
# ice_breakers: ["Track my order", "Opening hours"],
|
|
888
|
+
# commands: [{ command: "menu", description: "See today's menu" }]
|
|
889
|
+
# )
|
|
890
|
+
def update_conversational_components(phone_number, ice_breakers: nil, commands: nil)
|
|
891
|
+
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
892
|
+
|
|
893
|
+
body = {}
|
|
894
|
+
body[:iceBreakers] = ice_breakers unless ice_breakers.nil?
|
|
895
|
+
body[:commands] = commands.map { |c| c.is_a?(WhatsAppCommand) ? c.to_h : c } unless commands.nil?
|
|
896
|
+
raise ValidationError, "Provide ice_breakers, commands, or both" if body.empty?
|
|
897
|
+
|
|
898
|
+
encoded_number = URI.encode_www_form_component(phone_number)
|
|
899
|
+
response = @client.patch("/whatsapp/senders/#{encoded_number}/conversational_components", body)
|
|
900
|
+
WhatsAppConversationalComponents.new(response)
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# Switch WhatsApp calling on or off for a sender. Free.
|
|
904
|
+
#
|
|
905
|
+
# Once it is on, a WhatsApp user calling the number rings exactly like
|
|
906
|
+
# a phone call (in the dashboard or with your AI agent, per the number's
|
|
907
|
+
# voice settings), billed at the normal inbound rate. Turning it on
|
|
908
|
+
# needs voice switched on for the number first. There is no API for
|
|
909
|
+
# placing WhatsApp calls. Requires a live API key with the
|
|
910
|
+
# +whatsapp:write+ scope and, in a team workspace, an owner or admin
|
|
911
|
+
# (+settings:write+).
|
|
912
|
+
#
|
|
913
|
+
# @param phone_number [String] Your WhatsApp-connected sending number,
|
|
914
|
+
# in E.164 format
|
|
915
|
+
# @param enabled [Boolean] true to switch calling on, false to switch it off
|
|
916
|
+
# @return [WhatsAppCallingSettings]
|
|
917
|
+
# @raise [Sendly::APIError] +voice_not_enabled+ with +status_code+ 409
|
|
918
|
+
# when voice isn't on for the number
|
|
919
|
+
# @raise [Sendly::ValidationError] +whatsapp_calling_unavailable+ (422)
|
|
920
|
+
# when Meta refused: calling needs the account at the 2,000
|
|
921
|
+
# recipients a day messaging limit and an approved display name
|
|
922
|
+
# @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
|
|
923
|
+
# @raise [Sendly::ServerError] +whatsapp_calling_update_failed+ (502).
|
|
924
|
+
# The client retries a 5xx before raising.
|
|
925
|
+
#
|
|
926
|
+
# @example
|
|
927
|
+
# settings = client.whatsapp.senders.set_calling("+14155550123", enabled: true)
|
|
928
|
+
# puts settings.calling_enabled?
|
|
929
|
+
def set_calling(phone_number, enabled:)
|
|
930
|
+
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
931
|
+
|
|
932
|
+
encoded_number = URI.encode_www_form_component(phone_number)
|
|
933
|
+
response = @client.patch("/whatsapp/senders/#{encoded_number}/calling", { enabled: enabled })
|
|
934
|
+
WhatsAppCallingSettings.new(response)
|
|
935
|
+
end
|
|
433
936
|
end
|
|
434
937
|
|
|
435
938
|
# Manage Meta-reviewed message templates.
|
|
@@ -438,7 +941,8 @@ module Sendly
|
|
|
438
941
|
@client = client
|
|
439
942
|
end
|
|
440
943
|
|
|
441
|
-
# List your WhatsApp templates.
|
|
944
|
+
# List your WhatsApp templates. Needs the +whatsapp:read+ scope; test
|
|
945
|
+
# keys work.
|
|
442
946
|
#
|
|
443
947
|
# @return [Hash] +{ templates: Array<WhatsAppTemplate> }+
|
|
444
948
|
#
|
|
@@ -455,18 +959,24 @@ module Sendly
|
|
|
455
959
|
# Create a template and submit it to Meta for review.
|
|
456
960
|
#
|
|
457
961
|
# Review usually takes 24-48h; the template is usable once its status
|
|
458
|
-
# is +"APPROVED"+. Requires a live API key
|
|
962
|
+
# is +"APPROVED"+. Requires a live API key with the +whatsapp:write+
|
|
963
|
+
# scope and, in a team workspace, an owner, admin or member
|
|
964
|
+
# (+templates:write+). A marketing template without an opt-out button is
|
|
965
|
+
# still accepted, with a warning.
|
|
459
966
|
#
|
|
460
967
|
# @param sender [String] The WhatsApp-connected sending number this
|
|
461
968
|
# template belongs to, in E.164 format
|
|
462
969
|
# @param name [String] Template name: lowercase letters, digits, and
|
|
463
970
|
# underscores (e.g. "order_shipped")
|
|
464
971
|
# @param language [String] Template language code (e.g. "en_US")
|
|
465
|
-
# @param category [String] "AUTHENTICATION", "UTILITY", or "MARKETING"
|
|
466
|
-
#
|
|
972
|
+
# @param category [String] "AUTHENTICATION", "UTILITY", or "MARKETING"
|
|
973
|
+
# (the server uppercases it). Required, with no default; it drives Meta
|
|
974
|
+
# review rules and pricing and can't be changed later
|
|
467
975
|
# @param body [String] Body text. Use {{1}}, {{2}}, ... for variables;
|
|
468
976
|
# every placeholder needs an example value in +examples+
|
|
469
|
-
# @param header [String, nil] Optional text header
|
|
977
|
+
# @param header [String, nil] Optional text header. Fixed text only: a
|
|
978
|
+
# header containing {{n}} is refused with
|
|
979
|
+
# +template_header_variable_unsupported+
|
|
470
980
|
# @param footer [String, nil] Optional footer line
|
|
471
981
|
# @param buttons [Array<Hash>, nil] Optional buttons, each with :type
|
|
472
982
|
# ("url", "quick_reply", or "otp"), :text, and for url buttons a :url
|
|
@@ -476,7 +986,15 @@ module Sendly
|
|
|
476
986
|
# Required when the body has variables.
|
|
477
987
|
# @return [WhatsAppTemplate] The created template (status "PENDING"),
|
|
478
988
|
# with any submission warnings
|
|
479
|
-
# @raise [Sendly::NotFoundError]
|
|
989
|
+
# @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404) if
|
|
990
|
+
# the sender isn't connected to WhatsApp; this is checked first
|
|
991
|
+
# @raise [Sendly::ValidationError] If the API refuses the template with a
|
|
992
|
+
# 400 +template_*+ code (in +e.response_body["error"]+) and a readable
|
|
993
|
+
# message: +template_category_invalid+ (category missing or not one of
|
|
994
|
+
# the three), +template_authentication_otp_button_required+,
|
|
995
|
+
# +template_authentication_no_links+ (a link in the body or a URL
|
|
996
|
+
# button on an authentication template) or
|
|
997
|
+
# +template_header_variable_unsupported+
|
|
480
998
|
#
|
|
481
999
|
# @example
|
|
482
1000
|
# template = client.whatsapp.templates.create(
|
|
@@ -517,11 +1035,14 @@ module Sendly
|
|
|
517
1035
|
# This is the recovery path for rejections: template names are locked
|
|
518
1036
|
# for ~30 days after deletion, so editing a rejected template (rather
|
|
519
1037
|
# than deleting and re-creating it) is the way to fix it. The updated
|
|
520
|
-
# template goes back to "PENDING" review.
|
|
1038
|
+
# template goes back to "PENDING" review. The category can't be
|
|
1039
|
+
# changed. Requires a live API key with the +whatsapp:write+ scope and,
|
|
1040
|
+
# in a team workspace, an owner, admin or member (+templates:write+).
|
|
521
1041
|
#
|
|
522
1042
|
# @param id [String] The template's id
|
|
523
1043
|
# @param body [String, nil] Replacement body text
|
|
524
|
-
# @param header [String, nil] Replacement text header
|
|
1044
|
+
# @param header [String, nil] Replacement text header; it can't contain
|
|
1045
|
+
# {{n}} variables (+template_header_variable_unsupported+)
|
|
525
1046
|
# @param footer [String, nil] Replacement footer
|
|
526
1047
|
# @param buttons [Array<Hash>, nil] Replacement buttons
|
|
527
1048
|
# @param examples [Hash, nil] Replacement example values for body placeholders
|
|
@@ -552,7 +1073,9 @@ module Sendly
|
|
|
552
1073
|
#
|
|
553
1074
|
# Meta locks a deleted template's name for ~30 days — re-creating it
|
|
554
1075
|
# fails with +template_name_locked+ until the lock lifts. To fix a
|
|
555
|
-
# rejected template, prefer {#update}. Requires a live API key
|
|
1076
|
+
# rejected template, prefer {#update}. Requires a live API key with the
|
|
1077
|
+
# +whatsapp:write+ scope and, in a team workspace, an owner, admin or
|
|
1078
|
+
# member (+templates:write+).
|
|
556
1079
|
#
|
|
557
1080
|
# @param id [String] The template's id
|
|
558
1081
|
# @return [WhatsAppTemplateDeletion]
|
|
@@ -576,10 +1099,12 @@ module Sendly
|
|
|
576
1099
|
# create Meta-reviewed message templates, and send via
|
|
577
1100
|
# +client.messages.send(channel: "whatsapp", ...)+.
|
|
578
1101
|
#
|
|
579
|
-
# Connecting a number is a one-time $19 setup (no monthly fee)
|
|
580
|
-
# ends with a human step: {WhatsAppSignupResource#create}
|
|
581
|
-
# +connect_url+ that a person must open in a browser and log in
|
|
582
|
-
# Facebook to link their WhatsApp Business Account.
|
|
1102
|
+
# Connecting a number is a one-time $19 setup (no monthly fee). The first
|
|
1103
|
+
# number always ends with a human step: {WhatsAppSignupResource#create}
|
|
1104
|
+
# returns a +connect_url+ that a person must open in a browser and log in
|
|
1105
|
+
# with Facebook to link their WhatsApp Business Account. More numbers can
|
|
1106
|
+
# then be added to that account with a code Meta sends to the number
|
|
1107
|
+
# (+business_account_id:+ on create, then {WhatsAppSignupResource#verify}).
|
|
583
1108
|
#
|
|
584
1109
|
# Two ways to reach a recipient:
|
|
585
1110
|
#
|
|
@@ -590,6 +1115,31 @@ module Sendly
|
|
|
590
1115
|
# marketing — pricing follows the category and destination country.
|
|
591
1116
|
# Note: Meta has paused marketing template delivery to US (+1) numbers.
|
|
592
1117
|
#
|
|
1118
|
+
# Pricing: free-form text or media inside the 24-hour window costs 1
|
|
1119
|
+
# credit each for the first 1,000 per sending number per calendar month
|
|
1120
|
+
# (UTC), then the destination's utility template price; countries without
|
|
1121
|
+
# a listed price use the default utility price of 12 credits. Templates
|
|
1122
|
+
# are priced by category and destination country; countries without a
|
|
1123
|
+
# listed price use 33 (marketing), 12 (utility) and 12 (authentication)
|
|
1124
|
+
# credits. A failed send gives its slot back.
|
|
1125
|
+
#
|
|
1126
|
+
# Scopes and keys: sends go through +messages.send(channel: "whatsapp")+
|
|
1127
|
+
# and need +sms:send+, not +whatsapp:write+, and a live key. Reads (signup
|
|
1128
|
+
# status, templates, the window, senders and sender profiles) need
|
|
1129
|
+
# +whatsapp:read+ and accept test keys. Signup, template create/edit/delete
|
|
1130
|
+
# and profile edits need +whatsapp:write+ and a live key (otherwise 403
|
|
1131
|
+
# +whatsapp_requires_live_key+). In a team workspace, connecting and
|
|
1132
|
+
# profile edits need an owner or admin (+settings:write+), and template
|
|
1133
|
+
# writes need an owner, admin or member (+templates:write+); a missing
|
|
1134
|
+
# role returns 403 +insufficient_permissions+. Reading conversational
|
|
1135
|
+
# components needs +whatsapp:read+ too and accepts test keys. The profile
|
|
1136
|
+
# photo, conversational component and calling changes, and submitting or
|
|
1137
|
+
# resending a verification code, need the same as a profile edit.
|
|
1138
|
+
#
|
|
1139
|
+
# WhatsApp is enabled per person: the user who owns the API key, not the
|
|
1140
|
+
# workspace. While it is off, sends return 403 +whatsapp_not_enabled+ and
|
|
1141
|
+
# every method on this resource gets 404 +not_found+.
|
|
1142
|
+
#
|
|
593
1143
|
# @example Connect a number, create a template, then send
|
|
594
1144
|
# # 1. Connect ($19 one-time; a human must open the connect URL)
|
|
595
1145
|
# signup = client.whatsapp.signup.create(phone_number: "+15559876543")
|
|
@@ -615,7 +1165,7 @@ module Sendly
|
|
|
615
1165
|
attr_reader :signup
|
|
616
1166
|
|
|
617
1167
|
# @return [WhatsAppSendersResource] List connected numbers and manage
|
|
618
|
-
# their business profiles
|
|
1168
|
+
# their business profiles, photos, conversational components and calling
|
|
619
1169
|
attr_reader :senders
|
|
620
1170
|
|
|
621
1171
|
# @return [WhatsAppTemplatesResource] Manage Meta-reviewed templates
|
|
@@ -633,7 +1183,8 @@ module Sendly
|
|
|
633
1183
|
#
|
|
634
1184
|
# Free-form text and media only deliver while a window is open (it
|
|
635
1185
|
# opens when the recipient messages you and lasts 24h from their last
|
|
636
|
-
# inbound message). Outside a window, send an approved template.
|
|
1186
|
+
# inbound message). Outside a window, send an approved template. Needs
|
|
1187
|
+
# the +whatsapp:read+ scope; test keys work.
|
|
637
1188
|
#
|
|
638
1189
|
# @param from [String] Your WhatsApp-connected sending number, in E.164 format
|
|
639
1190
|
# @param to [String] The recipient's number, in E.164 format
|