sendly 4.3.0 → 4.4.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: 3aee146f9046fd6558c2ec3af67cf715a780538d5f054a212d1fd27c36c52a59
4
- data.tar.gz: f88147e73d75d0a731e2c8fd65634ac79ce5a162908e03d7c778122bb841914f
3
+ metadata.gz: 5a69a00fe63398e0ad342ed03ad9ddf38d668fe0d2ddd85533a2aa43e534eacb
4
+ data.tar.gz: 8488579a38c0f09720a8d9e67a5084819eb884dfe6cc99afc66be5d143ae5323
5
5
  SHA512:
6
- metadata.gz: bd2892234c3a4b5dbd721fe37a0b4ecc8693ea20440fcfa04d724ce9d6ad94fd6f42319aefa9148d3a05e801ac0f2db225096c1b23e7897e54625b406ef1115e
7
- data.tar.gz: 89476582a5b3026c6a07d54fb563425df47328d1ba5f00f5da9e17731dfe3e229ceb96b699efa219f9c9fcaa1907e7bc3e4174b5c52b6b3ce6f004553daead90
6
+ metadata.gz: caa9cd2bb16dc70c5eea42f760fe9295587e0f3b8c7a83bb4de6af903b5ba29ff6ec89c6c53ab211fc630c68d8139648516a95dccd27412382be08012ba922ed
7
+ data.tar.gz: b5ef93d9ce46c42dd53ef375b23aa66a5027e6534bf93425ab507d37f4c2f17311cdefbda52269e2fbad9b167808ad96abae255fd0b216942d5af1873d5f8337
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # sendly (Ruby)
2
2
 
3
+ ## 4.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **WhatsApp to SMS fallback.** `messages.send(channel: "whatsapp", fallback_to_sms: true)` sends `fallbackToSms`; it was dropped before. When WhatsApp can't deliver the message (the 24-hour window is closed, WhatsApp refuses it, or reports it undeliverable later), it goes as an SMS from the same number, billed as an SMS. The new `Sendly::WhatsAppSmsFallback` (`channel`, `status`, `reason`, `message_id`, `credits_used`, `error`, `content_dropped`, with `pending?` and `sent?`, and `STATUSES` and `REASONS`) says what happened: it is `WhatsAppMessage#fallback`, and `WhatsAppMessage#fell_back?` is true when the SMS went. When the SMS went in place of the WhatsApp message, `status` is `"failed"` and `fallback.status` is `"sent"`. `Message` gains `fallback` (on a WhatsApp message from `messages.get` or `messages.list`) and `fallback_of` (on the SMS, the id of the WhatsApp message it replaced), and `to_h` includes them when present. The number must already be able to send SMS to the recipient (US and Canada), or the API answers 403 `sms_fallback_unavailable`; marketing and authentication templates, and media without a caption, get 422 `sms_fallback_not_supported`.
8
+
9
+ ### Patch Changes
10
+
11
+ - **Errors carry the API's error code.** `e.code` is the code the API sent in `error`, such as `"whatsapp_window_closed"`, `"sms_fallback_unavailable"` or `"invalid_api_key"`, on every error class, so refusals with the same status can be told apart. Before, only an unrecognised status read a code, and from a `code` field the API doesn't send, so `e.code` was the class's own code (`"VALIDATION_ERROR"`, `"API_ERROR"` and so on). That code is still used when the body names none (an `error` that is a sentence, or no `error` at all); a body's `code` field is read when `error` isn't a code. Code that compared `e.code` with a class code for an API error should rescue the class instead, or compare with the API's code.
12
+
3
13
  ## 4.3.0
4
14
 
5
15
  ### Minor Changes
data/Gemfile.lock CHANGED
@@ -1,14 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sendly (4.3.0)
4
+ sendly (4.4.0)
5
5
  faraday (~> 2.0)
6
6
  faraday-retry (~> 2.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- addressable (2.9.0)
11
+ addressable (2.8.8)
12
12
  public_suffix (>= 2.0.2, < 8.0)
13
13
  ast (2.4.3)
14
14
  bigdecimal (3.3.1)
@@ -36,7 +36,7 @@ GEM
36
36
  ast (~> 2.4.1)
37
37
  racc
38
38
  prism (1.6.0)
39
- public_suffix (6.0.2)
39
+ public_suffix (7.0.0)
40
40
  racc (1.8.1)
41
41
  rainbow (3.1.1)
42
42
  rake (13.3.1)
@@ -74,7 +74,7 @@ GEM
74
74
  unicode-emoji (~> 4.1)
75
75
  unicode-emoji (4.1.0)
76
76
  uri (1.1.1)
77
- webmock (3.26.4)
77
+ webmock (3.26.1)
78
78
  addressable (>= 2.8.0)
79
79
  crack (>= 0.3.2)
80
80
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -92,4 +92,4 @@ DEPENDENCIES
92
92
  webmock (~> 3.0)
93
93
 
94
94
  BUNDLED WITH
95
- 2.5.23
95
+ 2.6.9
data/README.md CHANGED
@@ -1316,6 +1316,25 @@ client.messages.send(
1316
1316
  text: "Here is your receipt",
1317
1317
  media_urls: ["https://acme.example/receipt.pdf"]
1318
1318
  )
1319
+
1320
+ # Fall back to SMS from the same number when WhatsApp can't deliver: the
1321
+ # window is closed, WhatsApp refuses it, or reports it undeliverable later.
1322
+ # The SMS is billed as an SMS. The number must already be able to send SMS to
1323
+ # the recipient (US and Canada), or this raises 403 sms_fallback_unavailable;
1324
+ # marketing and authentication templates, and media without a caption, raise
1325
+ # 422 sms_fallback_not_supported.
1326
+ message = client.messages.send(
1327
+ channel: "whatsapp",
1328
+ to: "+15555550100",
1329
+ from: "+15125550123",
1330
+ text: "Your table is ready!",
1331
+ fallback_to_sms: true
1332
+ )
1333
+ message.fallback.status # "pending", "sent", "failed", "unavailable" or "skipped"
1334
+ message.fallback.reason # "window_closed", "whatsapp_refused" or "whatsapp_failed"
1335
+ message.fell_back? # true when the SMS went (message.status is then "failed")
1336
+ sms = client.messages.get(message.fallback.message_id) if message.fallback.message_id
1337
+ sms&.fallback_of # the WhatsApp message's id
1319
1338
  ```
1320
1339
 
1321
1340
  WhatsApp errors map onto the usual classes:
@@ -1770,12 +1789,12 @@ begin
1770
1789
  rescue Sendly::AuthenticationError => e
1771
1790
  puts "Invalid API key"
1772
1791
  rescue Sendly::RateLimitError => e
1773
- if e.response_body&.dig("error") == "too_many_failed_key_attempts"
1792
+ if e.code == "too_many_failed_key_attempts"
1774
1793
  # Repeated wrong API keys from this address locked it out for up to
1775
1794
  # 5 minutes. Fix the key; do not retry until the lockout ends.
1776
1795
  puts "Locked out for #{e.retry_after}s: check the API key"
1777
1796
  else
1778
- puts "Rate limited (#{e.response_body&.dig('error')}), retry after #{e.retry_after} seconds"
1797
+ puts "Rate limited (#{e.code}), retry after #{e.retry_after} seconds"
1779
1798
  end
1780
1799
  rescue Sendly::InsufficientCreditsError => e
1781
1800
  puts "Add more credits to your account"
@@ -1786,17 +1805,18 @@ rescue Sendly::NotFoundError => e
1786
1805
  rescue Sendly::NetworkError => e
1787
1806
  puts "Network error: #{e.message}"
1788
1807
  rescue Sendly::Error => e
1789
- puts "Error: #{e.message} (#{e.response_body&.dig('error') || e.code})"
1808
+ puts "Error: #{e.message} (#{e.code})"
1790
1809
  end
1791
1810
  ```
1792
1811
 
1793
1812
  Every error is a `Sendly::Error`, which carries `message`, `code`,
1794
1813
  `status_code`, `details` and `response_body` — the parsed JSON body of the
1795
1814
  API's response, or `nil` for an error raised before the request went out.
1796
- The API's own error code is `e.response_body["error"]`; `code` is the SDK's
1797
- name for the class (`"VALIDATION_ERROR"`, `"RATE_LIMIT_EXCEEDED"` and so on),
1798
- except on `APIError`, where it is the body's `"code"` when there is one. Some
1799
- refusals put more in that body than a message: a 409 `agent_in_use` lists the
1815
+ `code` is the API's own error code, such as `"whatsapp_window_closed"` or
1816
+ `"sms_fallback_unavailable"` (the body's `"error"`), so refusals with the same
1817
+ status can be told apart. When the body names no code, `code` is the SDK's
1818
+ name for the class (`"VALIDATION_ERROR"`, `"RATE_LIMIT_EXCEEDED"` and so on).
1819
+ Some refusals put more in that body than a message: a 409 `agent_in_use` lists the
1800
1820
  numbers under `"numbers"`, a 422 `invalid_address` carries a corrected
1801
1821
  address under `"suggested"`.
1802
1822
 
@@ -1809,7 +1829,7 @@ endpoint.
1809
1829
  | Class | Raised for |
1810
1830
  |-------|-----------|
1811
1831
  | `Sendly::AuthenticationError` | 401, and a missing or malformed API key at construction |
1812
- | `Sendly::ValidationError` | 400 and 422; `field_errors` holds the API's `errors` list. `status_code` reads 400 for both, so tell them apart by `response_body["error"]` |
1832
+ | `Sendly::ValidationError` | 400 and 422; `field_errors` holds the API's `errors` list. `status_code` reads 400 for both, so tell them apart by `code` |
1813
1833
  | `Sendly::InsufficientCreditsError` | 402 |
1814
1834
  | `Sendly::NotFoundError` | 404, including a feature that is not enabled for you |
1815
1835
  | `Sendly::RateLimitError` | 429; `retry_after` in seconds when the API sent one |
@@ -2066,7 +2086,6 @@ client.enterprise.workspaces.list_keys("ws_xxx")
2066
2086
  # Enterprise-level account, credits and billing
2067
2087
  client.enterprise.get_account
2068
2088
  client.enterprise.credits.get
2069
- client.enterprise.credits.deposit(amount: 100_000, description: "Q4 top-up")
2070
2089
  client.enterprise.settings.get_auto_top_up
2071
2090
  client.enterprise.settings.update_auto_top_up(enabled: true, threshold: 1000, amount: 10_000)
2072
2091
  client.enterprise.billing.get_breakdown(period: "30d")
@@ -55,6 +55,7 @@ module Sendly
55
55
  ring_timeout callee_declined callee_busy caller_cancelled room_closed_unanswered
56
56
  agent_left_unanswered agent_caller_never_joined invalid_number destination_rejected
57
57
  max_duration credits_exhausted media_aborted peer_connection_lost room_closed agent_left
58
+ transferred voicemail voicemail_unavailable
58
59
  setup_failed agent_dispatch_failed agent_api_unreachable agent_already_ended
59
60
  ended
60
61
  ].freeze
data/lib/sendly/errors.rb CHANGED
@@ -3,7 +3,10 @@
3
3
  module Sendly
4
4
  # Base error class for all Sendly errors
5
5
  class Error < StandardError
6
- # @return [String, nil] Error code from the API
6
+ # @return [String, nil] The API's error code, such as
7
+ # +"whatsapp_window_closed"+ or +"sms_fallback_unavailable"+, for an
8
+ # error built from an API response that names one. Otherwise the
9
+ # class's own code, such as +"VALIDATION_ERROR"+.
7
10
  attr_reader :code
8
11
 
9
12
  # @return [Hash, nil] Additional error details
@@ -40,8 +43,8 @@ module Sendly
40
43
 
41
44
  # Raised when the API key is invalid or missing
42
45
  class AuthenticationError < Error
43
- def initialize(message = "Invalid or missing API key")
44
- super(message, code: "AUTHENTICATION_ERROR", status_code: 401)
46
+ def initialize(message = "Invalid or missing API key", code: nil)
47
+ super(message, code: code || "AUTHENTICATION_ERROR", status_code: 401)
45
48
  end
46
49
  end
47
50
 
@@ -50,16 +53,16 @@ module Sendly
50
53
  # @return [Integer, nil] Seconds to wait before retrying
51
54
  attr_reader :retry_after
52
55
 
53
- def initialize(message = "Rate limit exceeded", retry_after: nil)
56
+ def initialize(message = "Rate limit exceeded", retry_after: nil, code: nil)
54
57
  @retry_after = retry_after
55
- super(message, code: "RATE_LIMIT_EXCEEDED", status_code: 429)
58
+ super(message, code: code || "RATE_LIMIT_EXCEEDED", status_code: 429)
56
59
  end
57
60
  end
58
61
 
59
62
  # Raised when the account has insufficient credits
60
63
  class InsufficientCreditsError < Error
61
- def initialize(message = "Insufficient credits")
62
- super(message, code: "INSUFFICIENT_CREDITS", status_code: 402)
64
+ def initialize(message = "Insufficient credits", code: nil)
65
+ super(message, code: code || "INSUFFICIENT_CREDITS", status_code: 402)
63
66
  end
64
67
  end
65
68
 
@@ -70,16 +73,16 @@ module Sendly
70
73
  # e.g. +[{ "path" => "brand.ein", "message" => "Enter a 9-digit EIN" }]+
71
74
  attr_reader :field_errors
72
75
 
73
- def initialize(message = "Validation failed", field_errors: nil, details: nil)
76
+ def initialize(message = "Validation failed", field_errors: nil, details: nil, code: nil)
74
77
  @field_errors = field_errors
75
- super(message, code: "VALIDATION_ERROR", details: details, status_code: 400)
78
+ super(message, code: code || "VALIDATION_ERROR", details: details, status_code: 400)
76
79
  end
77
80
  end
78
81
 
79
82
  # Raised when the requested resource is not found
80
83
  class NotFoundError < Error
81
- def initialize(message = "Resource not found")
82
- super(message, code: "NOT_FOUND", status_code: 404)
84
+ def initialize(message = "Resource not found", code: nil)
85
+ super(message, code: code || "NOT_FOUND", status_code: 404)
83
86
  end
84
87
  end
85
88
 
@@ -107,38 +110,50 @@ module Sendly
107
110
 
108
111
  # Raised for server errors (5xx)
109
112
  class ServerError < Error
110
- def initialize(message = "Server error occurred", status_code: 500)
111
- super(message, code: "SERVER_ERROR", status_code: status_code)
113
+ def initialize(message = "Server error occurred", status_code: 500, code: nil)
114
+ super(message, code: code || "SERVER_ERROR", status_code: status_code)
112
115
  end
113
116
  end
114
117
 
115
118
  # Convert API response to appropriate error
116
119
  class ErrorFactory
120
+ API_ERROR_CODE = /\A[a-z][a-z0-9_]*\z/
121
+ private_constant :API_ERROR_CODE
122
+
117
123
  def self.from_response(status, body, retry_after_header: nil)
118
124
  message = body["message"] || body["error"] || "Unknown error"
119
- code = body["code"]
125
+ code = api_error_code(body)
120
126
  details = body["details"]
121
127
 
122
128
  error = case status
123
129
  when 400, 422
124
- ValidationError.new(message, details: details, field_errors: body["errors"])
130
+ ValidationError.new(message, details: details, field_errors: body["errors"], code: code)
125
131
  when 401
126
- AuthenticationError.new(message)
132
+ AuthenticationError.new(message, code: code)
127
133
  when 402
128
- InsufficientCreditsError.new(message)
134
+ InsufficientCreditsError.new(message, code: code)
129
135
  when 404
130
- NotFoundError.new(message)
136
+ NotFoundError.new(message, code: code)
131
137
  when 429
132
138
  retry_after = parse_retry_after(retry_after_header) || body["retry_after"] || body["retryAfter"]
133
- RateLimitError.new(message, retry_after: retry_after)
139
+ RateLimitError.new(message, retry_after: retry_after, code: code)
134
140
  when 500..599
135
- ServerError.new(message, status_code: status)
141
+ ServerError.new(message, status_code: status, code: code)
136
142
  else
137
143
  APIError.new(message, status_code: status, code: code, details: details)
138
144
  end
139
145
  error.with_response_body(body)
140
146
  end
141
147
 
148
+ def self.api_error_code(body)
149
+ error = body["error"]
150
+ return error.strip if error.is_a?(String) && error.strip.match?(API_ERROR_CODE)
151
+
152
+ code = body["code"]
153
+ code.is_a?(String) && !code.strip.empty? ? code : nil
154
+ end
155
+ private_class_method :api_error_code
156
+
142
157
  def self.parse_retry_after(value)
143
158
  text = value.to_s.strip
144
159
  return text.to_i if text.match?(/\A\d+\z/)
@@ -62,9 +62,17 @@ module Sendly
62
62
  # { action: { text: ..., postbackData: ..., url: ... } }. Passed
63
63
  # through verbatim, so use the camelCase keys shown. Suggestions have
64
64
  # no SMS form and are dropped on a fallback.
65
- # @param fallback_to_sms [Boolean] RCS only: deliver +text+ as plain SMS
65
+ # @param fallback_to_sms [Boolean] RCS: deliver +text+ as plain SMS
66
66
  # when the recipient can't receive RCS (default true). Pass false to
67
- # fail with 422 +rcs_not_supported_for_recipient+ instead.
67
+ # fail with 422 +rcs_not_supported_for_recipient+ instead. WhatsApp:
68
+ # default false. With true, a message WhatsApp can't deliver (the
69
+ # 24-hour window is closed, WhatsApp refuses it, or reports it
70
+ # undeliverable later) goes as an SMS from the same number, billed as
71
+ # an SMS; +message.fallback+ ({Sendly::WhatsAppSmsFallback}) says what
72
+ # happened. The number must already be able to send SMS to the
73
+ # recipient (US and Canada), or the API answers 403
74
+ # +sms_fallback_unavailable+; marketing and authentication templates,
75
+ # and media without a caption, get 422 +sms_fallback_not_supported+.
68
76
  # @param idempotency_key [String] Idempotency key for this operation
69
77
  # (1-255 printable ASCII characters). The SDK already generates a key
70
78
  # per logical request automatically, so the server can dedupe the
@@ -130,6 +138,17 @@ module Sendly
130
138
  # puts message.whatsapp.kind # "template"
131
139
  # puts message.credits_used # priced by country + category
132
140
  #
141
+ # @example WhatsApp with an SMS fallback
142
+ # message = client.messages.send(
143
+ # channel: "whatsapp",
144
+ # to: "+15551234567",
145
+ # from: "+15559876543",
146
+ # text: "Your table is ready!",
147
+ # fallback_to_sms: true
148
+ # )
149
+ # puts message.fallback&.status # "sent" when it went as an SMS
150
+ # puts message.fell_back?
151
+ #
133
152
  # @example RCS text with suggested replies and actions
134
153
  # message = client.messages.send(
135
154
  # channel: "rcs",
@@ -189,6 +208,7 @@ module Sendly
189
208
  body[:mediaUrls] = media_urls if has_media
190
209
  body[:template] = template if template
191
210
  body[:metadata] = metadata if metadata
211
+ body[:fallbackToSms] = fallback_to_sms unless fallback_to_sms.nil?
192
212
 
193
213
  response = client.post("/messages", body, idempotency_key: idempotency_key)
194
214
  return WhatsAppMessage.new(response)
data/lib/sendly/types.rb CHANGED
@@ -69,6 +69,14 @@ module Sendly
69
69
  # @return [String] "sms", "mms", "rcs" or "whatsapp"; "sms" when the response does not say
70
70
  attr_reader :message_format
71
71
 
72
+ # @return [Sendly::WhatsAppSmsFallback, nil] On a WhatsApp message sent
73
+ # with +fallback_to_sms+, what happened to its SMS fallback
74
+ attr_reader :fallback
75
+
76
+ # @return [String, nil] On an SMS fallback, the id of the WhatsApp
77
+ # message it replaced
78
+ attr_reader :fallback_of
79
+
72
80
  # Message status constants (sending removed - doesn't exist in database)
73
81
  STATUSES = %w[queued sent delivered failed bounced retrying].freeze
74
82
 
@@ -98,6 +106,8 @@ module Sendly
98
106
  @ai_metadata = data["aiMetadata"]
99
107
  @media_urls = data["mediaUrls"] || data["media_urls"] || []
100
108
  @message_format = data["messageFormat"] || data["message_format"] || "sms"
109
+ @fallback = data["fallback"].is_a?(Hash) ? WhatsAppSmsFallback.new(data["fallback"]) : nil
110
+ @fallback_of = data["fallbackOf"] || data["fallback_of"]
101
111
  end
102
112
 
103
113
  # Check if message was delivered
@@ -143,7 +153,9 @@ module Sendly
143
153
  metadata: metadata,
144
154
  ai_metadata: ai_metadata,
145
155
  media_urls: media_urls,
146
- message_format: message_format
156
+ message_format: message_format,
157
+ fallback: fallback&.to_h,
158
+ fallback_of: fallback_of
147
159
  }.compact
148
160
  end
149
161
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "4.3.0"
4
+ VERSION = "4.4.0"
5
5
  end
@@ -103,6 +103,11 @@ module Sendly
103
103
  EVENT_SHORT_CODE_REJECTED = "short_code.rejected"
104
104
  EVENT_SHORT_CODE_FILED = "short_code.filed"
105
105
  EVENT_SHORT_CODE_LIVE = "short_code.live"
106
+ EVENT_SHORT_CODE_SUSPENDED = "short_code.suspended"
107
+ EVENT_SHORT_CODE_REACTIVATED = "short_code.reactivated"
108
+ EVENT_SHORT_CODE_CANCELLED = "short_code.cancelled"
109
+ EVENT_SHORT_CODE_PAYMENT_SUCCEEDED = "short_code.payment_succeeded"
110
+ EVENT_SHORT_CODE_PAYMENT_FAILED = "short_code.payment_failed"
106
111
 
107
112
  # Source of a list-health event. Frozen enum — new values will be
108
113
  # added in minor SDK versions, never removed.
@@ -420,6 +420,51 @@ module Sendly
420
420
  end
421
421
  end
422
422
 
423
+ # The SMS fallback of a WhatsApp message sent with +fallback_to_sms: true+.
424
+ # +status+ is +"pending"+ until WhatsApp's outcome is known, +"sent"+ when
425
+ # the SMS went, +"failed"+ when the SMS was refused or credits ran out,
426
+ # +"unavailable"+ when the SMS could no longer be sent from this number,
427
+ # and +"skipped"+ when the WhatsApp failure isn't one an SMS fixes.
428
+ # +reason+ is what made the message fall back (+"window_closed"+,
429
+ # +"whatsapp_refused"+ or +"whatsapp_failed"+). +message_id+ is the SMS
430
+ # message's id once it exists, +credits_used+ what the SMS cost, +error+
431
+ # why the SMS didn't go, in plain English, and +content_dropped+ the
432
+ # template parts with no SMS form, such as +"quick_replies"+. Values the
433
+ # API adds later come through unchanged.
434
+ class WhatsAppSmsFallback
435
+ attr_reader :channel, :status, :reason, :message_id, :credits_used,
436
+ :error, :content_dropped
437
+
438
+ STATUSES = %w[pending sent failed unavailable skipped].freeze
439
+ REASONS = %w[window_closed whatsapp_refused whatsapp_failed].freeze
440
+
441
+ def initialize(data)
442
+ @channel = data["channel"] || "sms"
443
+ @status = data["status"]
444
+ @reason = data["reason"]
445
+ @message_id = data["messageId"] || data["message_id"]
446
+ @credits_used = data["creditsUsed"] || data["credits_used"]
447
+ @error = data["error"]
448
+ @content_dropped = data["contentDropped"] || data["content_dropped"]
449
+ end
450
+
451
+ def pending?
452
+ status == "pending"
453
+ end
454
+
455
+ def sent?
456
+ status == "sent"
457
+ end
458
+
459
+ def to_h
460
+ {
461
+ channel: channel, status: status, reason: reason,
462
+ message_id: message_id, credits_used: credits_used, error: error,
463
+ content_dropped: content_dropped
464
+ }.compact
465
+ end
466
+ end
467
+
423
468
  # A sent WhatsApp message. Unlike {Message}, +segments+ is always 1
424
469
  # (WhatsApp has no segment concept), +text+ is the caption for a media
425
470
  # send (pass it as +text+ with +media_urls+) and nil for template sends
@@ -431,10 +476,14 @@ module Sendly
431
476
  # utility price of 12 credits. Templates are priced by category and
432
477
  # destination country; countries without a listed price use 33
433
478
  # (marketing), 12 (utility) and 12 (authentication) credits. A failed
434
- # send gives its slot back.
479
+ # send gives its slot back. +fallback+ is a {WhatsAppSmsFallback} when the
480
+ # send asked for +fallback_to_sms+: when WhatsApp couldn't take the
481
+ # message and it went as an SMS instead, +status+ is +"failed"+ and
482
+ # +fallback.status+ is +"sent"+ (check with {#fell_back?}).
435
483
  class WhatsAppMessage
436
484
  attr_reader :id, :channel, :message_format, :to, :from, :text, :status,
437
- :segments, :credits_used, :whatsapp, :created_at, :metadata
485
+ :segments, :credits_used, :whatsapp, :created_at, :metadata,
486
+ :fallback
438
487
 
439
488
  def initialize(data)
440
489
  @id = data["id"]
@@ -449,6 +498,11 @@ module Sendly
449
498
  @whatsapp = data["whatsapp"] ? WhatsAppMessageDetails.new(data["whatsapp"]) : nil
450
499
  @created_at = data["createdAt"] || data["created_at"]
451
500
  @metadata = data["metadata"]
501
+ @fallback = data["fallback"].is_a?(Hash) ? WhatsAppSmsFallback.new(data["fallback"]) : nil
502
+ end
503
+
504
+ def fell_back?
505
+ fallback&.sent? || false
452
506
  end
453
507
 
454
508
  def delivered?
@@ -464,7 +518,7 @@ module Sendly
464
518
  id: id, channel: channel, message_format: message_format,
465
519
  to: to, from: from, text: text, status: status, segments: segments,
466
520
  credits_used: credits_used, whatsapp: whatsapp&.to_h,
467
- created_at: created_at, metadata: metadata
521
+ created_at: created_at, metadata: metadata, fallback: fallback&.to_h
468
522
  }.compact
469
523
  end
470
524
  end
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: 4.3.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sendly
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-02 00:00:00.000000000 Z
11
+ date: 2026-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -159,7 +159,7 @@ metadata:
159
159
  source_code_uri: https://github.com/SendlyHQ/sendly-ruby
160
160
  changelog_uri: https://github.com/SendlyHQ/sendly-ruby/blob/main/CHANGELOG.md
161
161
  documentation_uri: https://sendly.live/docs
162
- post_install_message:
162
+ post_install_message:
163
163
  rdoc_options: []
164
164
  require_paths:
165
165
  - lib
@@ -174,8 +174,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
174
  - !ruby/object:Gem::Version
175
175
  version: '0'
176
176
  requirements: []
177
- rubygems_version: 3.3.5
178
- signing_key:
177
+ rubygems_version: 3.5.22
178
+ signing_key:
179
179
  specification_version: 4
180
180
  summary: Official Ruby SDK for the Sendly SMS API
181
181
  test_files: []