sendly 4.4.0__tar.gz → 4.5.0__tar.gz

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.
Files changed (72) hide show
  1. {sendly-4.4.0 → sendly-4.5.0}/CHANGELOG.md +11 -0
  2. {sendly-4.4.0 → sendly-4.5.0}/PKG-INFO +94 -24
  3. {sendly-4.4.0 → sendly-4.5.0}/README.md +93 -23
  4. {sendly-4.4.0 → sendly-4.5.0}/pyproject.toml +1 -1
  5. {sendly-4.4.0 → sendly-4.5.0}/sendly/__init__.py +1 -1
  6. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/messages.py +9 -4
  7. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/whatsapp.py +28 -33
  8. {sendly-4.4.0 → sendly-4.5.0}/sendly/types.py +7 -2
  9. {sendly-4.4.0 → sendly-4.5.0}/sendly/utils/http.py +1 -1
  10. {sendly-4.4.0 → sendly-4.5.0}/tests/test_whatsapp.py +15 -0
  11. {sendly-4.4.0 → sendly-4.5.0}/tests/test_whatsapp_extras.py +2 -4
  12. {sendly-4.4.0 → sendly-4.5.0}/.github/header.svg +0 -0
  13. {sendly-4.4.0 → sendly-4.5.0}/.gitignore +0 -0
  14. {sendly-4.4.0 → sendly-4.5.0}/examples/async_example.py +0 -0
  15. {sendly-4.4.0 → sendly-4.5.0}/examples/error_handling.py +0 -0
  16. {sendly-4.4.0 → sendly-4.5.0}/examples/send_sms.py +0 -0
  17. {sendly-4.4.0 → sendly-4.5.0}/sendly/client.py +0 -0
  18. {sendly-4.4.0 → sendly-4.5.0}/sendly/errors.py +0 -0
  19. {sendly-4.4.0 → sendly-4.5.0}/sendly/py.typed +0 -0
  20. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/__init__.py +0 -0
  21. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/account.py +0 -0
  22. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/business_upgrade.py +0 -0
  23. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/calls.py +0 -0
  24. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/campaigns.py +0 -0
  25. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/contacts.py +0 -0
  26. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/conversations.py +0 -0
  27. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/drafts.py +0 -0
  28. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/enterprise.py +0 -0
  29. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/labels.py +0 -0
  30. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/links.py +0 -0
  31. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/media.py +0 -0
  32. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/numbers.py +0 -0
  33. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/rcs.py +0 -0
  34. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/rules.py +0 -0
  35. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/templates.py +0 -0
  36. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/tendlc.py +0 -0
  37. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/verify.py +0 -0
  38. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/voice.py +0 -0
  39. {sendly-4.4.0 → sendly-4.5.0}/sendly/resources/webhooks.py +0 -0
  40. {sendly-4.4.0 → sendly-4.5.0}/sendly/utils/__init__.py +0 -0
  41. {sendly-4.4.0 → sendly-4.5.0}/sendly/utils/validation.py +0 -0
  42. {sendly-4.4.0 → sendly-4.5.0}/sendly/webhooks.py +0 -0
  43. {sendly-4.4.0 → sendly-4.5.0}/tests/conftest.py +0 -0
  44. {sendly-4.4.0 → sendly-4.5.0}/tests/test_account.py +0 -0
  45. {sendly-4.4.0 → sendly-4.5.0}/tests/test_async_client.py +0 -0
  46. {sendly-4.4.0 → sendly-4.5.0}/tests/test_calls.py +0 -0
  47. {sendly-4.4.0 → sendly-4.5.0}/tests/test_campaigns.py +0 -0
  48. {sendly-4.4.0 → sendly-4.5.0}/tests/test_client.py +0 -0
  49. {sendly-4.4.0 → sendly-4.5.0}/tests/test_contacts.py +0 -0
  50. {sendly-4.4.0 → sendly-4.5.0}/tests/test_conversations.py +0 -0
  51. {sendly-4.4.0 → sendly-4.5.0}/tests/test_enterprise.py +0 -0
  52. {sendly-4.4.0 → sendly-4.5.0}/tests/test_errors.py +0 -0
  53. {sendly-4.4.0 → sendly-4.5.0}/tests/test_http_body_kwarg.py +0 -0
  54. {sendly-4.4.0 → sendly-4.5.0}/tests/test_http_retry.py +0 -0
  55. {sendly-4.4.0 → sendly-4.5.0}/tests/test_idempotency.py +0 -0
  56. {sendly-4.4.0 → sendly-4.5.0}/tests/test_media.py +0 -0
  57. {sendly-4.4.0 → sendly-4.5.0}/tests/test_messages.py +0 -0
  58. {sendly-4.4.0 → sendly-4.5.0}/tests/test_messages.py.bak +0 -0
  59. {sendly-4.4.0 → sendly-4.5.0}/tests/test_messages_batch.py +0 -0
  60. {sendly-4.4.0 → sendly-4.5.0}/tests/test_messages_schedule.py +0 -0
  61. {sendly-4.4.0 → sendly-4.5.0}/tests/test_messages_wire.py +0 -0
  62. {sendly-4.4.0 → sendly-4.5.0}/tests/test_numbers.py +0 -0
  63. {sendly-4.4.0 → sendly-4.5.0}/tests/test_path_ids.py +0 -0
  64. {sendly-4.4.0 → sendly-4.5.0}/tests/test_rcs.py +0 -0
  65. {sendly-4.4.0 → sendly-4.5.0}/tests/test_rcs_registration.py +0 -0
  66. {sendly-4.4.0 → sendly-4.5.0}/tests/test_rules.py +0 -0
  67. {sendly-4.4.0 → sendly-4.5.0}/tests/test_templates.py +0 -0
  68. {sendly-4.4.0 → sendly-4.5.0}/tests/test_tendlc.py +0 -0
  69. {sendly-4.4.0 → sendly-4.5.0}/tests/test_validation.py +0 -0
  70. {sendly-4.4.0 → sendly-4.5.0}/tests/test_voice.py +0 -0
  71. {sendly-4.4.0 → sendly-4.5.0}/tests/test_webhooks.py +0 -0
  72. {sendly-4.4.0 → sendly-4.5.0}/tests/test_webhooks_resource.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # sendly (Python)
2
2
 
3
+ ## 4.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `WhatsAppMessage.fell_back` is `True` when the message went as an SMS instead (`fallback.status` is `"sent"`), matching Ruby's `fell_back?` and the other SDKs.
8
+
9
+ ### Patch Changes
10
+
11
+ - The async `messages.send` docstring said `fallback_to_sms` is RCS only; it documents the WhatsApp fallback too, like the sync one.
12
+ - 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.
13
+
3
14
  ## 4.4.0
4
15
 
5
16
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sendly
3
- Version: 4.4.0
3
+ Version: 4.5.0
4
4
  Summary: Official Sendly Python SDK for SMS messaging
5
5
  Project-URL: Homepage, https://sendly.live
6
6
  Project-URL: Documentation, https://sendly.live/docs
@@ -448,7 +448,7 @@ File uploads (`media.upload()`, `business_upgrade.start()` and `resubmit()`,
448
448
  make is the `too_many_concurrent_verifications` wait. `whatsapp.signup.verify()`
449
449
  and `whatsapp.signup.create()` with a `business_account_id` also raise a
450
450
  timeout, a network error or a `5xx` at once, because repeating them can use up
451
- a verification attempt or start a new charged signup; they still wait out a
451
+ a verification attempt or start a new signup; they still wait out a
452
452
  `429` the API never ran.
453
453
 
454
454
  After the last attempt the SDK raises the error it last saw (`TimeoutError`,
@@ -1355,7 +1355,7 @@ client.links.update(link.code, True)
1355
1355
 
1356
1356
  ## WhatsApp
1357
1357
 
1358
- Connect a number you own to WhatsApp ($19 one-time, no monthly fee), create
1358
+ Connect a number you own to WhatsApp (free to connect, no monthly fee), create
1359
1359
  Meta-reviewed message templates, and send on the `whatsapp` channel. Connecting
1360
1360
  the first number always ends with a human step: hand the `connect_url` to your
1361
1361
  user - they open it in a browser and log in with Facebook to link their
@@ -1387,10 +1387,8 @@ print(f'Open {signup.connect_url} and log in with Facebook')
1387
1387
  # After the Facebook step it stays 'registering' while WhatsApp activates the
1388
1388
  # number. Activation usually takes a few minutes but can take hours. If it
1389
1389
  # hasn't finished about 6 hours after the session began, the session fails
1390
- # with registration_timeout and the fee is refunded ('failed', with
1391
- # signup.failure_reasons explaining why). If the connection fails, the $19 fee
1392
- # is refunded automatically; once a number has connected, a later disconnect
1393
- # gets nothing back.
1390
+ # with registration_timeout ('failed', with signup.failure_reasons explaining
1391
+ # why).
1394
1392
 
1395
1393
  # 2. Create a template (Meta reviews it, usually 24-48h). category is required
1396
1394
  # (UTILITY, AUTHENTICATION or MARKETING) with no default, and an update can't
@@ -1474,7 +1472,7 @@ components = client.whatsapp.senders.get_conversational_components('+15555550123
1474
1472
  client.whatsapp.senders.set_calling('+15555550123', enabled=True)
1475
1473
 
1476
1474
  # Add another number to an account you already connected: no Facebook step.
1477
- # Same $19 fee, refunded if it fails. WhatsApp sends the number a 6-digit code
1475
+ # Free, like a Facebook connection. WhatsApp sends the number a 6-digit code
1478
1476
  senders = client.whatsapp.senders.list().senders
1479
1477
  account_id = next(
1480
1478
  s.business_account_id for s in senders if s.status == 'active' and s.business_account_id
@@ -1498,14 +1496,14 @@ Senders also carry `business_account_id` (None while `pending`),
1498
1496
  name on file), `calling_enabled`, and `outbound_calling_allowed`, which is
1499
1497
  False for +1, +20, +84 and +234 numbers. A signup that is `verifying` carries
1500
1498
  `verification_method` and `verification_attempts_remaining`; after 5 wrong
1501
- codes it fails with `verification_failed` and the fee is refunded. Calls carry
1499
+ codes it fails with `verification_failed`. Calls carry
1502
1500
  `channel` (`phone`, `whatsapp` or `browser`; see `CallChannel`).
1503
1501
 
1504
1502
  `verification_code` comes only from `signup.get()`. Until a code has been
1505
1503
  submitted, it is the newest code that has arrived since the signup started, so
1506
1504
  after a resend it still shows the earlier code until the new one arrives. Once
1507
1505
  WhatsApp has checked a code, only a code that arrived after the last
1508
- submission or resend is returned. A submission answered with 502
1506
+ submission or resend is returned. A submission answered with 503
1509
1507
  `whatsapp_verification_unavailable` is not counted, so the same unchecked code
1510
1508
  can come back, and submitting it again is safe.
1511
1509
 
@@ -1531,14 +1529,14 @@ Errors specific to WhatsApp:
1531
1529
  body carries `retryAfter: 3600` and the response a `Retry-After: 3600`
1532
1530
  header. The SDK retries it like any `5xx` before raising;
1533
1531
  `e.response.retry_after` says how many seconds to wait before trying again.
1534
- - `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed, charged signups in
1532
+ - `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed signups in
1535
1533
  24 hours. It is final and not retried; try again the next day.
1536
1534
  - `whatsapp_send_failed` on a send: HTTP 422 when WhatsApp refused the message,
1537
1535
  which is final (cached under the idempotency key and replayed for 24 hours),
1538
- or HTTP 502 when the message provably never reached the carrier, so it was
1539
- not sent and is safe to send again. A 502 is never cached, and the SDK
1540
- retries it under the same idempotency key. Either way the message was not
1541
- charged.
1536
+ or HTTP 503 when the message provably never reached WhatsApp, so it was not
1537
+ sent and is safe to resend under the same idempotency key. A 503 is never
1538
+ cached, and the SDK retries it automatically, up to 3 times under the same
1539
+ key, before raising. Either way the message was not charged.
1542
1540
  - `whatsapp_send_unconfirmed` (HTTP 409) on a send: the outcome is unknown.
1543
1541
  The message was marked failed and refunded but may still be delivered, so
1544
1542
  check before sending it again (it could arrive twice). It is not retried
@@ -1556,28 +1554,28 @@ Errors specific to WhatsApp:
1556
1554
  marketing template without an opt-out button only gets a warning.
1557
1555
  - Profile photo: `file_required` and `whatsapp_profile_photo_invalid` (HTTP
1558
1556
  400), `whatsapp_profile_photo_too_large` (HTTP 413),
1559
- `whatsapp_profile_update_failed` (HTTP 502). Conversational components:
1557
+ `whatsapp_profile_update_failed` (HTTP 503). Conversational components:
1560
1558
  `invalid_request` (HTTP 400) with the reason,
1561
1559
  `whatsapp_conversational_components_fetch_failed` and
1562
- `whatsapp_conversational_components_update_failed` (HTTP 502). Calling:
1560
+ `whatsapp_conversational_components_update_failed` (HTTP 503). Calling:
1563
1561
  `voice_not_enabled` (HTTP 409), `whatsapp_calling_unavailable` (HTTP 422;
1564
1562
  WhatsApp only allows calling once the account may message 2,000 people a
1565
1563
  day and the display name is approved), `whatsapp_calling_update_failed`
1566
- (HTTP 502).
1564
+ (HTTP 503).
1567
1565
  - Adding a number by code: `whatsapp_business_account_not_found` (HTTP 404),
1568
1566
  `display_name_required` (HTTP 400), `whatsapp_signup_in_progress` and
1569
1567
  `whatsapp_already_enabled` (HTTP 409), and
1570
- `whatsapp_verification_start_failed` (HTTP 422 refused, 502 unreachable; the
1571
- fee is refunded either way). A Facebook `signup.create()` for a number being
1568
+ `whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable). A
1569
+ Facebook `signup.create()` for a number being
1572
1570
  added by code gets `whatsapp_verification_in_progress` (HTTP 409).
1573
1571
  `verify()`: `invalid_verification_code` (HTTP 400, not 6 digits),
1574
1572
  `whatsapp_verification_code_invalid` (HTTP 422, with
1575
1573
  `e.response.attempts_remaining`), `whatsapp_verification_failed` (HTTP 409,
1576
1574
  too many wrong codes), `whatsapp_verification_busy` (HTTP 409, retry),
1577
1575
  `whatsapp_verification_unavailable` and `whatsapp_activation_pending` (HTTP
1578
- 502). `resend()`: `whatsapp_verification_resend_too_soon` (HTTP 429, wait
1576
+ 503). `resend()`: `whatsapp_verification_resend_too_soon` (HTTP 429, wait
1579
1577
  `e.response.retry_after` seconds) and `whatsapp_verification_resend_failed`
1580
- (HTTP 422 or 502). Both raise `signup_not_active` (HTTP 409) once the signup
1578
+ (HTTP 422 or 503). Both raise `signup_not_active` (HTTP 409) once the signup
1581
1579
  stops waiting for a code, and `signup_not_found` (HTTP 404). The SDK never
1582
1580
  retries a `5xx`, a timeout or a network error from `signup.create()` with a
1583
1581
  `business_account_id` or from `verify()`; it retries `resend()` like any
@@ -1585,6 +1583,78 @@ Errors specific to WhatsApp:
1585
1583
 
1586
1584
  `message.whatsapp.message_id` is `None` until the first delivery report lands.
1587
1585
 
1586
+ ### SMS fallback
1587
+
1588
+ Pass `fallback_to_sms=True` and a WhatsApp message that can't be delivered
1589
+ goes as an SMS from the same number instead.
1590
+
1591
+ ```python
1592
+ from sendly import Sendly, SendlyError, WhatsAppSmsFallback
1593
+
1594
+ client = Sendly('sk_live_v1_YOUR_API_KEY')
1595
+
1596
+ try:
1597
+ message = client.messages.send(
1598
+ channel='whatsapp',
1599
+ to='+15555550100',
1600
+ from_='+15555550123',
1601
+ text='Your table is ready!',
1602
+ fallback_to_sms=True,
1603
+ )
1604
+ print(message.fallback.status) # 'pending', 'sent', 'failed', 'unavailable' or 'skipped'
1605
+ print(message.fallback.reason) # 'window_closed', 'whatsapp_refused' or 'whatsapp_failed'
1606
+ if message.fell_back and message.fallback.message_id:
1607
+ # The SMS went instead (message.status is 'failed')
1608
+ sms = client.messages.get(message.fallback.message_id)
1609
+ print(sms.fallback_of) # the WhatsApp message's id
1610
+ except SendlyError as e:
1611
+ extra = (e.response.model_extra or {}) if e.response else {}
1612
+ if e.code == 'sms_fallback_unavailable':
1613
+ pass # 403: this number can't send SMS to the recipient; nothing was sent
1614
+ elif e.code == 'sms_fallback_not_supported':
1615
+ print(extra.get('reason')) # marketing_template, authentication_template or media_without_caption
1616
+ elif extra.get('fallback'):
1617
+ # 422 whatsapp_window_closed or whatsapp_send_failed: the SMS couldn't go
1618
+ # either. A closed window also carries the WhatsApp message's id.
1619
+ fallback = WhatsAppSmsFallback.model_validate(extra['fallback'])
1620
+ print(e.code, extra.get('id'), fallback.status, fallback.error)
1621
+ else:
1622
+ raise
1623
+ ```
1624
+
1625
+ - It falls back when the 24-hour window is closed (the SMS goes at once:
1626
+ free-form text and media go only within 24 hours of the customer's last
1627
+ message, templates any time), when WhatsApp refuses the message, or when
1628
+ WhatsApp reports it undeliverable later. Never after WhatsApp reports it
1629
+ delivered or read, and at most once per message.
1630
+ - The number must already be able to send SMS to the recipient (a verified
1631
+ toll-free number, or a local number on a 10DLC campaign, to a US or Canada
1632
+ number), or the send is refused with HTTP 403 `sms_fallback_unavailable`
1633
+ before anything is sent.
1634
+ - Marketing and authentication templates, and media without a caption, can't
1635
+ fall back: HTTP 422 `sms_fallback_not_supported`. A utility template falls
1636
+ back as its text header, body and footer with the variables filled in, and
1637
+ its URL and phone buttons as `label: target`; quick replies are dropped and
1638
+ listed in `fallback.content_dropped`.
1639
+ - Billing: the WhatsApp charge is refunded (a closed window is never charged)
1640
+ and the SMS is billed as an SMS. A message that fell back at once comes
1641
+ back with `status == 'failed'`, `credits_used == 0` and
1642
+ `fallback.status == 'sent'` (`message.fell_back` is `True`);
1643
+ `fallback.credits_used` is what the SMS cost.
1644
+ - No SMS goes on an HTTP 409 `whatsapp_send_unconfirmed` or an HTTP 503
1645
+ `whatsapp_send_failed`. `fallback.status` is `'unavailable'` when SMS rules
1646
+ stop it (the contact opted out, a promotional-looking text in the
1647
+ recipient's quiet hours, restricted content), with the reason in
1648
+ `fallback.error`, and `'skipped'` for a WhatsApp error an SMS can't fix.
1649
+ - There is no time-based fallback: a message WhatsApp accepted but hasn't
1650
+ delivered, for example to a phone that is off, stays `'pending'`, and no
1651
+ SMS goes unless WhatsApp later reports a failure.
1652
+ - `messages.get()` and `messages.list()` return the same `fallback` on the
1653
+ WhatsApp message, so check a pending one later. The SMS carries
1654
+ `fallback_of`, the WhatsApp message's id.
1655
+
1656
+ Guide: [Send WhatsApp with an SMS Fallback](https://sendly.live/docs/whatsapp/send-with-fallback).
1657
+
1588
1658
  ## RCS
1589
1659
 
1590
1660
  Send branded, verified-sender messages on Android: rich cards, suggestion
@@ -1946,8 +2016,8 @@ default, or `CA`) are optional.
1946
2016
 
1947
2017
  Refusals: `agent_required` (agent mode with no agent), `agent_not_found`,
1948
2018
  `agent_disabled` (turn the agent on first), `agent_limit`, `agent_in_use`,
1949
- `number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 502,
1950
- retry shortly), `carrier_refused` (HTTP 502, retry shortly unless the message
2019
+ `number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 503,
2020
+ retry shortly), `carrier_refused` (HTTP 503, retry shortly unless the message
1951
2021
  says the number couldn't be found for emergency registration: contact
1952
2022
  support), `voice_unavailable` (HTTP 503),
1953
2023
  `e911_not_applicable` (only US and Canadian numbers take an emergency
@@ -413,7 +413,7 @@ File uploads (`media.upload()`, `business_upgrade.start()` and `resubmit()`,
413
413
  make is the `too_many_concurrent_verifications` wait. `whatsapp.signup.verify()`
414
414
  and `whatsapp.signup.create()` with a `business_account_id` also raise a
415
415
  timeout, a network error or a `5xx` at once, because repeating them can use up
416
- a verification attempt or start a new charged signup; they still wait out a
416
+ a verification attempt or start a new signup; they still wait out a
417
417
  `429` the API never ran.
418
418
 
419
419
  After the last attempt the SDK raises the error it last saw (`TimeoutError`,
@@ -1320,7 +1320,7 @@ client.links.update(link.code, True)
1320
1320
 
1321
1321
  ## WhatsApp
1322
1322
 
1323
- Connect a number you own to WhatsApp ($19 one-time, no monthly fee), create
1323
+ Connect a number you own to WhatsApp (free to connect, no monthly fee), create
1324
1324
  Meta-reviewed message templates, and send on the `whatsapp` channel. Connecting
1325
1325
  the first number always ends with a human step: hand the `connect_url` to your
1326
1326
  user - they open it in a browser and log in with Facebook to link their
@@ -1352,10 +1352,8 @@ print(f'Open {signup.connect_url} and log in with Facebook')
1352
1352
  # After the Facebook step it stays 'registering' while WhatsApp activates the
1353
1353
  # number. Activation usually takes a few minutes but can take hours. If it
1354
1354
  # hasn't finished about 6 hours after the session began, the session fails
1355
- # with registration_timeout and the fee is refunded ('failed', with
1356
- # signup.failure_reasons explaining why). If the connection fails, the $19 fee
1357
- # is refunded automatically; once a number has connected, a later disconnect
1358
- # gets nothing back.
1355
+ # with registration_timeout ('failed', with signup.failure_reasons explaining
1356
+ # why).
1359
1357
 
1360
1358
  # 2. Create a template (Meta reviews it, usually 24-48h). category is required
1361
1359
  # (UTILITY, AUTHENTICATION or MARKETING) with no default, and an update can't
@@ -1439,7 +1437,7 @@ components = client.whatsapp.senders.get_conversational_components('+15555550123
1439
1437
  client.whatsapp.senders.set_calling('+15555550123', enabled=True)
1440
1438
 
1441
1439
  # Add another number to an account you already connected: no Facebook step.
1442
- # Same $19 fee, refunded if it fails. WhatsApp sends the number a 6-digit code
1440
+ # Free, like a Facebook connection. WhatsApp sends the number a 6-digit code
1443
1441
  senders = client.whatsapp.senders.list().senders
1444
1442
  account_id = next(
1445
1443
  s.business_account_id for s in senders if s.status == 'active' and s.business_account_id
@@ -1463,14 +1461,14 @@ Senders also carry `business_account_id` (None while `pending`),
1463
1461
  name on file), `calling_enabled`, and `outbound_calling_allowed`, which is
1464
1462
  False for +1, +20, +84 and +234 numbers. A signup that is `verifying` carries
1465
1463
  `verification_method` and `verification_attempts_remaining`; after 5 wrong
1466
- codes it fails with `verification_failed` and the fee is refunded. Calls carry
1464
+ codes it fails with `verification_failed`. Calls carry
1467
1465
  `channel` (`phone`, `whatsapp` or `browser`; see `CallChannel`).
1468
1466
 
1469
1467
  `verification_code` comes only from `signup.get()`. Until a code has been
1470
1468
  submitted, it is the newest code that has arrived since the signup started, so
1471
1469
  after a resend it still shows the earlier code until the new one arrives. Once
1472
1470
  WhatsApp has checked a code, only a code that arrived after the last
1473
- submission or resend is returned. A submission answered with 502
1471
+ submission or resend is returned. A submission answered with 503
1474
1472
  `whatsapp_verification_unavailable` is not counted, so the same unchecked code
1475
1473
  can come back, and submitting it again is safe.
1476
1474
 
@@ -1496,14 +1494,14 @@ Errors specific to WhatsApp:
1496
1494
  body carries `retryAfter: 3600` and the response a `Retry-After: 3600`
1497
1495
  header. The SDK retries it like any `5xx` before raising;
1498
1496
  `e.response.retry_after` says how many seconds to wait before trying again.
1499
- - `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed, charged signups in
1497
+ - `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed signups in
1500
1498
  24 hours. It is final and not retried; try again the next day.
1501
1499
  - `whatsapp_send_failed` on a send: HTTP 422 when WhatsApp refused the message,
1502
1500
  which is final (cached under the idempotency key and replayed for 24 hours),
1503
- or HTTP 502 when the message provably never reached the carrier, so it was
1504
- not sent and is safe to send again. A 502 is never cached, and the SDK
1505
- retries it under the same idempotency key. Either way the message was not
1506
- charged.
1501
+ or HTTP 503 when the message provably never reached WhatsApp, so it was not
1502
+ sent and is safe to resend under the same idempotency key. A 503 is never
1503
+ cached, and the SDK retries it automatically, up to 3 times under the same
1504
+ key, before raising. Either way the message was not charged.
1507
1505
  - `whatsapp_send_unconfirmed` (HTTP 409) on a send: the outcome is unknown.
1508
1506
  The message was marked failed and refunded but may still be delivered, so
1509
1507
  check before sending it again (it could arrive twice). It is not retried
@@ -1521,28 +1519,28 @@ Errors specific to WhatsApp:
1521
1519
  marketing template without an opt-out button only gets a warning.
1522
1520
  - Profile photo: `file_required` and `whatsapp_profile_photo_invalid` (HTTP
1523
1521
  400), `whatsapp_profile_photo_too_large` (HTTP 413),
1524
- `whatsapp_profile_update_failed` (HTTP 502). Conversational components:
1522
+ `whatsapp_profile_update_failed` (HTTP 503). Conversational components:
1525
1523
  `invalid_request` (HTTP 400) with the reason,
1526
1524
  `whatsapp_conversational_components_fetch_failed` and
1527
- `whatsapp_conversational_components_update_failed` (HTTP 502). Calling:
1525
+ `whatsapp_conversational_components_update_failed` (HTTP 503). Calling:
1528
1526
  `voice_not_enabled` (HTTP 409), `whatsapp_calling_unavailable` (HTTP 422;
1529
1527
  WhatsApp only allows calling once the account may message 2,000 people a
1530
1528
  day and the display name is approved), `whatsapp_calling_update_failed`
1531
- (HTTP 502).
1529
+ (HTTP 503).
1532
1530
  - Adding a number by code: `whatsapp_business_account_not_found` (HTTP 404),
1533
1531
  `display_name_required` (HTTP 400), `whatsapp_signup_in_progress` and
1534
1532
  `whatsapp_already_enabled` (HTTP 409), and
1535
- `whatsapp_verification_start_failed` (HTTP 422 refused, 502 unreachable; the
1536
- fee is refunded either way). A Facebook `signup.create()` for a number being
1533
+ `whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable). A
1534
+ Facebook `signup.create()` for a number being
1537
1535
  added by code gets `whatsapp_verification_in_progress` (HTTP 409).
1538
1536
  `verify()`: `invalid_verification_code` (HTTP 400, not 6 digits),
1539
1537
  `whatsapp_verification_code_invalid` (HTTP 422, with
1540
1538
  `e.response.attempts_remaining`), `whatsapp_verification_failed` (HTTP 409,
1541
1539
  too many wrong codes), `whatsapp_verification_busy` (HTTP 409, retry),
1542
1540
  `whatsapp_verification_unavailable` and `whatsapp_activation_pending` (HTTP
1543
- 502). `resend()`: `whatsapp_verification_resend_too_soon` (HTTP 429, wait
1541
+ 503). `resend()`: `whatsapp_verification_resend_too_soon` (HTTP 429, wait
1544
1542
  `e.response.retry_after` seconds) and `whatsapp_verification_resend_failed`
1545
- (HTTP 422 or 502). Both raise `signup_not_active` (HTTP 409) once the signup
1543
+ (HTTP 422 or 503). Both raise `signup_not_active` (HTTP 409) once the signup
1546
1544
  stops waiting for a code, and `signup_not_found` (HTTP 404). The SDK never
1547
1545
  retries a `5xx`, a timeout or a network error from `signup.create()` with a
1548
1546
  `business_account_id` or from `verify()`; it retries `resend()` like any
@@ -1550,6 +1548,78 @@ Errors specific to WhatsApp:
1550
1548
 
1551
1549
  `message.whatsapp.message_id` is `None` until the first delivery report lands.
1552
1550
 
1551
+ ### SMS fallback
1552
+
1553
+ Pass `fallback_to_sms=True` and a WhatsApp message that can't be delivered
1554
+ goes as an SMS from the same number instead.
1555
+
1556
+ ```python
1557
+ from sendly import Sendly, SendlyError, WhatsAppSmsFallback
1558
+
1559
+ client = Sendly('sk_live_v1_YOUR_API_KEY')
1560
+
1561
+ try:
1562
+ message = client.messages.send(
1563
+ channel='whatsapp',
1564
+ to='+15555550100',
1565
+ from_='+15555550123',
1566
+ text='Your table is ready!',
1567
+ fallback_to_sms=True,
1568
+ )
1569
+ print(message.fallback.status) # 'pending', 'sent', 'failed', 'unavailable' or 'skipped'
1570
+ print(message.fallback.reason) # 'window_closed', 'whatsapp_refused' or 'whatsapp_failed'
1571
+ if message.fell_back and message.fallback.message_id:
1572
+ # The SMS went instead (message.status is 'failed')
1573
+ sms = client.messages.get(message.fallback.message_id)
1574
+ print(sms.fallback_of) # the WhatsApp message's id
1575
+ except SendlyError as e:
1576
+ extra = (e.response.model_extra or {}) if e.response else {}
1577
+ if e.code == 'sms_fallback_unavailable':
1578
+ pass # 403: this number can't send SMS to the recipient; nothing was sent
1579
+ elif e.code == 'sms_fallback_not_supported':
1580
+ print(extra.get('reason')) # marketing_template, authentication_template or media_without_caption
1581
+ elif extra.get('fallback'):
1582
+ # 422 whatsapp_window_closed or whatsapp_send_failed: the SMS couldn't go
1583
+ # either. A closed window also carries the WhatsApp message's id.
1584
+ fallback = WhatsAppSmsFallback.model_validate(extra['fallback'])
1585
+ print(e.code, extra.get('id'), fallback.status, fallback.error)
1586
+ else:
1587
+ raise
1588
+ ```
1589
+
1590
+ - It falls back when the 24-hour window is closed (the SMS goes at once:
1591
+ free-form text and media go only within 24 hours of the customer's last
1592
+ message, templates any time), when WhatsApp refuses the message, or when
1593
+ WhatsApp reports it undeliverable later. Never after WhatsApp reports it
1594
+ delivered or read, and at most once per message.
1595
+ - The number must already be able to send SMS to the recipient (a verified
1596
+ toll-free number, or a local number on a 10DLC campaign, to a US or Canada
1597
+ number), or the send is refused with HTTP 403 `sms_fallback_unavailable`
1598
+ before anything is sent.
1599
+ - Marketing and authentication templates, and media without a caption, can't
1600
+ fall back: HTTP 422 `sms_fallback_not_supported`. A utility template falls
1601
+ back as its text header, body and footer with the variables filled in, and
1602
+ its URL and phone buttons as `label: target`; quick replies are dropped and
1603
+ listed in `fallback.content_dropped`.
1604
+ - Billing: the WhatsApp charge is refunded (a closed window is never charged)
1605
+ and the SMS is billed as an SMS. A message that fell back at once comes
1606
+ back with `status == 'failed'`, `credits_used == 0` and
1607
+ `fallback.status == 'sent'` (`message.fell_back` is `True`);
1608
+ `fallback.credits_used` is what the SMS cost.
1609
+ - No SMS goes on an HTTP 409 `whatsapp_send_unconfirmed` or an HTTP 503
1610
+ `whatsapp_send_failed`. `fallback.status` is `'unavailable'` when SMS rules
1611
+ stop it (the contact opted out, a promotional-looking text in the
1612
+ recipient's quiet hours, restricted content), with the reason in
1613
+ `fallback.error`, and `'skipped'` for a WhatsApp error an SMS can't fix.
1614
+ - There is no time-based fallback: a message WhatsApp accepted but hasn't
1615
+ delivered, for example to a phone that is off, stays `'pending'`, and no
1616
+ SMS goes unless WhatsApp later reports a failure.
1617
+ - `messages.get()` and `messages.list()` return the same `fallback` on the
1618
+ WhatsApp message, so check a pending one later. The SMS carries
1619
+ `fallback_of`, the WhatsApp message's id.
1620
+
1621
+ Guide: [Send WhatsApp with an SMS Fallback](https://sendly.live/docs/whatsapp/send-with-fallback).
1622
+
1553
1623
  ## RCS
1554
1624
 
1555
1625
  Send branded, verified-sender messages on Android: rich cards, suggestion
@@ -1911,8 +1981,8 @@ default, or `CA`) are optional.
1911
1981
 
1912
1982
  Refusals: `agent_required` (agent mode with no agent), `agent_not_found`,
1913
1983
  `agent_disabled` (turn the agent on first), `agent_limit`, `agent_in_use`,
1914
- `number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 502,
1915
- retry shortly), `carrier_refused` (HTTP 502, retry shortly unless the message
1984
+ `number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 503,
1985
+ retry shortly), `carrier_refused` (HTTP 503, retry shortly unless the message
1916
1986
  says the number couldn't be found for emergency registration: contact
1917
1987
  support), `voice_unavailable` (HTTP 503),
1918
1988
  `e911_not_applicable` (only US and Canadian numbers take an emergency
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sendly"
7
- version = "4.4.0"
7
+ version = "4.5.0"
8
8
  description = "Official Sendly Python SDK for SMS messaging"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -29,7 +29,7 @@ Async Example:
29
29
  >>> asyncio.run(main())
30
30
  """
31
31
 
32
- __version__ = "4.4.0"
32
+ __version__ = "4.5.0"
33
33
 
34
34
  # Main clients
35
35
  from .client import AsyncSendly, Sendly
@@ -152,8 +152,8 @@ class MessagesResource:
152
152
  SendlyError: ``whatsapp_send_failed`` on WhatsApp: 422 when
153
153
  WhatsApp refused the message, which is final and not retried
154
154
  (cached under the idempotency key and replayed for 24 hours);
155
- 502 when the message provably never reached the carrier, so it
156
- was not sent and is safe to send again. A 502 is never cached,
155
+ 503 when the message provably never reached the carrier, so it
156
+ was not sent and is safe to send again. A 503 is never cached,
157
157
  so the SDK retries it like any 5xx under the same idempotency
158
158
  key. Either way the message wasn't charged. No send returns
159
159
  503 ``whatsapp_unavailable``.
@@ -1060,8 +1060,13 @@ class AsyncMessagesResource:
1060
1060
  agent_id: RCS only - the agent to send as
1061
1061
  card: RCS only - a rich card (title and description required)
1062
1062
  suggestions: RCS only - suggestion chips for a text message
1063
- fallback_to_sms: RCS only - deliver as SMS when the recipient
1064
- doesn't support RCS (default True)
1063
+ fallback_to_sms: RCS - deliver as SMS when the recipient
1064
+ doesn't support RCS (default True). Pass False to get a 422
1065
+ rcs_not_supported_for_recipient instead. WhatsApp - default
1066
+ False; True sends an SMS from the same number when WhatsApp
1067
+ can't deliver the message (see message.fallback). The number
1068
+ must be able to send SMS to the recipient, or the API answers
1069
+ 403 sms_fallback_unavailable
1065
1070
  idempotency_key: Optional key (1-255 printable ASCII characters)
1066
1071
  sent as the Idempotency-Key header - retrying with the same
1067
1072
  key returns the original result instead of sending again.
@@ -5,7 +5,7 @@ WhatsApp is a first-class Sendly channel: connect a number you own, create
5
5
  Meta-reviewed message templates, and send via
6
6
  ``client.messages.send(channel='whatsapp', ...)``.
7
7
 
8
- Connecting a number is a one-time $19 setup (no monthly fee) and always ends
8
+ Connecting a number is free (no setup or monthly fee) and always ends
9
9
  with a human step: ``signup.create()`` returns a ``connect_url`` that a person
10
10
  must open in a browser and log in with Facebook to link their WhatsApp
11
11
  Business Account. Hand the URL to your user - that connection cannot be
@@ -99,40 +99,35 @@ class WhatsAppSignupResource:
99
99
  ) -> Union[WhatsAppSignupSession, WhatsAppSignup]:
100
100
  """Start connecting a number to WhatsApp.
101
101
 
102
- Charges a one-time $19 setup fee (no monthly fee) and returns a
102
+ Connecting a number is free (no setup or monthly fee). Returns a
103
103
  ``connect_url``. Completing the connection requires a human: hand the
104
104
  URL to your user - they open it in a browser and log in with Facebook
105
105
  to link their WhatsApp Business Account. Then poll :meth:`get` until
106
106
  the status is ``active``.
107
107
 
108
108
  Calling again for a number with an in-flight signup returns the
109
- existing signup (same ``connect_url``) without charging again.
109
+ existing signup (same ``connect_url``).
110
110
  Requires a live API key with the ``whatsapp:write`` scope and, in a
111
111
  team workspace, an owner or admin (``settings:write``). After the
112
112
  Facebook step the signup stays ``registering`` while WhatsApp
113
113
  activates the number. Activation usually takes a few minutes but can
114
114
  take hours. If it hasn't finished about 6 hours after the session
115
- began, the session fails with ``registration_timeout`` and the fee is
116
- refunded. If the connection fails, the $19 fee is refunded
117
- automatically; once the number has connected there is no refund, and
118
- a later disconnect gets nothing back.
115
+ began, the session fails with ``registration_timeout``.
119
116
 
120
117
  Pass ``business_account_id`` to add the number to a WhatsApp
121
118
  Business Account this workspace has already connected, without the
122
- Facebook step. The same $19 fee applies (refunded automatically if the
123
- connection fails), then WhatsApp sends the number a 6-digit code by
124
- text or voice call. This returns a :class:`WhatsAppSignup` with status
119
+ Facebook step. It is free too: WhatsApp sends the number a 6-digit
120
+ code by text or voice call. This returns a :class:`WhatsAppSignup` with status
125
121
  ``verifying`` and no ``connect_url``: submit the code with
126
122
  :meth:`verify`, or read it from :meth:`get` as ``verification_code``
127
123
  once the text reaches the number. Calling again for a number that is
128
- already verifying returns that signup without a second charge or a
129
- second code, with two exceptions: if the first call stopped before
124
+ already verifying returns that signup without a second code, with
125
+ two exceptions: if the first call stopped before
130
126
  the code was requested, the repeat requests it (201); and a verifying
131
- signup more than 3 hours old is failed and refunded, and a new charged
132
- one is started. With ``business_account_id`` a 5xx, a timeout or a
127
+ signup more than 3 hours old is failed and a new one is started. With ``business_account_id`` a 5xx, a timeout or a
133
128
  network error is raised at once, never retried: each attempt can
134
- start a new signup that is charged and, when it fails, refunded. Only
135
- a 429 the API never ran is retried.
129
+ start a new signup and ask WhatsApp for another code. Only a 429 the
130
+ API never ran is retried.
136
131
 
137
132
  Args:
138
133
  phone_number: The number to connect, in E.164 format. Must be an
@@ -169,13 +164,13 @@ class WhatsAppSignupResource:
169
164
  lacks ``whatsapp:write`` or the role isn't owner or admin.
170
165
  SendlyError: ``whatsapp_requires_live_key`` (403) with a test key.
171
166
  SendlyError: ``whatsapp_unavailable`` (503) while WhatsApp
172
- connections are unavailable. Nothing is charged. Only signup
167
+ connections are unavailable. Only signup
173
168
  returns it, with ``retryAfter: 3600`` in the body and a
174
169
  ``Retry-After: 3600`` header. The SDK retries it like any 5xx
175
170
  before raising.
176
171
  SendlyError: ``whatsapp_signup_limit_reached`` (429) after 5
177
- failed, charged signups in 24 hours. Not retried; try again
178
- the next day.
172
+ failed signups (or 20 cancelled) in 24 hours. Not retried;
173
+ try again the next day.
179
174
  SendlyError: ``whatsapp_verification_in_progress`` (409) without
180
175
  ``business_account_id`` for a number being added by code;
181
176
  ``e.response.model_extra['id']`` is that signup.
@@ -188,9 +183,9 @@ class WhatsAppSignupResource:
188
183
  (``e.response.model_extra['id']`` is that signup);
189
184
  ``whatsapp_already_enabled`` (409) when it is connected.
190
185
  SendlyError: ``whatsapp_verification_start_failed``: 422 when
191
- WhatsApp refused to send a code (final), 502 when it couldn't
192
- be reached. Either way the signup failed and the fee is
193
- refunded; start again. Not retried automatically.
186
+ WhatsApp refused to send a code (final), 503 when it couldn't
187
+ be reached. Either way the signup failed; start again. Not
188
+ retried automatically.
194
189
  """
195
190
  validate_phone_number(phone_number)
196
191
  body = _signup_body(phone_number, business_account_id, verification_method, display_name)
@@ -218,7 +213,7 @@ class WhatsAppSignupResource:
218
213
  (``settings:write``).
219
214
 
220
215
  A 5xx, a timeout or a network error is raised at once, never retried:
221
- every submission uses one of the 5 attempts, and a 502
216
+ every submission uses one of the 5 attempts, and a 503
222
217
  ``whatsapp_activation_pending`` means WhatsApp already accepted the
223
218
  code. Only a 429 the API never ran is retried.
224
219
 
@@ -233,14 +228,14 @@ class WhatsAppSignupResource:
233
228
  wrong code; ``e.response.attempts_remaining`` says how many
234
229
  tries are left.
235
230
  SendlyError: ``whatsapp_verification_failed`` (409) after 5 wrong
236
- codes: the signup failed and the fee is refunded.
231
+ codes: the signup failed.
237
232
  SendlyError: ``whatsapp_verification_busy`` (409) while another
238
233
  code for the number is being checked; try again in a moment.
239
234
  SendlyError: ``signup_not_active`` (409) when the signup isn't
240
235
  waiting for a code, or is more than 3 hours old.
241
- SendlyError: ``whatsapp_verification_unavailable`` (502) when
236
+ SendlyError: ``whatsapp_verification_unavailable`` (503) when
242
237
  WhatsApp couldn't check the code (the attempt isn't counted),
243
- or ``whatsapp_activation_pending`` (502) when the code was
238
+ or ``whatsapp_activation_pending`` (503) when the code was
244
239
  accepted but the connection didn't finish; Sendly is alerted,
245
240
  so check back with :meth:`get`. Neither is retried
246
241
  automatically.
@@ -280,7 +275,7 @@ class WhatsAppSignupResource:
280
275
  ``e.response.retry_after`` is the wait in seconds. Not
281
276
  retried.
282
277
  SendlyError: ``whatsapp_verification_resend_failed``: 422 when
283
- WhatsApp wouldn't send another code yet, 502 when it couldn't
278
+ WhatsApp wouldn't send another code yet, 503 when it couldn't
284
279
  be reached.
285
280
  SendlyError: ``signup_not_active`` (409) when the signup isn't
286
281
  waiting for a code, or is more than 3 hours old.
@@ -444,7 +439,7 @@ class WhatsAppSendersResource:
444
439
  5 MB.
445
440
  SendlyError: ``whatsapp_sender_not_connected`` (404) when the
446
441
  number isn't connected to WhatsApp.
447
- SendlyError: ``whatsapp_profile_update_failed`` (502) when
442
+ SendlyError: ``whatsapp_profile_update_failed`` (503) when
448
443
  WhatsApp refused the photo or couldn't be reached; check the
449
444
  image is square and at least 192 pixels wide. A 5xx, a
450
445
  timeout or a network error is raised at once, not retried
@@ -482,7 +477,7 @@ class WhatsAppSendersResource:
482
477
  Raises:
483
478
  SendlyError: ``whatsapp_sender_not_connected`` (404) when the
484
479
  number isn't connected to WhatsApp.
485
- SendlyError: ``whatsapp_profile_update_failed`` (502) when
480
+ SendlyError: ``whatsapp_profile_update_failed`` (503) when
486
481
  WhatsApp couldn't remove it.
487
482
  """
488
483
  validate_phone_number(phone_number)
@@ -512,7 +507,7 @@ class WhatsAppSendersResource:
512
507
  SendlyError: ``whatsapp_sender_not_connected`` (404) when the
513
508
  number isn't connected to WhatsApp.
514
509
  SendlyError: ``whatsapp_conversational_components_fetch_failed``
515
- (502) when WhatsApp couldn't be reached.
510
+ (503) when WhatsApp couldn't be reached.
516
511
  """
517
512
  validate_phone_number(phone_number)
518
513
  data = self._http.request(
@@ -555,7 +550,7 @@ class WhatsAppSendersResource:
555
550
  SendlyError: ``whatsapp_sender_not_connected`` (404) when the
556
551
  number isn't connected to WhatsApp.
557
552
  SendlyError: ``whatsapp_conversational_components_update_failed``
558
- (502) when WhatsApp couldn't save them.
553
+ (503) when WhatsApp couldn't save them.
559
554
 
560
555
  Example:
561
556
  >>> client.whatsapp.senders.update_conversational_components(
@@ -599,7 +594,7 @@ class WhatsAppSendersResource:
599
594
  at least 2,000 people a day and the display name is approved.
600
595
  SendlyError: ``whatsapp_sender_not_connected`` (404) when the
601
596
  number isn't connected to WhatsApp.
602
- SendlyError: ``whatsapp_calling_update_failed`` (502) when
597
+ SendlyError: ``whatsapp_calling_update_failed`` (503) when
603
598
  WhatsApp couldn't be reached.
604
599
 
605
600
  Example:
@@ -771,7 +766,7 @@ class WhatsAppResource:
771
766
  """WhatsApp API resource (sync)
772
767
 
773
768
  Example:
774
- >>> # 1. Connect a number ($19 one-time, no monthly fee). The connect
769
+ >>> # 1. Connect a number (free, no monthly fee). The connect
775
770
  >>> # URL must be opened by a human - they log in with Facebook in
776
771
  >>> # a browser to link their WhatsApp Business Account.
777
772
  >>> signup = client.whatsapp.signup.create('+15559876543')
@@ -2941,7 +2941,7 @@ class WhatsAppSignup(BaseModel):
2941
2941
  "it still shows the earlier code until the new one arrives. Once "
2942
2942
  "WhatsApp has checked a code, only a code that arrived after the "
2943
2943
  "last submission or resend is returned. A submission answered with "
2944
- "502 whatsapp_verification_unavailable is not counted, so the same "
2944
+ "503 whatsapp_verification_unavailable is not counted, so the same "
2945
2945
  "unchecked code can come back, and submitting it again is safe"
2946
2946
  ),
2947
2947
  )
@@ -3320,12 +3320,17 @@ class WhatsAppMessage(BaseModel):
3320
3320
  description=(
3321
3321
  "Present when the send asked for fallback_to_sms. When WhatsApp "
3322
3322
  "couldn't take the message and it went as an SMS, status is "
3323
- "failed and fallback.status is sent"
3323
+ "failed and fallback.status is sent (check with fell_back)"
3324
3324
  ),
3325
3325
  )
3326
3326
 
3327
3327
  model_config = ConfigDict(populate_by_name=True)
3328
3328
 
3329
+ @property
3330
+ def fell_back(self) -> bool:
3331
+ """True when the message went as an SMS instead: fallback.status is sent"""
3332
+ return self.fallback is not None and self.fallback.status == "sent"
3333
+
3329
3334
 
3330
3335
  # ============================================================================
3331
3336
  # RCS
@@ -28,7 +28,7 @@ T = TypeVar("T")
28
28
  DEFAULT_BASE_URL = "https://sendly.live/api/v1"
29
29
  DEFAULT_TIMEOUT = 30.0
30
30
  DEFAULT_MAX_RETRIES = 3
31
- SDK_VERSION = "4.4.0"
31
+ SDK_VERSION = "4.5.0"
32
32
 
33
33
 
34
34
  _WAIT_AND_RETRY_CODES = (
@@ -1088,6 +1088,19 @@ class TestWhatsAppSmsFallback:
1088
1088
  assert message.fallback.status == "sent"
1089
1089
  assert message.fallback.message_id == "msg_sms_1"
1090
1090
  assert message.fallback.credits_used == 2
1091
+ assert message.fell_back is True
1092
+
1093
+ @pytest.mark.parametrize("status", ["pending", "failed", "unavailable", "skipped"])
1094
+ def test_fell_back_is_false_until_the_sms_went(
1095
+ self, mock_whatsapp_message, status
1096
+ ):
1097
+ message = WhatsAppMessage.model_validate(
1098
+ {
1099
+ **mock_whatsapp_message,
1100
+ "fallback": {"channel": "sms", "status": status},
1101
+ }
1102
+ )
1103
+ assert message.fell_back is False
1091
1104
 
1092
1105
  def test_leaves_fallback_out_by_default(
1093
1106
  self, api_key, mock_whatsapp_message, httpx_mock: HTTPXMock
@@ -1106,6 +1119,7 @@ class TestWhatsAppSmsFallback:
1106
1119
  payload = json.loads(httpx_mock.get_request().read().decode())
1107
1120
  assert "fallbackToSms" not in payload
1108
1121
  assert message.fallback is None
1122
+ assert message.fell_back is False
1109
1123
 
1110
1124
  @pytest.mark.asyncio
1111
1125
  async def test_async_sends_fallback_to_sms(
@@ -1129,6 +1143,7 @@ class TestWhatsAppSmsFallback:
1129
1143
  payload = json.loads(httpx_mock.get_request().read().decode())
1130
1144
  assert payload["fallbackToSms"] is True
1131
1145
  assert message.fallback.status == "sent"
1146
+ assert message.fell_back is True
1132
1147
 
1133
1148
 
1134
1149
  class TestWhatsAppRecoveredSend:
@@ -892,8 +892,7 @@ def no_async_sleep(monkeypatch):
892
892
  START_FAILED = {
893
893
  "error": "whatsapp_verification_start_failed",
894
894
  "message": (
895
- "WhatsApp couldn't start verifying this number. Any setup fee is refunded "
896
- "automatically. Please try again shortly."
895
+ "WhatsApp couldn't start verifying this number. Please try again shortly."
897
896
  ),
898
897
  }
899
898
 
@@ -944,8 +943,7 @@ class TestServerErrorsOnCallsThatMustNotRepeat:
944
943
  json={
945
944
  "error": "internal_error",
946
945
  "message": (
947
- "Something went wrong asking WhatsApp for the code. Any setup fee is "
948
- "refunded automatically. Please try again."
946
+ "Something went wrong asking WhatsApp for the code. Please try again."
949
947
  ),
950
948
  },
951
949
  is_reusable=True,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes