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
data/lib/sendly/messages.rb
CHANGED
|
@@ -13,8 +13,11 @@ module Sendly
|
|
|
13
13
|
# Send an SMS, WhatsApp, or RCS message
|
|
14
14
|
#
|
|
15
15
|
# Pass +channel: "whatsapp"+ to send on WhatsApp. WhatsApp sends require
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# the +sms:send+ scope (not +whatsapp:write+), a live API key and a
|
|
17
|
+
# +from+ number with an active WhatsApp connection (see
|
|
18
|
+
# +client.whatsapp.signup+). WhatsApp is enabled per person (the user who
|
|
19
|
+
# owns the API key, not the workspace); while it is off the API responds
|
|
20
|
+
# 403 +whatsapp_not_enabled+. Provide exactly one of +text+
|
|
18
21
|
# (free-form, max 4096 bytes), +media_urls+ (a single attachment;
|
|
19
22
|
# optional +text+ becomes its caption, max 1024 bytes), or +template+
|
|
20
23
|
# (an approved template). Free-form text and media only deliver inside
|
|
@@ -71,7 +74,22 @@ module Sendly
|
|
|
71
74
|
# response instead of executing again.
|
|
72
75
|
# @return [Sendly::Message, Sendly::WhatsAppMessage, Sendly::RcsMessage] The sent message
|
|
73
76
|
#
|
|
74
|
-
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
77
|
+
# @raise [Sendly::ValidationError] If parameters are invalid, or on
|
|
78
|
+
# WhatsApp the 422 +whatsapp_send_failed+ when WhatsApp refused the
|
|
79
|
+
# message (final, not retried, not charged; cached under the
|
|
80
|
+
# idempotency key and replayed for 24 hours)
|
|
81
|
+
# @raise [Sendly::ServerError] On WhatsApp, the 502 +whatsapp_send_failed+:
|
|
82
|
+
# the message provably never reached the carrier, so it was not sent
|
|
83
|
+
# and is safe to send again. It isn't charged and is never cached, so
|
|
84
|
+
# the client retries it like any 5xx first under the same idempotency
|
|
85
|
+
# key. No send returns 503 +whatsapp_unavailable+.
|
|
86
|
+
# @raise [Sendly::APIError] On WhatsApp, the 409
|
|
87
|
+
# +whatsapp_send_unconfirmed+ (+status_code+ 409): the outcome is
|
|
88
|
+
# unknown. The message was marked failed and refunded but may still be
|
|
89
|
+
# delivered, so check before sending it again (it could arrive twice).
|
|
90
|
+
# It is not retried automatically, and it is cached under the
|
|
91
|
+
# idempotency key, so repeating the request with the same key returns
|
|
92
|
+
# this answer again.
|
|
75
93
|
# @raise [Sendly::InsufficientCreditsError] If account has no credits
|
|
76
94
|
# @raise [Sendly::RateLimitError] If rate limit is exceeded
|
|
77
95
|
#
|
|
@@ -327,7 +345,7 @@ module Sendly
|
|
|
327
345
|
#
|
|
328
346
|
# @param status [String] Filter by status
|
|
329
347
|
# @param to [String] Filter by recipient
|
|
330
|
-
# @param batch_size [Integer] Number of messages per request
|
|
348
|
+
# @param batch_size [Integer] Number of messages per request (the API returns at most 100)
|
|
331
349
|
# @yield [Message] Each message
|
|
332
350
|
# @return [Enumerator] If no block given
|
|
333
351
|
#
|
|
@@ -343,9 +361,9 @@ module Sendly
|
|
|
343
361
|
page = list(limit: batch_size, offset: offset, status: status, to: to)
|
|
344
362
|
page.each(&block)
|
|
345
363
|
|
|
346
|
-
break
|
|
364
|
+
break if !page.has_more || page.count.zero?
|
|
347
365
|
|
|
348
|
-
offset +=
|
|
366
|
+
offset += page.count
|
|
349
367
|
end
|
|
350
368
|
end
|
|
351
369
|
|
|
@@ -446,7 +464,9 @@ module Sendly
|
|
|
446
464
|
# @param metadata [Hash] Shared metadata for all messages (max 4KB). Each message can also have its own metadata hash which takes priority.
|
|
447
465
|
# @param idempotency_key [String] Idempotency key for this operation
|
|
448
466
|
# (1-255 printable ASCII characters, optional)
|
|
449
|
-
# @return [Hash] Batch
|
|
467
|
+
# @return [Hash] Batch result: +batchId+, +status+ (processing, completed,
|
|
468
|
+
# partial_failure or failed), +total+, +sent+, +failed+, +creditsUsed+,
|
|
469
|
+
# +creditsRefunded+ and +messages+
|
|
450
470
|
#
|
|
451
471
|
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
452
472
|
# @raise [Sendly::InsufficientCreditsError] If account has insufficient credits
|
|
@@ -458,7 +478,7 @@ module Sendly
|
|
|
458
478
|
# { to: "+15559876543", text: "Hello Bob!" }
|
|
459
479
|
# ]
|
|
460
480
|
# )
|
|
461
|
-
# puts "Batch #{result['batchId']}: #{result['
|
|
481
|
+
# puts "Batch #{result['batchId']}: #{result['total']} messages, #{result['status']}"
|
|
462
482
|
def send_batch(messages:, from: nil, message_type: nil, metadata: nil, idempotency_key: nil)
|
|
463
483
|
raise ValidationError, "Messages array is required" if messages.nil? || messages.empty?
|
|
464
484
|
|
|
@@ -505,12 +525,12 @@ module Sendly
|
|
|
505
525
|
#
|
|
506
526
|
# @param limit [Integer] Maximum batches to return (default: 20, max: 100)
|
|
507
527
|
# @param offset [Integer] Number of batches to skip
|
|
508
|
-
# @param status [String] Filter by status (processing, completed, failed)
|
|
528
|
+
# @param status [String] Filter by status (processing, completed, partial_failure, failed)
|
|
509
529
|
# @return [Hash] Paginated list of batches
|
|
510
530
|
#
|
|
511
531
|
# @example
|
|
512
532
|
# batches = client.messages.list_batches(limit: 10)
|
|
513
|
-
# batches["data"].each { |b| puts "#{b['
|
|
533
|
+
# batches["data"].each { |b| puts "#{b['id']}: #{b['status']}" }
|
|
514
534
|
def list_batches(limit: 20, offset: 0, status: nil)
|
|
515
535
|
params = {
|
|
516
536
|
limit: [limit, 100].min,
|
|
@@ -537,7 +557,7 @@ module Sendly
|
|
|
537
557
|
# { to: "+15559876543", text: "Hello Bob!" }
|
|
538
558
|
# ]
|
|
539
559
|
# )
|
|
540
|
-
# puts "
|
|
560
|
+
# puts "Sufficient credits: #{preview['hasSufficientCredits']}"
|
|
541
561
|
# puts "Credits needed: #{preview['creditsNeeded']}"
|
|
542
562
|
def preview_batch(messages:, from: nil, message_type: nil)
|
|
543
563
|
raise ValidationError, "Messages array is required" if messages.nil? || messages.empty?
|
data/lib/sendly/types.rb
CHANGED
|
@@ -63,6 +63,12 @@ module Sendly
|
|
|
63
63
|
# @return [Hash, nil] AI classification metadata for inbound messages
|
|
64
64
|
attr_reader :ai_metadata
|
|
65
65
|
|
|
66
|
+
# @return [Array<String>] Media attached to the message; empty when there is none
|
|
67
|
+
attr_reader :media_urls
|
|
68
|
+
|
|
69
|
+
# @return [String] "sms", "mms", "rcs" or "whatsapp"; "sms" when the response does not say
|
|
70
|
+
attr_reader :message_format
|
|
71
|
+
|
|
66
72
|
# Message status constants (sending removed - doesn't exist in database)
|
|
67
73
|
STATUSES = %w[queued sent delivered failed bounced retrying].freeze
|
|
68
74
|
|
|
@@ -90,6 +96,8 @@ module Sendly
|
|
|
90
96
|
@retry_count = data["retryCount"] || 0
|
|
91
97
|
@metadata = data["metadata"]
|
|
92
98
|
@ai_metadata = data["aiMetadata"]
|
|
99
|
+
@media_urls = data["mediaUrls"] || data["media_urls"] || []
|
|
100
|
+
@message_format = data["messageFormat"] || data["message_format"] || "sms"
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
# Check if message was delivered
|
|
@@ -133,7 +141,9 @@ module Sendly
|
|
|
133
141
|
error_code: error_code,
|
|
134
142
|
retry_count: retry_count,
|
|
135
143
|
metadata: metadata,
|
|
136
|
-
ai_metadata: ai_metadata
|
|
144
|
+
ai_metadata: ai_metadata,
|
|
145
|
+
media_urls: media_urls,
|
|
146
|
+
message_format: message_format
|
|
137
147
|
}.compact
|
|
138
148
|
end
|
|
139
149
|
|
|
@@ -155,7 +165,7 @@ module Sendly
|
|
|
155
165
|
# @return [Array<Message>] Messages in this page
|
|
156
166
|
attr_reader :data
|
|
157
167
|
|
|
158
|
-
# @return [Integer] Total number of messages
|
|
168
|
+
# @return [Integer] Total number of messages that match the query, across all pages
|
|
159
169
|
attr_reader :total
|
|
160
170
|
|
|
161
171
|
# @return [Integer] Current limit
|
|
@@ -168,11 +178,16 @@ module Sendly
|
|
|
168
178
|
attr_reader :has_more
|
|
169
179
|
|
|
170
180
|
def initialize(response)
|
|
181
|
+
pagination = response["pagination"] || {}
|
|
171
182
|
@data = (response["data"] || []).map { |m| Message.new(m) }
|
|
172
|
-
@total = response["count"] || @data.length
|
|
173
|
-
@limit = response["limit"] || 20
|
|
174
|
-
@offset = response["offset"] || 0
|
|
175
|
-
@has_more =
|
|
183
|
+
@total = pagination["total"] || response["total"] || response["count"] || @data.length
|
|
184
|
+
@limit = pagination["limit"] || response["limit"] || 20
|
|
185
|
+
@offset = pagination["offset"] || response["offset"] || 0
|
|
186
|
+
@has_more = if pagination.key?("hasMore")
|
|
187
|
+
pagination["hasMore"]
|
|
188
|
+
else
|
|
189
|
+
(@offset + @data.length) < @total
|
|
190
|
+
end
|
|
176
191
|
end
|
|
177
192
|
|
|
178
193
|
# Iterate over messages
|
|
@@ -433,14 +448,31 @@ module Sendly
|
|
|
433
448
|
end
|
|
434
449
|
|
|
435
450
|
# Result of testing a webhook
|
|
451
|
+
#
|
|
452
|
+
# Only a delivered test comes back as a result: when the test delivery
|
|
453
|
+
# fails, {Sendly::WebhooksResource#test} raises {Sendly::ValidationError}
|
|
454
|
+
# with the API's message.
|
|
436
455
|
class WebhookTestResult
|
|
437
456
|
attr_reader :success, :status_code, :response_time_ms, :error
|
|
438
457
|
|
|
458
|
+
# @return [String, nil] The API's summary of the test delivery
|
|
459
|
+
attr_reader :message
|
|
460
|
+
|
|
461
|
+
# @return [String, nil] ID of the test delivery
|
|
462
|
+
attr_reader :delivery_id
|
|
463
|
+
|
|
464
|
+
# @return [Hash] The raw parsed response
|
|
465
|
+
attr_reader :raw
|
|
466
|
+
|
|
439
467
|
def initialize(data)
|
|
468
|
+
delivery = data["delivery"] || {}
|
|
469
|
+
@raw = data
|
|
440
470
|
@success = data["success"]
|
|
441
|
-
@status_code = data["status_code"] || data["statusCode"]
|
|
442
|
-
@response_time_ms = data["response_time_ms"] || data["responseTimeMs"]
|
|
443
|
-
@error = data["error"]
|
|
471
|
+
@status_code = data["status_code"] || data["statusCode"] || delivery["status_code"]
|
|
472
|
+
@response_time_ms = data["response_time_ms"] || data["responseTimeMs"] || delivery["response_time"]
|
|
473
|
+
@error = data["error"] || delivery["error"]
|
|
474
|
+
@message = data["message"]
|
|
475
|
+
@delivery_id = delivery["id"] || delivery["delivery_id"]
|
|
444
476
|
end
|
|
445
477
|
|
|
446
478
|
def success?
|
|
@@ -449,14 +481,49 @@ module Sendly
|
|
|
449
481
|
end
|
|
450
482
|
|
|
451
483
|
# Result of rotating webhook secret
|
|
484
|
+
#
|
|
485
|
+
# The API signs deliveries with the new secret from the moment the rotation
|
|
486
|
+
# returns and does not keep the old one, so {#new_secret} is the only secret
|
|
487
|
+
# that verifies deliveries from then on.
|
|
452
488
|
class WebhookSecretRotation
|
|
453
|
-
|
|
489
|
+
# @return [Sendly::Webhook, nil] Always +nil+: the rotation response does not include the webhook
|
|
490
|
+
attr_reader :webhook
|
|
491
|
+
|
|
492
|
+
# @return [String] The new signing secret. It is shown only once.
|
|
493
|
+
attr_reader :new_secret
|
|
494
|
+
|
|
495
|
+
# @return [Time, nil] Always +nil+: the API does not keep the old secret, so it has no expiry
|
|
496
|
+
attr_reader :old_secret_expires_at
|
|
497
|
+
|
|
498
|
+
# @return [String, nil] Confirmation message
|
|
499
|
+
attr_reader :message
|
|
500
|
+
|
|
501
|
+
# @return [String, nil] The webhook's ID
|
|
502
|
+
attr_reader :id
|
|
503
|
+
|
|
504
|
+
# @return [Integer, nil] The webhook's secret version as the API reports it
|
|
505
|
+
attr_reader :new_secret_version
|
|
506
|
+
|
|
507
|
+
# @return [Integer, nil] The grace period the API reports (24). The old
|
|
508
|
+
# secret is not kept, so it does not verify deliveries during it.
|
|
509
|
+
attr_reader :grace_period_hours
|
|
510
|
+
|
|
511
|
+
# @return [Time, nil] When the secret was rotated
|
|
512
|
+
attr_reader :rotated_at
|
|
513
|
+
|
|
514
|
+
# @return [Hash] The raw parsed response
|
|
515
|
+
attr_reader :raw
|
|
454
516
|
|
|
455
517
|
def initialize(data)
|
|
456
|
-
@
|
|
457
|
-
@
|
|
518
|
+
@raw = data
|
|
519
|
+
@webhook = data["webhook"] ? Webhook.new(data["webhook"]) : nil
|
|
520
|
+
@new_secret = data["new_secret"] || data["newSecret"] || data["secret"]
|
|
458
521
|
@old_secret_expires_at = parse_time(data["old_secret_expires_at"] || data["oldSecretExpiresAt"])
|
|
459
522
|
@message = data["message"]
|
|
523
|
+
@id = data["id"]
|
|
524
|
+
@new_secret_version = data["new_secret_version"] || data["newSecretVersion"]
|
|
525
|
+
@grace_period_hours = data["grace_period_hours"] || data["gracePeriodHours"]
|
|
526
|
+
@rotated_at = parse_time(data["rotated_at"] || data["rotatedAt"])
|
|
460
527
|
end
|
|
461
528
|
|
|
462
529
|
private
|
|
@@ -475,13 +542,56 @@ module Sendly
|
|
|
475
542
|
|
|
476
543
|
# Represents account information
|
|
477
544
|
class Account
|
|
478
|
-
|
|
545
|
+
# @return [String, nil] The user's ID
|
|
546
|
+
attr_reader :id
|
|
547
|
+
|
|
548
|
+
# @return [String, nil] The user's email address
|
|
549
|
+
attr_reader :email
|
|
550
|
+
|
|
551
|
+
# @return [String, nil] The name of the workspace the API key belongs to,
|
|
552
|
+
# or +nil+ when the key is not bound to a workspace
|
|
553
|
+
attr_reader :name
|
|
554
|
+
|
|
555
|
+
# @return [Time, nil] When the user signed up
|
|
556
|
+
attr_reader :created_at
|
|
557
|
+
|
|
558
|
+
# @return [Hash, nil] The workspace the API key belongs to
|
|
559
|
+
# (+"id"+, +"name"+, +"isPersonal"+), or +nil+
|
|
560
|
+
attr_reader :organization
|
|
561
|
+
|
|
562
|
+
# @return [String, nil] The workspace's ID, the +organization_id+ in webhook payloads
|
|
563
|
+
attr_reader :organization_id
|
|
564
|
+
|
|
565
|
+
# @return [Hash, nil] Credit balances (+"balance"+, +"reservedBalance"+)
|
|
566
|
+
attr_reader :credits
|
|
567
|
+
|
|
568
|
+
# @return [Hash, nil] Business verification (+"status"+, +"type"+, +"region"+,
|
|
569
|
+
# +"submittedAt"+, +"updatedAt"+), or +nil+ when there is none
|
|
570
|
+
attr_reader :verification
|
|
571
|
+
|
|
572
|
+
# @return [Hash, nil] The API key making the call (+"id"+, +"name"+, +"type"+,
|
|
573
|
+
# +"scopes"+, +"createdAt"+, +"lastUsedAt"+)
|
|
574
|
+
attr_reader :api_key
|
|
575
|
+
|
|
576
|
+
# @return [Hash, nil] Sending limits (+"messagesPerMinute"+, +"messagesPerDay"+)
|
|
577
|
+
attr_reader :limits
|
|
578
|
+
|
|
579
|
+
# @return [Hash] The raw parsed response
|
|
580
|
+
attr_reader :raw
|
|
479
581
|
|
|
480
582
|
def initialize(data)
|
|
481
|
-
|
|
482
|
-
@
|
|
483
|
-
@
|
|
484
|
-
@
|
|
583
|
+
user = data["user"] || {}
|
|
584
|
+
@raw = data
|
|
585
|
+
@organization = data["organization"]
|
|
586
|
+
@organization_id = @organization && @organization["id"]
|
|
587
|
+
@id = user["id"] || data["id"]
|
|
588
|
+
@email = user["email"] || data["email"]
|
|
589
|
+
@name = (@organization && @organization["name"]) || data["name"]
|
|
590
|
+
@created_at = parse_time(user["createdAt"] || user["created_at"] || data["created_at"] || data["createdAt"])
|
|
591
|
+
@credits = data["credits"]
|
|
592
|
+
@verification = data["verification"]
|
|
593
|
+
@api_key = data["apiKey"] || data["api_key"]
|
|
594
|
+
@limits = data["limits"]
|
|
485
595
|
end
|
|
486
596
|
|
|
487
597
|
private
|
|
@@ -509,8 +619,9 @@ module Sendly
|
|
|
509
619
|
class CreditTransaction
|
|
510
620
|
attr_reader :id, :type, :amount, :balance_after, :description, :message_id, :created_at
|
|
511
621
|
|
|
512
|
-
# Transaction type constants
|
|
513
|
-
|
|
622
|
+
# Transaction type constants. Auto-recharges are recorded as +purchase+;
|
|
623
|
+
# +adjustment+ is never recorded.
|
|
624
|
+
TYPES = %w[purchase usage refund adjustment bonus transfer admin_grant admin_seed].freeze
|
|
514
625
|
|
|
515
626
|
def initialize(data)
|
|
516
627
|
@id = data["id"]
|
|
@@ -542,20 +653,41 @@ module Sendly
|
|
|
542
653
|
|
|
543
654
|
# Represents an API key
|
|
544
655
|
class ApiKey
|
|
545
|
-
attr_reader :id, :name, :type, :prefix, :
|
|
656
|
+
attr_reader :id, :name, :type, :prefix, :permissions,
|
|
546
657
|
:created_at, :last_used_at, :expires_at, :is_revoked
|
|
547
658
|
|
|
659
|
+
# @return [nil] Always +nil+: no API response carries the last four characters
|
|
660
|
+
attr_reader :last_four
|
|
661
|
+
|
|
662
|
+
# @return [Array<String>] The key's scopes (the same list as {#permissions})
|
|
663
|
+
attr_reader :scopes
|
|
664
|
+
|
|
665
|
+
# @return [Boolean, nil] Whether the key is active, or +nil+ when the response does not say
|
|
666
|
+
attr_reader :is_active
|
|
667
|
+
|
|
668
|
+
# @return [Time, nil] When the key was revoked
|
|
669
|
+
attr_reader :revoked_at
|
|
670
|
+
|
|
548
671
|
def initialize(data)
|
|
549
672
|
@id = data["id"]
|
|
550
673
|
@name = data["name"]
|
|
551
674
|
@type = data["type"]
|
|
552
675
|
@prefix = data["prefix"]
|
|
553
676
|
@last_four = data["last_four"] || data["lastFour"]
|
|
554
|
-
@permissions = data["permissions"] || []
|
|
677
|
+
@permissions = data["permissions"] || data["scopes"] || []
|
|
678
|
+
@scopes = data["scopes"] || data["permissions"] || []
|
|
555
679
|
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
556
680
|
@last_used_at = parse_time(data["last_used_at"] || data["lastUsedAt"])
|
|
557
681
|
@expires_at = parse_time(data["expires_at"] || data["expiresAt"])
|
|
558
|
-
@
|
|
682
|
+
@revoked_at = parse_time(data["revoked_at"] || data["revokedAt"])
|
|
683
|
+
@is_active = data.key?("isActive") ? data["isActive"] : data["is_active"]
|
|
684
|
+
@is_revoked = if data.key?("isRevoked") || data.key?("is_revoked")
|
|
685
|
+
data["isRevoked"] || data["is_revoked"] || false
|
|
686
|
+
elsif !@is_active.nil?
|
|
687
|
+
!@is_active
|
|
688
|
+
else
|
|
689
|
+
!@revoked_at.nil?
|
|
690
|
+
end
|
|
559
691
|
end
|
|
560
692
|
|
|
561
693
|
def test?
|
|
@@ -842,13 +974,20 @@ module Sendly
|
|
|
842
974
|
|
|
843
975
|
attr_reader :data, :total, :limit, :offset, :has_more
|
|
844
976
|
|
|
845
|
-
|
|
977
|
+
# @param response [Hash] The parsed list response
|
|
978
|
+
# @param limit [Integer, nil] The page size the API used, when the response does not say
|
|
979
|
+
# @param offset [Integer, nil] The offset the API used, when the response does not say
|
|
980
|
+
def initialize(response, limit = nil, offset = nil)
|
|
846
981
|
@data = (response["data"] || []).map { |d| Draft.new(d) }
|
|
847
982
|
pagination = response["pagination"] || {}
|
|
848
983
|
@total = pagination["total"] || @data.length
|
|
849
|
-
@limit = pagination["limit"] || 20
|
|
850
|
-
@offset = pagination["offset"] || 0
|
|
851
|
-
@has_more = pagination
|
|
984
|
+
@limit = pagination["limit"] || limit || 20
|
|
985
|
+
@offset = pagination["offset"] || offset || 0
|
|
986
|
+
@has_more = if pagination.key?("hasMore") || pagination.key?("has_more")
|
|
987
|
+
pagination["hasMore"] || pagination["has_more"] || false
|
|
988
|
+
else
|
|
989
|
+
(@offset + @data.length) < @total
|
|
990
|
+
end
|
|
852
991
|
end
|
|
853
992
|
|
|
854
993
|
def each(&block)
|
data/lib/sendly/version.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Sendly
|
|
|
16
16
|
# @param mode [String, nil] Event mode filter: "all", "test", or "live" (live requires verification)
|
|
17
17
|
# @param metadata [Hash, nil] Custom metadata
|
|
18
18
|
# @return [Sendly::WebhookCreatedResponse]
|
|
19
|
+
# @raise [ArgumentError] If +url+ is not an https:// URL or +events+ is
|
|
20
|
+
# empty; no request is sent
|
|
19
21
|
#
|
|
20
22
|
# @example
|
|
21
23
|
# webhook = client.webhooks.create(
|
|
@@ -65,6 +67,8 @@ module Sendly
|
|
|
65
67
|
# @param mode [String, nil] Event mode filter: "all", "test", or "live"
|
|
66
68
|
# @param metadata [Hash, nil] Custom metadata
|
|
67
69
|
# @return [Sendly::Webhook]
|
|
70
|
+
# @raise [ArgumentError] If +webhook_id+ does not start with +whk_+ or
|
|
71
|
+
# +url+ is not an https:// URL; no request is sent
|
|
68
72
|
def update(webhook_id, url: nil, events: nil, description: nil, is_active: nil, mode: nil, metadata: nil)
|
|
69
73
|
validate_webhook_id!(webhook_id)
|
|
70
74
|
raise ArgumentError, "Webhook URL must be HTTPS" if url && !url.start_with?("https://")
|
|
@@ -94,7 +98,9 @@ module Sendly
|
|
|
94
98
|
# Test a webhook endpoint
|
|
95
99
|
#
|
|
96
100
|
# @param webhook_id [String] Webhook ID
|
|
97
|
-
# @return [Sendly::WebhookTestResult]
|
|
101
|
+
# @return [Sendly::WebhookTestResult] The delivered test, with its status code and timing
|
|
102
|
+
# @raise [Sendly::ValidationError] If the test delivery fails; the message
|
|
103
|
+
# is the API's, e.g. "Test webhook failed: ..."
|
|
98
104
|
def test(webhook_id)
|
|
99
105
|
validate_webhook_id!(webhook_id)
|
|
100
106
|
response = @client.post("/webhooks/#{URI.encode_www_form_component(webhook_id)}/test")
|
|
@@ -139,8 +145,10 @@ module Sendly
|
|
|
139
145
|
# Backfill missed webhook events from the underlying message log.
|
|
140
146
|
#
|
|
141
147
|
# Use when a circuit-breaker outage left events with no audit row (the
|
|
142
|
-
# case {#redeliver} cannot recover). Synthesized events
|
|
143
|
-
#
|
|
148
|
+
# case {#redeliver} cannot recover). Synthesized message events carry the
|
|
149
|
+
# same event id the original dispatch used, so dedupe on event.id. Do not
|
|
150
|
+
# dedupe on event.data.object.id: a message's sent and delivered events
|
|
151
|
+
# share it.
|
|
144
152
|
# Rejects with HTTP 409 if the circuit is currently open — call
|
|
145
153
|
# {#reset_circuit} first.
|
|
146
154
|
#
|
|
@@ -162,6 +170,11 @@ module Sendly
|
|
|
162
170
|
|
|
163
171
|
# Rotate the webhook signing secret
|
|
164
172
|
#
|
|
173
|
+
# Deliveries are signed with the new secret as soon as this returns, and
|
|
174
|
+
# the old secret is not kept, so have your endpoint accept both secrets
|
|
175
|
+
# while you deploy the new one. The new secret is shown only once, on
|
|
176
|
+
# {Sendly::WebhookSecretRotation#new_secret}.
|
|
177
|
+
#
|
|
165
178
|
# @param webhook_id [String] Webhook ID
|
|
166
179
|
# @return [Sendly::WebhookSecretRotation]
|
|
167
180
|
def rotate_secret(webhook_id)
|
|
@@ -170,14 +183,24 @@ module Sendly
|
|
|
170
183
|
WebhookSecretRotation.new(response)
|
|
171
184
|
end
|
|
172
185
|
|
|
173
|
-
# Get delivery history for a webhook
|
|
186
|
+
# Get delivery history for a webhook, newest first
|
|
174
187
|
#
|
|
175
188
|
# @param webhook_id [String] Webhook ID
|
|
189
|
+
# @param limit [Integer, nil] Maximum deliveries to return (the API defaults to 50, max 100)
|
|
190
|
+
# @param offset [Integer, nil] Number of deliveries to skip
|
|
191
|
+
# @param status [String, nil] Only deliveries with this status
|
|
192
|
+
# (see {Sendly::WebhookDelivery::STATUSES})
|
|
176
193
|
# @return [Array<Sendly::WebhookDelivery>]
|
|
177
|
-
def deliveries(webhook_id)
|
|
194
|
+
def deliveries(webhook_id, limit: nil, offset: nil, status: nil)
|
|
178
195
|
validate_webhook_id!(webhook_id)
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
params = {}
|
|
197
|
+
params[:limit] = limit unless limit.nil?
|
|
198
|
+
params[:offset] = offset unless offset.nil?
|
|
199
|
+
params[:status] = status unless status.nil?
|
|
200
|
+
|
|
201
|
+
response = @client.get("/webhooks/#{URI.encode_www_form_component(webhook_id)}/deliveries", params)
|
|
202
|
+
items = response.is_a?(Hash) ? response["deliveries"] : response
|
|
203
|
+
(items || []).map { |data| WebhookDelivery.new(data) }
|
|
181
204
|
end
|
|
182
205
|
|
|
183
206
|
# Retry a failed delivery
|