sendly 1.0.8 → 1.1.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/Gemfile.lock +1 -1
- data/lib/sendly/account_resource.rb +70 -0
- data/lib/sendly/client.rb +25 -0
- data/lib/sendly/types.rb +295 -2
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks_resource.rb +149 -0
- data/lib/sendly.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f970f1462ecd9facfec70a5ba0b167e5da0c697f1dcb96203874f1325cb1b03
|
|
4
|
+
data.tar.gz: d5b5ab964db06976dd6a848d137f5b2b8af07607c795ad4dd96d729ae4c37285
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d110ea4e88716ef5299c77d37d47ac55817f796113e7ebdadb21aed864d1ad1fcb2f082c724b50e20a4c473fd762ae69726e4b1b0907a21a982886a6719a1fc5
|
|
7
|
+
data.tar.gz: 31ac699cd4fc8305ba04c3ba829fcc5be63436811a6be8ce9606b69641ee401b9b0d17b2b91afccc3210677d006bd244efc35ef9ad6ce7cc4838f724dbb7fc72
|
data/Gemfile.lock
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
# Account resource for accessing account information, credits, and API keys
|
|
5
|
+
class AccountResource
|
|
6
|
+
# @param client [Sendly::Client] The API client
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get account information
|
|
12
|
+
#
|
|
13
|
+
# @return [Sendly::Account]
|
|
14
|
+
def get
|
|
15
|
+
response = @client.get("/account")
|
|
16
|
+
Account.new(response)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get credit balance
|
|
20
|
+
#
|
|
21
|
+
# @return [Sendly::Credits]
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# credits = client.account.credits
|
|
25
|
+
# puts "Available: #{credits.available_balance} credits"
|
|
26
|
+
def credits
|
|
27
|
+
response = @client.get("/credits")
|
|
28
|
+
Credits.new(response)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get credit transaction history
|
|
32
|
+
#
|
|
33
|
+
# @param limit [Integer, nil] Maximum number of transactions to return
|
|
34
|
+
# @param offset [Integer, nil] Number of transactions to skip
|
|
35
|
+
# @return [Array<Sendly::CreditTransaction>]
|
|
36
|
+
def transactions(limit: nil, offset: nil)
|
|
37
|
+
params = {}
|
|
38
|
+
params[:limit] = limit if limit
|
|
39
|
+
params[:offset] = offset if offset
|
|
40
|
+
|
|
41
|
+
response = @client.get("/credits/transactions", params)
|
|
42
|
+
response.map { |data| CreditTransaction.new(data) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# List API keys for the account
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Sendly::ApiKey>]
|
|
48
|
+
def api_keys
|
|
49
|
+
response = @client.get("/keys")
|
|
50
|
+
response.map { |data| ApiKey.new(data) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get a specific API key by ID
|
|
54
|
+
#
|
|
55
|
+
# @param key_id [String] API key ID
|
|
56
|
+
# @return [Sendly::ApiKey]
|
|
57
|
+
def api_key(key_id)
|
|
58
|
+
response = @client.get("/keys/#{key_id}")
|
|
59
|
+
ApiKey.new(response)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get usage statistics for an API key
|
|
63
|
+
#
|
|
64
|
+
# @param key_id [String] API key ID
|
|
65
|
+
# @return [Hash] Usage statistics
|
|
66
|
+
def api_key_usage(key_id)
|
|
67
|
+
@client.get("/keys/#{key_id}/usage")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/sendly/client.rb
CHANGED
|
@@ -45,6 +45,20 @@ module Sendly
|
|
|
45
45
|
@messages ||= Messages.new(self)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Access the Webhooks resource
|
|
49
|
+
#
|
|
50
|
+
# @return [Sendly::WebhooksResource]
|
|
51
|
+
def webhooks
|
|
52
|
+
@webhooks ||= WebhooksResource.new(self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Access the Account resource
|
|
56
|
+
#
|
|
57
|
+
# @return [Sendly::AccountResource]
|
|
58
|
+
def account
|
|
59
|
+
@account ||= AccountResource.new(self)
|
|
60
|
+
end
|
|
61
|
+
|
|
48
62
|
# Make a GET request
|
|
49
63
|
#
|
|
50
64
|
# @param path [String] API path
|
|
@@ -63,6 +77,15 @@ module Sendly
|
|
|
63
77
|
request(:post, path, body: body)
|
|
64
78
|
end
|
|
65
79
|
|
|
80
|
+
# Make a PATCH request
|
|
81
|
+
#
|
|
82
|
+
# @param path [String] API path
|
|
83
|
+
# @param body [Hash] Request body
|
|
84
|
+
# @return [Hash] Response body
|
|
85
|
+
def patch(path, body = {})
|
|
86
|
+
request(:patch, path, body: body)
|
|
87
|
+
end
|
|
88
|
+
|
|
66
89
|
# Make a DELETE request
|
|
67
90
|
#
|
|
68
91
|
# @param path [String] API path
|
|
@@ -137,6 +160,8 @@ module Sendly
|
|
|
137
160
|
Net::HTTP::Get.new(uri)
|
|
138
161
|
when :post
|
|
139
162
|
Net::HTTP::Post.new(uri)
|
|
163
|
+
when :patch
|
|
164
|
+
Net::HTTP::Patch.new(uri)
|
|
140
165
|
when :delete
|
|
141
166
|
Net::HTTP::Delete.new(uri)
|
|
142
167
|
else
|
data/lib/sendly/types.rb
CHANGED
|
@@ -18,6 +18,9 @@ module Sendly
|
|
|
18
18
|
# @return [String] Delivery status
|
|
19
19
|
attr_reader :status
|
|
20
20
|
|
|
21
|
+
# @return [String] Message direction (outbound or inbound)
|
|
22
|
+
attr_reader :direction
|
|
23
|
+
|
|
21
24
|
# @return [String, nil] Error message if failed
|
|
22
25
|
attr_reader :error
|
|
23
26
|
|
|
@@ -30,14 +33,29 @@ module Sendly
|
|
|
30
33
|
# @return [Boolean] Whether sent in sandbox mode
|
|
31
34
|
attr_reader :is_sandbox
|
|
32
35
|
|
|
36
|
+
# @return [String, nil] How the message was sent (number_pool, alphanumeric, sandbox)
|
|
37
|
+
attr_reader :sender_type
|
|
38
|
+
|
|
39
|
+
# @return [String, nil] Telnyx message ID for tracking
|
|
40
|
+
attr_reader :telnyx_message_id
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] Warning message
|
|
43
|
+
attr_reader :warning
|
|
44
|
+
|
|
45
|
+
# @return [String, nil] Note about sender behavior
|
|
46
|
+
attr_reader :sender_note
|
|
47
|
+
|
|
33
48
|
# @return [Time, nil] Creation timestamp
|
|
34
49
|
attr_reader :created_at
|
|
35
50
|
|
|
36
51
|
# @return [Time, nil] Delivery timestamp
|
|
37
52
|
attr_reader :delivered_at
|
|
38
53
|
|
|
39
|
-
# Message status constants
|
|
40
|
-
STATUSES = %w[queued
|
|
54
|
+
# Message status constants (sending removed - doesn't exist in database)
|
|
55
|
+
STATUSES = %w[queued sent delivered failed].freeze
|
|
56
|
+
|
|
57
|
+
# Sender type constants
|
|
58
|
+
SENDER_TYPES = %w[number_pool alphanumeric sandbox].freeze
|
|
41
59
|
|
|
42
60
|
def initialize(data)
|
|
43
61
|
@id = data["id"]
|
|
@@ -45,10 +63,15 @@ module Sendly
|
|
|
45
63
|
@from = data["from"]
|
|
46
64
|
@text = data["text"]
|
|
47
65
|
@status = data["status"]
|
|
66
|
+
@direction = data["direction"] || "outbound"
|
|
48
67
|
@error = data["error"]
|
|
49
68
|
@segments = data["segments"] || 1
|
|
50
69
|
@credits_used = data["creditsUsed"] || 0
|
|
51
70
|
@is_sandbox = data["isSandbox"] || false
|
|
71
|
+
@sender_type = data["senderType"]
|
|
72
|
+
@telnyx_message_id = data["telnyxMessageId"]
|
|
73
|
+
@warning = data["warning"]
|
|
74
|
+
@sender_note = data["senderNote"]
|
|
52
75
|
@created_at = parse_time(data["createdAt"])
|
|
53
76
|
@delivered_at = parse_time(data["deliveredAt"])
|
|
54
77
|
end
|
|
@@ -80,10 +103,15 @@ module Sendly
|
|
|
80
103
|
from: from,
|
|
81
104
|
text: text,
|
|
82
105
|
status: status,
|
|
106
|
+
direction: direction,
|
|
83
107
|
error: error,
|
|
84
108
|
segments: segments,
|
|
85
109
|
credits_used: credits_used,
|
|
86
110
|
is_sandbox: is_sandbox,
|
|
111
|
+
sender_type: sender_type,
|
|
112
|
+
telnyx_message_id: telnyx_message_id,
|
|
113
|
+
warning: warning,
|
|
114
|
+
sender_note: sender_note,
|
|
87
115
|
created_at: created_at&.iso8601,
|
|
88
116
|
delivered_at: delivered_at&.iso8601
|
|
89
117
|
}.compact
|
|
@@ -159,4 +187,269 @@ module Sendly
|
|
|
159
187
|
data.last
|
|
160
188
|
end
|
|
161
189
|
end
|
|
190
|
+
|
|
191
|
+
# ============================================================================
|
|
192
|
+
# Webhooks
|
|
193
|
+
# ============================================================================
|
|
194
|
+
|
|
195
|
+
# Represents a configured webhook endpoint
|
|
196
|
+
class Webhook
|
|
197
|
+
attr_reader :id, :url, :events, :description, :is_active, :failure_count,
|
|
198
|
+
:last_failure_at, :circuit_state, :circuit_opened_at, :api_version,
|
|
199
|
+
:metadata, :created_at, :updated_at, :total_deliveries,
|
|
200
|
+
:successful_deliveries, :success_rate, :last_delivery_at
|
|
201
|
+
|
|
202
|
+
# Circuit state constants
|
|
203
|
+
CIRCUIT_STATES = %w[closed open half_open].freeze
|
|
204
|
+
|
|
205
|
+
def initialize(data)
|
|
206
|
+
@id = data["id"]
|
|
207
|
+
@url = data["url"]
|
|
208
|
+
@events = data["events"] || []
|
|
209
|
+
@description = data["description"]
|
|
210
|
+
# Handle both snake_case API response and camelCase
|
|
211
|
+
@is_active = data["is_active"] || data["isActive"] || false
|
|
212
|
+
@failure_count = data["failure_count"] || data["failureCount"] || 0
|
|
213
|
+
@last_failure_at = parse_time(data["last_failure_at"] || data["lastFailureAt"])
|
|
214
|
+
@circuit_state = data["circuit_state"] || data["circuitState"] || "closed"
|
|
215
|
+
@circuit_opened_at = parse_time(data["circuit_opened_at"] || data["circuitOpenedAt"])
|
|
216
|
+
@api_version = data["api_version"] || data["apiVersion"] || "2024-01"
|
|
217
|
+
@metadata = data["metadata"] || {}
|
|
218
|
+
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
219
|
+
@updated_at = parse_time(data["updated_at"] || data["updatedAt"])
|
|
220
|
+
@total_deliveries = data["total_deliveries"] || data["totalDeliveries"] || 0
|
|
221
|
+
@successful_deliveries = data["successful_deliveries"] || data["successfulDeliveries"] || 0
|
|
222
|
+
@success_rate = data["success_rate"] || data["successRate"] || 0
|
|
223
|
+
@last_delivery_at = parse_time(data["last_delivery_at"] || data["lastDeliveryAt"])
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def active?
|
|
227
|
+
is_active
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def circuit_open?
|
|
231
|
+
circuit_state == "open"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def to_h
|
|
235
|
+
{
|
|
236
|
+
id: id, url: url, events: events, description: description,
|
|
237
|
+
is_active: is_active, failure_count: failure_count,
|
|
238
|
+
circuit_state: circuit_state, api_version: api_version,
|
|
239
|
+
metadata: metadata, total_deliveries: total_deliveries,
|
|
240
|
+
successful_deliveries: successful_deliveries, success_rate: success_rate
|
|
241
|
+
}.compact
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def parse_time(value)
|
|
247
|
+
return nil if value.nil?
|
|
248
|
+
Time.parse(value)
|
|
249
|
+
rescue ArgumentError
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Webhook with secret (returned on creation)
|
|
255
|
+
class WebhookCreatedResponse < Webhook
|
|
256
|
+
attr_reader :secret
|
|
257
|
+
|
|
258
|
+
def initialize(data)
|
|
259
|
+
super(data)
|
|
260
|
+
@secret = data["secret"]
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Represents a webhook delivery attempt
|
|
265
|
+
class WebhookDelivery
|
|
266
|
+
attr_reader :id, :webhook_id, :event_id, :event_type, :attempt_number,
|
|
267
|
+
:max_attempts, :status, :response_status_code, :response_time_ms,
|
|
268
|
+
:error_message, :error_code, :next_retry_at, :created_at, :delivered_at
|
|
269
|
+
|
|
270
|
+
# Delivery status constants
|
|
271
|
+
STATUSES = %w[pending delivered failed cancelled].freeze
|
|
272
|
+
|
|
273
|
+
def initialize(data)
|
|
274
|
+
@id = data["id"]
|
|
275
|
+
@webhook_id = data["webhook_id"] || data["webhookId"]
|
|
276
|
+
@event_id = data["event_id"] || data["eventId"]
|
|
277
|
+
@event_type = data["event_type"] || data["eventType"]
|
|
278
|
+
@attempt_number = data["attempt_number"] || data["attemptNumber"] || 1
|
|
279
|
+
@max_attempts = data["max_attempts"] || data["maxAttempts"] || 6
|
|
280
|
+
@status = data["status"]
|
|
281
|
+
@response_status_code = data["response_status_code"] || data["responseStatusCode"]
|
|
282
|
+
@response_time_ms = data["response_time_ms"] || data["responseTimeMs"]
|
|
283
|
+
@error_message = data["error_message"] || data["errorMessage"]
|
|
284
|
+
@error_code = data["error_code"] || data["errorCode"]
|
|
285
|
+
@next_retry_at = parse_time(data["next_retry_at"] || data["nextRetryAt"])
|
|
286
|
+
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
287
|
+
@delivered_at = parse_time(data["delivered_at"] || data["deliveredAt"])
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def delivered?
|
|
291
|
+
status == "delivered"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def failed?
|
|
295
|
+
status == "failed"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
private
|
|
299
|
+
|
|
300
|
+
def parse_time(value)
|
|
301
|
+
return nil if value.nil?
|
|
302
|
+
Time.parse(value)
|
|
303
|
+
rescue ArgumentError
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Result of testing a webhook
|
|
309
|
+
class WebhookTestResult
|
|
310
|
+
attr_reader :success, :status_code, :response_time_ms, :error
|
|
311
|
+
|
|
312
|
+
def initialize(data)
|
|
313
|
+
@success = data["success"]
|
|
314
|
+
@status_code = data["status_code"] || data["statusCode"]
|
|
315
|
+
@response_time_ms = data["response_time_ms"] || data["responseTimeMs"]
|
|
316
|
+
@error = data["error"]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def success?
|
|
320
|
+
success
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Result of rotating webhook secret
|
|
325
|
+
class WebhookSecretRotation
|
|
326
|
+
attr_reader :webhook, :new_secret, :old_secret_expires_at, :message
|
|
327
|
+
|
|
328
|
+
def initialize(data)
|
|
329
|
+
@webhook = Webhook.new(data["webhook"])
|
|
330
|
+
@new_secret = data["new_secret"] || data["newSecret"]
|
|
331
|
+
@old_secret_expires_at = parse_time(data["old_secret_expires_at"] || data["oldSecretExpiresAt"])
|
|
332
|
+
@message = data["message"]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
private
|
|
336
|
+
|
|
337
|
+
def parse_time(value)
|
|
338
|
+
return nil if value.nil?
|
|
339
|
+
Time.parse(value)
|
|
340
|
+
rescue ArgumentError
|
|
341
|
+
nil
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# ============================================================================
|
|
346
|
+
# Account & Credits
|
|
347
|
+
# ============================================================================
|
|
348
|
+
|
|
349
|
+
# Represents account information
|
|
350
|
+
class Account
|
|
351
|
+
attr_reader :id, :email, :name, :created_at
|
|
352
|
+
|
|
353
|
+
def initialize(data)
|
|
354
|
+
@id = data["id"]
|
|
355
|
+
@email = data["email"]
|
|
356
|
+
@name = data["name"]
|
|
357
|
+
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private
|
|
361
|
+
|
|
362
|
+
def parse_time(value)
|
|
363
|
+
return nil if value.nil?
|
|
364
|
+
Time.parse(value)
|
|
365
|
+
rescue ArgumentError
|
|
366
|
+
nil
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Represents credit balance information
|
|
371
|
+
class Credits
|
|
372
|
+
attr_reader :balance, :reserved_balance, :available_balance
|
|
373
|
+
|
|
374
|
+
def initialize(data)
|
|
375
|
+
@balance = data["balance"] || 0
|
|
376
|
+
@reserved_balance = data["reserved_balance"] || data["reservedBalance"] || 0
|
|
377
|
+
@available_balance = data["available_balance"] || data["availableBalance"] || 0
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Represents a credit transaction
|
|
382
|
+
class CreditTransaction
|
|
383
|
+
attr_reader :id, :type, :amount, :balance_after, :description, :message_id, :created_at
|
|
384
|
+
|
|
385
|
+
# Transaction type constants
|
|
386
|
+
TYPES = %w[purchase usage refund adjustment bonus].freeze
|
|
387
|
+
|
|
388
|
+
def initialize(data)
|
|
389
|
+
@id = data["id"]
|
|
390
|
+
@type = data["type"]
|
|
391
|
+
@amount = data["amount"] || 0
|
|
392
|
+
@balance_after = data["balance_after"] || data["balanceAfter"] || 0
|
|
393
|
+
@description = data["description"]
|
|
394
|
+
@message_id = data["message_id"] || data["messageId"]
|
|
395
|
+
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def credit?
|
|
399
|
+
amount > 0
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def debit?
|
|
403
|
+
amount < 0
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
private
|
|
407
|
+
|
|
408
|
+
def parse_time(value)
|
|
409
|
+
return nil if value.nil?
|
|
410
|
+
Time.parse(value)
|
|
411
|
+
rescue ArgumentError
|
|
412
|
+
nil
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Represents an API key
|
|
417
|
+
class ApiKey
|
|
418
|
+
attr_reader :id, :name, :type, :prefix, :last_four, :permissions,
|
|
419
|
+
:created_at, :last_used_at, :expires_at, :is_revoked
|
|
420
|
+
|
|
421
|
+
def initialize(data)
|
|
422
|
+
@id = data["id"]
|
|
423
|
+
@name = data["name"]
|
|
424
|
+
@type = data["type"]
|
|
425
|
+
@prefix = data["prefix"]
|
|
426
|
+
@last_four = data["last_four"] || data["lastFour"]
|
|
427
|
+
@permissions = data["permissions"] || []
|
|
428
|
+
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
429
|
+
@last_used_at = parse_time(data["last_used_at"] || data["lastUsedAt"])
|
|
430
|
+
@expires_at = parse_time(data["expires_at"] || data["expiresAt"])
|
|
431
|
+
@is_revoked = data["is_revoked"] || data["isRevoked"] || false
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def test?
|
|
435
|
+
type == "test"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def live?
|
|
439
|
+
type == "live"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def revoked?
|
|
443
|
+
is_revoked
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
private
|
|
447
|
+
|
|
448
|
+
def parse_time(value)
|
|
449
|
+
return nil if value.nil?
|
|
450
|
+
Time.parse(value)
|
|
451
|
+
rescue ArgumentError
|
|
452
|
+
nil
|
|
453
|
+
end
|
|
454
|
+
end
|
|
162
455
|
end
|
data/lib/sendly/version.rb
CHANGED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
# Webhooks resource for managing webhook endpoints
|
|
5
|
+
class WebhooksResource
|
|
6
|
+
# @param client [Sendly::Client] The API client
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a new webhook endpoint
|
|
12
|
+
#
|
|
13
|
+
# @param url [String] HTTPS endpoint URL
|
|
14
|
+
# @param events [Array<String>] Event types to subscribe to
|
|
15
|
+
# @param description [String, nil] Optional description
|
|
16
|
+
# @param metadata [Hash, nil] Custom metadata
|
|
17
|
+
# @return [Sendly::WebhookCreatedResponse]
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# webhook = client.webhooks.create(
|
|
21
|
+
# url: "https://example.com/webhooks",
|
|
22
|
+
# events: ["message.delivered", "message.failed"]
|
|
23
|
+
# )
|
|
24
|
+
# puts "Secret: #{webhook.secret}" # Save this - only shown once!
|
|
25
|
+
def create(url:, events:, description: nil, metadata: nil)
|
|
26
|
+
raise ArgumentError, "Webhook URL must be HTTPS" unless url&.start_with?("https://")
|
|
27
|
+
raise ArgumentError, "At least one event type is required" if events.nil? || events.empty?
|
|
28
|
+
|
|
29
|
+
body = { url: url, events: events }
|
|
30
|
+
body[:description] = description if description
|
|
31
|
+
body[:metadata] = metadata if metadata
|
|
32
|
+
|
|
33
|
+
response = @client.post("/webhooks", body)
|
|
34
|
+
WebhookCreatedResponse.new(response)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# List all webhooks
|
|
38
|
+
#
|
|
39
|
+
# @return [Array<Sendly::Webhook>]
|
|
40
|
+
def list
|
|
41
|
+
response = @client.get("/webhooks")
|
|
42
|
+
response.map { |data| Webhook.new(data) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get a specific webhook by ID
|
|
46
|
+
#
|
|
47
|
+
# @param webhook_id [String] Webhook ID (whk_xxx)
|
|
48
|
+
# @return [Sendly::Webhook]
|
|
49
|
+
def get(webhook_id)
|
|
50
|
+
validate_webhook_id!(webhook_id)
|
|
51
|
+
response = @client.get("/webhooks/#{webhook_id}")
|
|
52
|
+
Webhook.new(response)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Update a webhook configuration
|
|
56
|
+
#
|
|
57
|
+
# @param webhook_id [String] Webhook ID
|
|
58
|
+
# @param url [String, nil] New URL
|
|
59
|
+
# @param events [Array<String>, nil] New event subscriptions
|
|
60
|
+
# @param description [String, nil] New description
|
|
61
|
+
# @param is_active [Boolean, nil] Enable/disable webhook
|
|
62
|
+
# @param metadata [Hash, nil] Custom metadata
|
|
63
|
+
# @return [Sendly::Webhook]
|
|
64
|
+
def update(webhook_id, url: nil, events: nil, description: nil, is_active: nil, metadata: nil)
|
|
65
|
+
validate_webhook_id!(webhook_id)
|
|
66
|
+
raise ArgumentError, "Webhook URL must be HTTPS" if url && !url.start_with?("https://")
|
|
67
|
+
|
|
68
|
+
body = {}
|
|
69
|
+
body[:url] = url unless url.nil?
|
|
70
|
+
body[:events] = events unless events.nil?
|
|
71
|
+
body[:description] = description unless description.nil?
|
|
72
|
+
body[:is_active] = is_active unless is_active.nil?
|
|
73
|
+
body[:metadata] = metadata unless metadata.nil?
|
|
74
|
+
|
|
75
|
+
response = @client.patch("/webhooks/#{webhook_id}", body)
|
|
76
|
+
Webhook.new(response)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Delete a webhook
|
|
80
|
+
#
|
|
81
|
+
# @param webhook_id [String] Webhook ID
|
|
82
|
+
# @return [void]
|
|
83
|
+
def delete(webhook_id)
|
|
84
|
+
validate_webhook_id!(webhook_id)
|
|
85
|
+
@client.delete("/webhooks/#{webhook_id}")
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Test a webhook endpoint
|
|
90
|
+
#
|
|
91
|
+
# @param webhook_id [String] Webhook ID
|
|
92
|
+
# @return [Sendly::WebhookTestResult]
|
|
93
|
+
def test(webhook_id)
|
|
94
|
+
validate_webhook_id!(webhook_id)
|
|
95
|
+
response = @client.post("/webhooks/#{webhook_id}/test")
|
|
96
|
+
WebhookTestResult.new(response)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Rotate the webhook signing secret
|
|
100
|
+
#
|
|
101
|
+
# @param webhook_id [String] Webhook ID
|
|
102
|
+
# @return [Sendly::WebhookSecretRotation]
|
|
103
|
+
def rotate_secret(webhook_id)
|
|
104
|
+
validate_webhook_id!(webhook_id)
|
|
105
|
+
response = @client.post("/webhooks/#{webhook_id}/rotate-secret")
|
|
106
|
+
WebhookSecretRotation.new(response)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get delivery history for a webhook
|
|
110
|
+
#
|
|
111
|
+
# @param webhook_id [String] Webhook ID
|
|
112
|
+
# @return [Array<Sendly::WebhookDelivery>]
|
|
113
|
+
def deliveries(webhook_id)
|
|
114
|
+
validate_webhook_id!(webhook_id)
|
|
115
|
+
response = @client.get("/webhooks/#{webhook_id}/deliveries")
|
|
116
|
+
response.map { |data| WebhookDelivery.new(data) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Retry a failed delivery
|
|
120
|
+
#
|
|
121
|
+
# @param webhook_id [String] Webhook ID
|
|
122
|
+
# @param delivery_id [String] Delivery ID
|
|
123
|
+
# @return [void]
|
|
124
|
+
def retry_delivery(webhook_id, delivery_id)
|
|
125
|
+
validate_webhook_id!(webhook_id)
|
|
126
|
+
validate_delivery_id!(delivery_id)
|
|
127
|
+
@client.post("/webhooks/#{webhook_id}/deliveries/#{delivery_id}/retry")
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# List available event types
|
|
132
|
+
#
|
|
133
|
+
# @return [Array<String>]
|
|
134
|
+
def event_types
|
|
135
|
+
response = @client.get("/webhooks/event-types")
|
|
136
|
+
(response["events"] || []).map { |e| e["type"] }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def validate_webhook_id!(webhook_id)
|
|
142
|
+
raise ArgumentError, "Invalid webhook ID format" unless webhook_id&.start_with?("whk_")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def validate_delivery_id!(delivery_id)
|
|
146
|
+
raise ArgumentError, "Invalid delivery ID format" unless delivery_id&.start_with?("del_")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/sendly.rb
CHANGED
|
@@ -9,6 +9,8 @@ require_relative "sendly/types"
|
|
|
9
9
|
require_relative "sendly/client"
|
|
10
10
|
require_relative "sendly/messages"
|
|
11
11
|
require_relative "sendly/webhooks"
|
|
12
|
+
require_relative "sendly/webhooks_resource"
|
|
13
|
+
require_relative "sendly/account_resource"
|
|
12
14
|
|
|
13
15
|
# Sendly Ruby SDK
|
|
14
16
|
#
|
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: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sendly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -123,12 +123,14 @@ files:
|
|
|
123
123
|
- examples/list_messages.rb
|
|
124
124
|
- examples/send_sms.rb
|
|
125
125
|
- lib/sendly.rb
|
|
126
|
+
- lib/sendly/account_resource.rb
|
|
126
127
|
- lib/sendly/client.rb
|
|
127
128
|
- lib/sendly/errors.rb
|
|
128
129
|
- lib/sendly/messages.rb
|
|
129
130
|
- lib/sendly/types.rb
|
|
130
131
|
- lib/sendly/version.rb
|
|
131
132
|
- lib/sendly/webhooks.rb
|
|
133
|
+
- lib/sendly/webhooks_resource.rb
|
|
132
134
|
homepage: https://github.com/sendly-live/sendly-ruby
|
|
133
135
|
licenses:
|
|
134
136
|
- MIT
|