sendly 4.3.0 → 4.5.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: 511e36e32d4475552b4000a026856bde62c06bd7f43a52b59646c347d7ff4b0c
4
+ data.tar.gz: 92531f77aa366320d2887a3b9a0ac9793c6562d507f3863577352830097f4d32
5
5
  SHA512:
6
- metadata.gz: bd2892234c3a4b5dbd721fe37a0b4ecc8693ea20440fcfa04d724ce9d6ad94fd6f42319aefa9148d3a05e801ac0f2db225096c1b23e7897e54625b406ef1115e
7
- data.tar.gz: 89476582a5b3026c6a07d54fb563425df47328d1ba5f00f5da9e17731dfe3e229ceb96b699efa219f9c9fcaa1907e7bc3e4174b5c52b6b3ce6f004553daead90
6
+ metadata.gz: 000020f0a21c87fb3f4b33ca84fad93e36a73abaa936ca602b22c3065e7fbc3d09b2c0fe62b02f2080dbab1f2b4d37d8ad60648d08e0a0d022a71bbcd64b1ee6
7
+ data.tar.gz: fbc1718f201afe7895d433d989bf41e7c0938fac6c029dca8380b2d88620ef56d65851a258b9d3e39ad683652023e8468ef534c8a459ce930fcf35cc6d07ddba
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # sendly (Ruby)
2
2
 
3
+ ## 4.5.0
4
+
5
+ ### Patch Changes
6
+
7
+ - **`Sendly::ValidationError#status_code` is the response's real status.** It was always 400, so a 422 (such as `whatsapp_window_closed` or `sms_fallback_not_supported`) reported 400. Code that compared `status_code` with 400 to spot a validation error should check the class (or `code`) instead. `ValidationError.new` takes an optional `status_code:`, 400 by default.
8
+ - Docs give 503, the status the API sends, instead of 502 for the WhatsApp and voice errors raised when WhatsApp or the carrier couldn't be reached: `whatsapp_send_failed` when the message never reached WhatsApp (not sent, and safe to send again with the same idempotency key), the WhatsApp signup, profile, conversational-components and calling errors, `voice_attach_failed` and `carrier_refused`. The API answers every 502 as a 503 with a `Sendly-Upstream-Status: 502` header. Nothing changes at runtime.
9
+
10
+ ## 4.4.0
11
+
12
+ ### Minor Changes
13
+
14
+ - **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`.
15
+
16
+ ### Patch Changes
17
+
18
+ - **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.
19
+
3
20
  ## 4.3.0
4
21
 
5
22
  ### 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.5.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
@@ -1153,11 +1153,11 @@ puts status.disabled?
1153
1153
 
1154
1154
  Connect a number you own to WhatsApp, create Meta-reviewed message
1155
1155
  templates, and send via `client.messages.send(channel: "whatsapp", ...)`.
1156
- Connecting is a one-time $19 setup (no monthly fee). The first number always
1156
+ Connecting is free (no monthly fee either). The first number always
1157
1157
  ends with a human step: the signup returns a connect URL a person must open in
1158
1158
  a browser and log in with Facebook to link their WhatsApp Business Account.
1159
1159
  More numbers can then be added to that account with a 6-digit code Meta sends
1160
- to the number, with no Facebook step (same $19 fee, refunded if it fails).
1160
+ to the number, with no Facebook step (also free).
1161
1161
 
1162
1162
  Sends go through `messages.send(channel: "whatsapp")` and need `sms:send`,
1163
1163
  not `whatsapp:write`. Reads (`signup.get`, templates, the window, senders and
@@ -1191,16 +1191,14 @@ gives its slot back. Note: Meta
1191
1191
  has paused marketing template delivery to US (+1) numbers.
1192
1192
 
1193
1193
  ```ruby
1194
- # 1. Connect a number ($19 one-time; a human must open the connect URL)
1194
+ # 1. Connect a number (free; a human must open the connect URL)
1195
1195
  signup = client.whatsapp.signup.create(phone_number: "+15125550123")
1196
1196
  puts "Have your user open: #{signup.connect_url}"
1197
1197
 
1198
1198
  # 2. Poll until active. After the Facebook step the signup is "registering"
1199
1199
  # while WhatsApp activates the number. Activation usually takes a few minutes
1200
1200
  # but can take hours. If it hasn't finished about 6 hours after the session
1201
- # began, the session fails with registration_timeout and the fee is refunded.
1202
- # If the connection fails, the $19 fee is refunded automatically; once a
1203
- # number has connected, a later disconnect gets nothing back.
1201
+ # began, the session fails with registration_timeout.
1204
1202
  status = client.whatsapp.signup.get(signup.id)
1205
1203
  puts status.failure_reasons if status.failed? # e.g. ["waba_mismatch"]
1206
1204
 
@@ -1257,7 +1255,7 @@ puts added.status # "verifying"
1257
1255
  # code has been submitted it is the newest code since the signup started, so
1258
1256
  # after a resend it still shows the earlier code until the new one arrives.
1259
1257
  # Once WhatsApp has checked a code, only a code that arrived after the last
1260
- # submission or resend is returned. A 502 whatsapp_verification_unavailable
1258
+ # submission or resend is returned. A 503 whatsapp_verification_unavailable
1261
1259
  # isn't counted, so the same unchecked code can come back, and submitting it
1262
1260
  # again is safe.
1263
1261
  code = client.whatsapp.signup.get(added.id).verification_code
@@ -1324,7 +1322,7 @@ WhatsApp errors map onto the usual classes:
1324
1322
  while WhatsApp connections are unavailable. Only signup returns it; no send
1325
1323
  does. Nothing is charged, the response carries a `Retry-After: 3600` header,
1326
1324
  and `e.response_body["retryAfter"]` (3600) says how many seconds to wait; the client
1327
- retries it like any 5xx before raising. After 5 failed, charged signups in
1325
+ retries it like any 5xx before raising. After 5 failed signups in
1328
1326
  24 hours it raises `Sendly::RateLimitError` for 429
1329
1327
  `whatsapp_signup_limit_reached`, which is not retried: try again the next
1330
1328
  day.
@@ -1332,10 +1330,13 @@ WhatsApp errors map onto the usual classes:
1332
1330
  `whatsapp_window_closed`); send a template instead.
1333
1331
  - `whatsapp_send_failed` is a `Sendly::ValidationError` (422) when WhatsApp
1334
1332
  refused the message, which is final (cached under the idempotency key and
1335
- replayed for 24 hours), and a `Sendly::ServerError` (502) when the message
1336
- provably never reached the carrier, so it was not sent and is safe to send
1337
- again; the 502 is never cached and the client retries it first under the
1338
- same idempotency key. Neither is charged.
1333
+ replayed for 24 hours), and a `Sendly::ServerError` (503) when the message
1334
+ provably never reached WhatsApp, so it was not sent and is safe to resend
1335
+ under the same idempotency key; the 503 is never cached and the client
1336
+ retries it automatically, up to 3 times under the same key, before raising.
1337
+ Neither is charged. With `fallback_to_sms: true`, a 422 means the SMS
1338
+ couldn't go either, and `e.response_body["fallback"]` says why; see
1339
+ [SMS fallback](#sms-fallback).
1339
1340
  - `whatsapp_send_unconfirmed` is a `Sendly::APIError` with `status_code` 409:
1340
1341
  the outcome is unknown. The message was marked failed and refunded but may
1341
1342
  still be delivered, so check before sending it again (it could arrive
@@ -1363,11 +1364,11 @@ WhatsApp errors map onto the usual classes:
1363
1364
  400 `display_name_required` and for 422 `whatsapp_verification_start_failed`
1364
1365
  (WhatsApp refused to verify the number; final), `Sendly::APIError` (409) for
1365
1366
  `whatsapp_signup_in_progress` and `whatsapp_already_enabled`, and
1366
- `Sendly::ServerError` for 502 `whatsapp_verification_start_failed` (WhatsApp
1367
+ `Sendly::ServerError` for 503 `whatsapp_verification_start_failed` (WhatsApp
1367
1368
  couldn't start verifying the number; start again). Either failure fails the
1368
- session and refunds its fee. With `business_account_id:` the client never
1369
- retries a 5xx, a timeout or a dropped connection, because each retry would
1370
- start a new charged session. A
1369
+ session. With `business_account_id:` the client never retries a 5xx, a
1370
+ timeout or a dropped connection, because each retry would start a new
1371
+ session. A
1371
1372
  Facebook signup for a number
1372
1373
  that is being added by code gets 409 `whatsapp_verification_in_progress`,
1373
1374
  with that session's id in `e.response_body["id"]`.
@@ -1375,9 +1376,9 @@ WhatsApp errors map onto the usual classes:
1375
1376
  `invalid_verification_code` (not 6 digits) and 422
1376
1377
  `whatsapp_verification_code_invalid` (a wrong code, with
1377
1378
  `e.response_body["attemptsRemaining"]`); `Sendly::APIError` (409) for
1378
- `whatsapp_verification_failed` (after 5 wrong codes the session fails and
1379
- the fee is refunded), `whatsapp_verification_busy` (try again) and
1380
- `signup_not_active`; and `Sendly::ServerError` for 502
1379
+ `whatsapp_verification_failed` (after 5 wrong codes the session fails),
1380
+ `whatsapp_verification_busy` (try again) and
1381
+ `signup_not_active`; and `Sendly::ServerError` for 503
1381
1382
  `whatsapp_verification_unavailable` (the attempt isn't counted) and
1382
1383
  `whatsapp_activation_pending` (the code was accepted but connecting didn't
1383
1384
  finish; check back shortly with `signup.get`). The client never retries a
@@ -1387,13 +1388,13 @@ WhatsApp errors map onto the usual classes:
1387
1388
  - `signup.resend` raises `Sendly::RateLimitError` for 429
1388
1389
  `whatsapp_verification_resend_too_soon` at once, with the seconds to wait in
1389
1390
  `e.retry_after`, and `whatsapp_verification_resend_failed` as a
1390
- `Sendly::ValidationError` (422) or a `Sendly::ServerError` (502).
1391
+ `Sendly::ValidationError` (422) or a `Sendly::ServerError` (503).
1391
1392
  - `senders.upload_profile_photo` raises `Sendly::ValidationError` for 400
1392
1393
  `whatsapp_profile_photo_invalid` (not a JPEG or PNG) and `file_required`,
1393
1394
  and `Sendly::APIError` with `status_code` 413 for
1394
1395
  `whatsapp_profile_photo_too_large`. The photo methods raise
1395
- `Sendly::ServerError` for 502 `whatsapp_profile_update_failed`, and the
1396
- conversational component methods for 502
1396
+ `Sendly::ServerError` for 503 `whatsapp_profile_update_failed`, and the
1397
+ conversational component methods for 503
1397
1398
  `whatsapp_conversational_components_fetch_failed` and
1398
1399
  `whatsapp_conversational_components_update_failed`; an invalid list is a
1399
1400
  `Sendly::ValidationError` (400 `invalid_request`) with a message naming the
@@ -1403,10 +1404,78 @@ WhatsApp errors map onto the usual classes:
1403
1404
  (switch voice on for the number first), `Sendly::ValidationError` for 422
1404
1405
  `whatsapp_calling_unavailable` (Meta only allows calling once the account may
1405
1406
  message at least 2,000 people a day and the display name is approved), and
1406
- `Sendly::ServerError` for 502 `whatsapp_calling_update_failed`.
1407
+ `Sendly::ServerError` for 503 `whatsapp_calling_update_failed`.
1407
1408
  - A sender method for a number that isn't connected raises
1408
1409
  `Sendly::NotFoundError` (404 `whatsapp_sender_not_connected`).
1409
1410
 
1411
+ ### SMS fallback
1412
+
1413
+ Pass `fallback_to_sms: true` and a WhatsApp message that can't be delivered
1414
+ goes as an SMS from the same number instead.
1415
+
1416
+ ```ruby
1417
+ begin
1418
+ message = client.messages.send(
1419
+ channel: "whatsapp",
1420
+ to: "+15555550100",
1421
+ from: "+15125550123",
1422
+ text: "Your table is ready!",
1423
+ fallback_to_sms: true
1424
+ )
1425
+ puts message.fallback.status # "pending", "sent", "failed", "unavailable" or "skipped"
1426
+ puts message.fallback.reason # "window_closed", "whatsapp_refused" or "whatsapp_failed"
1427
+ if message.fell_back? # the SMS went instead (message.status is "failed")
1428
+ sms = client.messages.get(message.fallback.message_id)
1429
+ puts sms.fallback_of # the WhatsApp message's id
1430
+ end
1431
+ rescue Sendly::Error => e
1432
+ body = e.response_body || {}
1433
+ case e.code
1434
+ when "sms_fallback_unavailable"
1435
+ # 403: this number can't send SMS to the recipient; nothing was sent
1436
+ when "sms_fallback_not_supported"
1437
+ puts body["reason"] # marketing_template, authentication_template or media_without_caption
1438
+ else
1439
+ raise unless body["fallback"]
1440
+ # 422 whatsapp_window_closed or whatsapp_send_failed: the SMS couldn't go
1441
+ # either. A closed window also carries the WhatsApp message's id.
1442
+ fallback = Sendly::WhatsAppSmsFallback.new(body["fallback"])
1443
+ puts "#{e.code} #{body["id"]} #{fallback.status}: #{fallback.error}"
1444
+ end
1445
+ end
1446
+ ```
1447
+
1448
+ - It falls back when the 24-hour window is closed (the SMS goes at once:
1449
+ free-form text and media go only within 24 hours of the customer's last
1450
+ message, templates any time), when WhatsApp refuses the message, or when
1451
+ WhatsApp reports it undeliverable later. Never after WhatsApp reports it
1452
+ delivered or read, and at most once per message.
1453
+ - The number must already be able to send SMS to the recipient (a verified
1454
+ toll-free number, or a local number on a 10DLC campaign, to a US or Canada
1455
+ number), or the send is refused with 403 `sms_fallback_unavailable`
1456
+ before anything is sent.
1457
+ - Marketing and authentication templates, and media without a caption, can't
1458
+ fall back: 422 `sms_fallback_not_supported`. A utility template falls
1459
+ back as its text header, body and footer with the variables filled in, and
1460
+ its URL and phone buttons as `label: target`; quick replies are dropped and
1461
+ listed in `fallback.content_dropped`.
1462
+ - Billing: the WhatsApp charge is refunded (a closed window is never charged)
1463
+ and the SMS is billed as an SMS. A message that fell back at once comes
1464
+ back with `status` `"failed"`, `credits_used` 0 and `fallback.status` `"sent"`; `fallback.credits_used` is
1465
+ what the SMS cost.
1466
+ - No SMS goes on a 409 `whatsapp_send_unconfirmed` or a 503
1467
+ `whatsapp_send_failed`. The status is `"unavailable"` when SMS rules stop it
1468
+ (the contact opted out, a promotional-looking text in the recipient's quiet
1469
+ hours, restricted content), with the reason in `fallback.error`, and
1470
+ `"skipped"` for a WhatsApp error an SMS can't fix.
1471
+ - There is no time-based fallback: a message WhatsApp accepted but hasn't
1472
+ delivered, for example to a phone that is off, stays `"pending"`, and no SMS
1473
+ goes unless WhatsApp later reports a failure.
1474
+ - `messages.get` and `messages.list` return the same fallback on the WhatsApp message, so check a
1475
+ pending one later. The SMS carries `fallback_of`, the WhatsApp message's id.
1476
+
1477
+ Guide: [Send WhatsApp with an SMS Fallback](https://sendly.live/docs/whatsapp/send-with-fallback).
1478
+
1410
1479
  ## RCS
1411
1480
 
1412
1481
  Send branded rich messages — text with suggested replies and actions, or
@@ -1750,8 +1819,8 @@ string), `invalid_voice_mode`, `agent_required`,
1750
1819
  the address could not be validated, and `e.response_body["suggested"]` holds
1751
1820
  a corrected address when one was found); `Sendly::APIError` with
1752
1821
  `status_code` 409 for `agent_disabled`, `agent_limit` (20 agents per
1753
- workspace) and `agent_in_use`; and `Sendly::ServerError` for 502
1754
- `voice_attach_failed` and `carrier_refused` and 503 `voice_unavailable`,
1822
+ workspace) and `agent_in_use`; and `Sendly::ServerError` for 503
1823
+ `voice_attach_failed`, `carrier_refused` and `voice_unavailable`,
1755
1824
  raised after the client has already retried the 5xx on its own. Not every
1756
1825
  `carrier_refused` is worth retrying: when the message says the number
1757
1826
  couldn't be found for emergency registration, retrying won't help, so
@@ -1770,12 +1839,12 @@ begin
1770
1839
  rescue Sendly::AuthenticationError => e
1771
1840
  puts "Invalid API key"
1772
1841
  rescue Sendly::RateLimitError => e
1773
- if e.response_body&.dig("error") == "too_many_failed_key_attempts"
1842
+ if e.code == "too_many_failed_key_attempts"
1774
1843
  # Repeated wrong API keys from this address locked it out for up to
1775
1844
  # 5 minutes. Fix the key; do not retry until the lockout ends.
1776
1845
  puts "Locked out for #{e.retry_after}s: check the API key"
1777
1846
  else
1778
- puts "Rate limited (#{e.response_body&.dig('error')}), retry after #{e.retry_after} seconds"
1847
+ puts "Rate limited (#{e.code}), retry after #{e.retry_after} seconds"
1779
1848
  end
1780
1849
  rescue Sendly::InsufficientCreditsError => e
1781
1850
  puts "Add more credits to your account"
@@ -1786,17 +1855,18 @@ rescue Sendly::NotFoundError => e
1786
1855
  rescue Sendly::NetworkError => e
1787
1856
  puts "Network error: #{e.message}"
1788
1857
  rescue Sendly::Error => e
1789
- puts "Error: #{e.message} (#{e.response_body&.dig('error') || e.code})"
1858
+ puts "Error: #{e.message} (#{e.code})"
1790
1859
  end
1791
1860
  ```
1792
1861
 
1793
1862
  Every error is a `Sendly::Error`, which carries `message`, `code`,
1794
1863
  `status_code`, `details` and `response_body` — the parsed JSON body of the
1795
1864
  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
1865
+ `code` is the API's own error code, such as `"whatsapp_window_closed"` or
1866
+ `"sms_fallback_unavailable"` (the body's `"error"`), so refusals with the same
1867
+ status can be told apart. When the body names no code, `code` is the SDK's
1868
+ name for the class (`"VALIDATION_ERROR"`, `"RATE_LIMIT_EXCEEDED"` and so on).
1869
+ Some refusals put more in that body than a message: a 409 `agent_in_use` lists the
1800
1870
  numbers under `"numbers"`, a 422 `invalid_address` carries a corrected
1801
1871
  address under `"suggested"`.
1802
1872
 
@@ -1809,7 +1879,7 @@ endpoint.
1809
1879
  | Class | Raised for |
1810
1880
  |-------|-----------|
1811
1881
  | `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"]` |
1882
+ | `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
1883
  | `Sendly::InsufficientCreditsError` | 402 |
1814
1884
  | `Sendly::NotFoundError` | 404, including a feature that is not enabled for you |
1815
1885
  | `Sendly::RateLimitError` | 429; `retry_after` in seconds when the API sent one |
@@ -2066,7 +2136,6 @@ client.enterprise.workspaces.list_keys("ws_xxx")
2066
2136
  # Enterprise-level account, credits and billing
2067
2137
  client.enterprise.get_account
2068
2138
  client.enterprise.credits.get
2069
- client.enterprise.credits.deposit(amount: 100_000, description: "Q4 top-up")
2070
2139
  client.enterprise.settings.get_auto_top_up
2071
2140
  client.enterprise.settings.update_auto_top_up(enabled: true, threshold: 1000, amount: 10_000)
2072
2141
  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, status_code: 400)
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: status_code)
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, status_code: status)
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
@@ -78,7 +86,7 @@ module Sendly
78
86
  # WhatsApp the 422 +whatsapp_send_failed+ when WhatsApp refused the
79
87
  # message (final, not retried, not charged; cached under the
80
88
  # idempotency key and replayed for 24 hours)
81
- # @raise [Sendly::ServerError] On WhatsApp, the 502 +whatsapp_send_failed+:
89
+ # @raise [Sendly::ServerError] On WhatsApp, the 503 +whatsapp_send_failed+:
82
90
  # the message provably never reached the carrier, so it was not sent
83
91
  # and is safe to send again. It isn't charged and is never cached, so
84
92
  # the client retries it like any 5xx first under the same idempotency
@@ -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.5.0"
5
5
  end
@@ -402,7 +402,7 @@ module Sendly
402
402
  # @raise [Sendly::NotFoundError] HTTP 404 +number_not_found+ / +agent_not_found+
403
403
  # @raise [Sendly::APIError] HTTP 409 +agent_disabled+ (the agent is
404
404
  # switched off), 403 +forbidden+ / +live_key_required+
405
- # @raise [Sendly::ServerError] HTTP 502 +voice_attach_failed+ (voice could
405
+ # @raise [Sendly::ServerError] HTTP 503 +voice_attach_failed+ (voice could
406
406
  # not be switched on; try again) or 503 +voice_unavailable+, after the
407
407
  # client's automatic retries. Either can follow a mode alone on a number
408
408
  # whose voice is off.
@@ -454,7 +454,7 @@ module Sendly
454
454
  # 422 +invalid_address+ when the address could not be validated, with a
455
455
  # corrected address (or +nil+) in +e.response_body["suggested"]+
456
456
  # @raise [Sendly::NotFoundError] HTTP 404 +number_not_found+
457
- # @raise [Sendly::ServerError] HTTP 502 +carrier_refused+ when the
457
+ # @raise [Sendly::ServerError] HTTP 503 +carrier_refused+ when the
458
458
  # registration was refused, raised after the client has already retried
459
459
  # the 5xx on its own. When the message says the number couldn't be
460
460
  # found for emergency registration, retrying won't help: contact
@@ -691,7 +691,7 @@ module Sendly
691
691
  # +agent_disabled+ / +agent_limit+ / +agent_in_use+ (409) and +forbidden+
692
692
  # / +live_key_required+ / +insufficient_permissions+ (403); and
693
693
  # {Sendly::ServerError} (not an +APIError+) for +voice_attach_failed+ /
694
- # +carrier_refused+ (502), +voice_unavailable+ (503) and
694
+ # +carrier_refused+ / +voice_unavailable+ (503) and
695
695
  # +voice_internal_error+ (500). Every API error keeps the parsed body on
696
696
  # {Sendly::Error#response_body}. The full list is {Call::ERROR_CODES}.
697
697
  #
@@ -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.
@@ -63,8 +63,8 @@ module Sendly
63
63
  # The status of a WhatsApp signup. +status+ is +"initiated"+,
64
64
  # +"registering"+ (WhatsApp is activating the number; activation usually
65
65
  # takes a few minutes but can take hours, and a session that hasn't
66
- # finished about 6 hours after it began fails with +registration_timeout+
67
- # and the fee is refunded), +"verifying"+ (a number being added to an
66
+ # finished about 6 hours after it began fails with +registration_timeout+),
67
+ # +"verifying"+ (a number being added to an
68
68
  # already-connected account, waiting for its code), +"active"+ or
69
69
  # +"failed"+. The API does not send +"expired"+,
70
70
  # so {#expired?} is never true; it stays for compatibility.
@@ -72,7 +72,8 @@ module Sendly
72
72
  # +"active"+ (from the start for a number added by code); it is nil while
73
73
  # +"initiated"+ or +"registering"+ and after a failure.
74
74
  # +failure_reasons+ is set when the status is +"failed"+, with one code:
75
- # +setup_fee_payment_failed+, +signup_abandoned+, +meta_exchange_failed+,
75
+ # +setup_fee_payment_failed+ (the connection couldn't be started),
76
+ # +signup_abandoned+, +meta_exchange_failed+,
76
77
  # +registration_failed+, +waba_already_connected+, +waba_mismatch+ (the
77
78
  # WhatsApp Business Account chosen in the Facebook step doesn't hold the
78
79
  # verified number), +registration_timeout+ (activation hadn't finished
@@ -80,7 +81,6 @@ module Sendly
80
81
  # +verification_start_failed+ (WhatsApp couldn't start verifying an added
81
82
  # number), +verification_failed+ (too many wrong codes) or
82
83
  # +verification_expired+ (a verifying session left untouched for an hour).
83
- # If the connection fails, the $19 fee is refunded automatically.
84
84
  #
85
85
  # While the status is +"verifying"+, +verification_method+ (+"sms"+ or
86
86
  # +"voice"+) and +verification_attempts_remaining+ are set, and on
@@ -90,7 +90,7 @@ module Sendly
90
90
  # started, so after a resend it still shows the earlier code until the new
91
91
  # one arrives. Once WhatsApp has checked a code, only a code that arrived
92
92
  # after the last submission or resend is returned. A submission answered
93
- # with 502 +whatsapp_verification_unavailable+ is not counted, so the same
93
+ # with 503 +whatsapp_verification_unavailable+ is not counted, so the same
94
94
  # unchecked code can come back, and submitting it again with
95
95
  # {WhatsAppSignupResource#verify} is safe. All three are nil in any other
96
96
  # status.
@@ -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
@@ -479,34 +533,31 @@ module Sendly
479
533
 
480
534
  # Start connecting a number to WhatsApp.
481
535
  #
482
- # Charges a one-time $19 setup fee (no monthly fee) and returns a
536
+ # Connecting a number is free (no setup or monthly fee). Returns a
483
537
  # +connect_url+. Completing the connection requires a human: hand the
484
538
  # URL to your user — they open it in a browser and log in with Facebook
485
539
  # to link their WhatsApp Business Account. Then poll {#get} until the
486
540
  # status is +"active"+.
487
541
  #
488
542
  # Calling again for a number with an in-flight signup returns the
489
- # existing signup (same +connect_url+) without charging again. Requires
543
+ # existing signup (same +connect_url+). Requires
490
544
  # a live API key with the +whatsapp:write+ scope (a test key gets 403
491
545
  # +whatsapp_requires_live_key+) and, in a team workspace, an owner or
492
546
  # admin (+settings:write+). After the Facebook step the signup stays
493
547
  # +"registering"+ while WhatsApp activates the number. Activation usually
494
548
  # takes a few minutes but can take hours. If it hasn't finished about 6
495
549
  # hours after the session began, the session fails with
496
- # +registration_timeout+ and the fee is refunded. If the connection
497
- # fails, the $19 fee is refunded automatically; once the number has
498
- # connected there is no refund, and a later disconnect gets nothing
499
- # back.
550
+ # +registration_timeout+.
500
551
  #
501
552
  # To add a number to a WhatsApp Business Account this workspace has
502
553
  # already connected, pass its +business_account_id+ (as shown on
503
554
  # {WhatsAppSender#business_account_id}). There is no Facebook step and no
504
- # +connect_url+: the same $19 fee is charged, Meta sends the number a
555
+ # +connect_url+: it is free too, Meta sends the number a
505
556
  # 6-digit code by text or voice call, and the session is +"verifying"+
506
557
  # until you submit the code with {#verify}. Calling again for a number
507
- # that is verifying returns the same session without charging again or
508
- # sending a second code; a verifying session older than 3 hours is
509
- # expired (and refunded) and a new one is started.
558
+ # that is verifying returns the same session without sending a second
559
+ # code; a verifying session older than 3 hours is expired and a new one
560
+ # is started.
510
561
  #
511
562
  # @param phone_number [String] The number to connect, in E.164 format.
512
563
  # Must be an active number in your workspace.
@@ -521,20 +572,20 @@ module Sendly
521
572
  # +business_account_id+.
522
573
  # @return [WhatsAppSignupSession]
523
574
  # @raise [Sendly::ServerError] +whatsapp_unavailable+ (503) while
524
- # WhatsApp connections are unavailable. Nothing is charged. Only
575
+ # WhatsApp connections are unavailable. Only
525
576
  # signup returns it, with +retryAfter+ 3600 in the body
526
577
  # (+e.response_body["retryAfter"]+) and a +Retry-After: 3600+ header.
527
578
  # The client retries it like any 5xx before raising.
528
579
  # @raise [Sendly::RateLimitError] +whatsapp_signup_limit_reached+ (429)
529
- # after 5 failed, charged signups in 24 hours. Not retried; try again
530
- # the next day.
580
+ # after 5 failed signups (or 20 cancelled) in 24 hours. Not retried;
581
+ # try again the next day.
531
582
  # @raise [Sendly::NotFoundError] +whatsapp_business_account_not_found+
532
583
  # (404) when +business_account_id+ isn't connected in this workspace
533
584
  # with an active number.
534
585
  # @raise [Sendly::ValidationError] +display_name_required+ (400) when no
535
586
  # display name is given and the account has none to reuse, and
536
587
  # +whatsapp_verification_start_failed+ (422) when WhatsApp refused to
537
- # verify the number: final, the session failed and the fee is refunded.
588
+ # verify the number: final, the session failed.
538
589
  # @raise [Sendly::APIError] 409 +whatsapp_signup_in_progress+ (a Facebook
539
590
  # connection for the number is in flight; +e.response_body["id"]+ is
540
591
  # that session), 409 +whatsapp_verification_in_progress+ (a Facebook
@@ -543,14 +594,14 @@ module Sendly
543
594
  # still being added, so try again in a moment) or 409
544
595
  # +whatsapp_already_enabled+.
545
596
  # @raise [Sendly::InsufficientCreditsError] 402 +payment_method_required+
546
- # or +payment_failed+ when the $19 fee can't be charged.
547
- # @raise [Sendly::ServerError] +whatsapp_verification_start_failed+ (502)
548
- # when WhatsApp couldn't start verifying the number: the session failed
549
- # and its fee is refunded, so start again. With +business_account_id+,
550
- # a 5xx is raised at once and never retried by the client, because
551
- # each retry would start a new charged session (a timeout or dropped
552
- # connection is never retried either). Without it the client retries a
553
- # 5xx before raising, as before.
597
+ # or +payment_failed+: not returned while connecting a number is free.
598
+ # @raise [Sendly::ServerError] +whatsapp_verification_start_failed+ (503)
599
+ # when WhatsApp couldn't start verifying the number: the session failed,
600
+ # so start again. With +business_account_id+, a 5xx is raised at once
601
+ # and never retried by the client, because each retry would start a
602
+ # new session (a timeout or dropped connection is never retried
603
+ # either). Without it the client retries a 5xx before raising, as
604
+ # before.
554
605
  #
555
606
  # @example
556
607
  # signup = client.whatsapp.signup.create(phone_number: "+15559876543")
@@ -595,14 +646,14 @@ module Sendly
595
646
  # (422) for a wrong code, with the attempts left in
596
647
  # +e.response_body["attemptsRemaining"]+
597
648
  # @raise [Sendly::APIError] 409 +whatsapp_verification_failed+ after 5
598
- # wrong codes (the session failed, the fee is refunded and
649
+ # wrong codes (the session failed and
599
650
  # +whatsapp_account.failed+ fires), 409 +whatsapp_verification_busy+
600
651
  # while another code for the number is being checked (try again), or
601
652
  # 409 +signup_not_active+ when the signup isn't waiting for a code or is
602
653
  # older than 3 hours
603
654
  # @raise [Sendly::NotFoundError] +signup_not_found+ (404)
604
- # @raise [Sendly::ServerError] 502 +whatsapp_verification_unavailable+
605
- # (WhatsApp couldn't check the code; the attempt isn't counted) or 502
655
+ # @raise [Sendly::ServerError] 503 +whatsapp_verification_unavailable+
656
+ # (WhatsApp couldn't check the code; the attempt isn't counted) or 503
606
657
  # +whatsapp_activation_pending+ (the code was accepted but connecting
607
658
  # the number didn't finish; check back shortly with {#get}). A 5xx, a
608
659
  # timeout or a dropped connection is raised at once and never retried
@@ -641,7 +692,7 @@ module Sendly
641
692
  # waiting for a code or is older than 3 hours
642
693
  # @raise [Sendly::NotFoundError] +signup_not_found+ (404)
643
694
  # @raise [Sendly::ServerError] +whatsapp_verification_resend_failed+
644
- # (502) when WhatsApp couldn't be reached. The client retries a 5xx
695
+ # (503) when WhatsApp couldn't be reached. The client retries a 5xx
645
696
  # before raising.
646
697
  #
647
698
  # @example
@@ -793,7 +844,7 @@ module Sendly
793
844
  # @raise [Sendly::APIError] +whatsapp_profile_photo_too_large+ with
794
845
  # +status_code+ 413 when the photo is over 5 MB
795
846
  # @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
796
- # @raise [Sendly::ServerError] +whatsapp_profile_update_failed+ (502)
847
+ # @raise [Sendly::ServerError] +whatsapp_profile_update_failed+ (503)
797
848
  # when WhatsApp refused the image or couldn't be reached. A 5xx, a
798
849
  # timeout or a dropped connection is raised at once and never retried
799
850
  # by the client; check the image is
@@ -822,7 +873,7 @@ module Sendly
822
873
  # @return [WhatsAppSenderProfile] The profile, normally with
823
874
  # +profile_photo_url+ nil
824
875
  # @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
825
- # @raise [Sendly::ServerError] +whatsapp_profile_update_failed+ (502).
876
+ # @raise [Sendly::ServerError] +whatsapp_profile_update_failed+ (503).
826
877
  # The client retries a 5xx before raising.
827
878
  #
828
879
  # @example
@@ -845,7 +896,7 @@ module Sendly
845
896
  # @return [WhatsAppConversationalComponents]
846
897
  # @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
847
898
  # @raise [Sendly::ServerError]
848
- # +whatsapp_conversational_components_fetch_failed+ (502). The client
899
+ # +whatsapp_conversational_components_fetch_failed+ (503). The client
849
900
  # retries a 5xx before raising.
850
901
  #
851
902
  # @example
@@ -879,7 +930,7 @@ module Sendly
879
930
  # +invalid_request+ (400) with a message naming the problem
880
931
  # @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
881
932
  # @raise [Sendly::ServerError]
882
- # +whatsapp_conversational_components_update_failed+ (502). The client
933
+ # +whatsapp_conversational_components_update_failed+ (503). The client
883
934
  # retries a 5xx before raising.
884
935
  #
885
936
  # @example
@@ -920,7 +971,7 @@ module Sendly
920
971
  # when Meta refused: calling needs the account at the 2,000
921
972
  # recipients a day messaging limit and an approved display name
922
973
  # @raise [Sendly::NotFoundError] +whatsapp_sender_not_connected+ (404)
923
- # @raise [Sendly::ServerError] +whatsapp_calling_update_failed+ (502).
974
+ # @raise [Sendly::ServerError] +whatsapp_calling_update_failed+ (503).
924
975
  # The client retries a 5xx before raising.
925
976
  #
926
977
  # @example
@@ -1099,7 +1150,7 @@ module Sendly
1099
1150
  # create Meta-reviewed message templates, and send via
1100
1151
  # +client.messages.send(channel: "whatsapp", ...)+.
1101
1152
  #
1102
- # Connecting a number is a one-time $19 setup (no monthly fee). The first
1153
+ # Connecting a number is free (no setup or monthly fee). The first
1103
1154
  # number always ends with a human step: {WhatsAppSignupResource#create}
1104
1155
  # returns a +connect_url+ that a person must open in a browser and log in
1105
1156
  # with Facebook to link their WhatsApp Business Account. More numbers can
@@ -1141,7 +1192,7 @@ module Sendly
1141
1192
  # every method on this resource gets 404 +not_found+.
1142
1193
  #
1143
1194
  # @example Connect a number, create a template, then send
1144
- # # 1. Connect ($19 one-time; a human must open the connect URL)
1195
+ # # 1. Connect (free; a human must open the connect URL)
1145
1196
  # signup = client.whatsapp.signup.create(phone_number: "+15559876543")
1146
1197
  # puts "Have your user open: #{signup.connect_url}"
1147
1198
  #
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.5.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-18 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: []