sendly 1.5.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8287e0f6c55aa228285671746446a7eb04577569e56515ed76571da61a0392b0
4
- data.tar.gz: 62017808d07348e8d6e5809b367300e11976a754e7c3323c8155d2d9140befb4
3
+ metadata.gz: 98bfa3c2ecbb430550c840762d157889b5b8ab55d76df74f05be318d2bb05aaf
4
+ data.tar.gz: afc33e6317641bf8aa3153a3d4e622672ed818a43529e005169ca904ed2a273c
5
5
  SHA512:
6
- metadata.gz: 2c3a234e3caa740975c00ad3c1454dbe24f423385f480f180e33f1ac0ef890bbe019f58606ff3f50bb0b508280d9352997dd20b88a6705d277d493235d3ecc57
7
- data.tar.gz: 5e7cba5b80ec78994f3de8b0dfe5c3f63f6c00cede6f5a98c3d7a679755386ee543477f588c672c248181dbfc279bd8834c5108f1a1071378f9e51b23ea94c40
6
+ metadata.gz: c6bf0670c06655163a3e5917ebc2a18bc7afaede56e9ed58d13961d0e63209aaa1c4c55ec3ea5b3e68a4c3118d672c9e67214e91be58f178e12c495efa75d3bf
7
+ data.tar.gz: e94e644039d0fa2fb7d9e08f9f949a900b9bf8d7b2222e96b70ab2f92426fefb6fedfc40282bc59369ed7a1110e37eff9e9acdcaedc33b3a2807b5d66591f5cc
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sendly (1.5.1)
4
+ sendly (2.2.0)
5
5
  faraday (~> 2.0)
6
6
  faraday-retry (~> 2.0)
7
7
 
data/README.md CHANGED
@@ -4,24 +4,17 @@ Official Ruby SDK for the Sendly SMS API.
4
4
 
5
5
  ## Installation
6
6
 
7
- Add to your Gemfile:
7
+ ```bash
8
+ # gem
9
+ gem install sendly
8
10
 
9
- ```ruby
11
+ # Bundler (add to Gemfile)
10
12
  gem 'sendly'
11
- ```
12
-
13
- Then run:
14
13
 
15
- ```bash
14
+ # then run
16
15
  bundle install
17
16
  ```
18
17
 
19
- Or install directly:
20
-
21
- ```bash
22
- gem install sendly
23
- ```
24
-
25
18
  ## Quick Start
26
19
 
27
20
  ```ruby
@@ -82,7 +75,7 @@ Sendly.send_message(to: "+15551234567", text: "Hello!")
82
75
  ```ruby
83
76
  client = Sendly::Client.new(
84
77
  "sk_live_v1_xxx",
85
- base_url: "https://api.sendly.live/v1",
78
+ base_url: "https://sendly.live/api/v1",
86
79
  timeout: 60,
87
80
  max_retries: 5
88
81
  )
@@ -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 sending sent delivered failed].freeze
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "1.5.1"
4
+ VERSION = "2.2.0"
5
5
  end
@@ -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,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sendly
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sendly
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2025-12-22 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: faraday
@@ -122,12 +123,14 @@ files:
122
123
  - examples/list_messages.rb
123
124
  - examples/send_sms.rb
124
125
  - lib/sendly.rb
126
+ - lib/sendly/account_resource.rb
125
127
  - lib/sendly/client.rb
126
128
  - lib/sendly/errors.rb
127
129
  - lib/sendly/messages.rb
128
130
  - lib/sendly/types.rb
129
131
  - lib/sendly/version.rb
130
132
  - lib/sendly/webhooks.rb
133
+ - lib/sendly/webhooks_resource.rb
131
134
  homepage: https://github.com/sendly-live/sendly-ruby
132
135
  licenses:
133
136
  - MIT
@@ -136,6 +139,7 @@ metadata:
136
139
  source_code_uri: https://github.com/sendly-live/sendly-ruby
137
140
  changelog_uri: https://github.com/sendly-live/sendly-ruby/blob/main/CHANGELOG.md
138
141
  documentation_uri: https://sendly.live/docs
142
+ post_install_message:
139
143
  rdoc_options: []
140
144
  require_paths:
141
145
  - lib
@@ -150,7 +154,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
154
  - !ruby/object:Gem::Version
151
155
  version: '0'
152
156
  requirements: []
153
- rubygems_version: 3.6.9
157
+ rubygems_version: 3.4.19
158
+ signing_key:
154
159
  specification_version: 4
155
160
  summary: Official Ruby SDK for the Sendly SMS API
156
161
  test_files: []