sendly 4.4.0__tar.gz → 4.5.1__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.
- {sendly-4.4.0 → sendly-4.5.1}/CHANGELOG.md +11 -0
- {sendly-4.4.0 → sendly-4.5.1}/PKG-INFO +92 -19
- {sendly-4.4.0 → sendly-4.5.1}/README.md +91 -18
- {sendly-4.4.0 → sendly-4.5.1}/pyproject.toml +1 -1
- {sendly-4.4.0 → sendly-4.5.1}/sendly/__init__.py +1 -1
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/messages.py +9 -4
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/whatsapp.py +19 -18
- {sendly-4.4.0 → sendly-4.5.1}/sendly/types.py +7 -2
- {sendly-4.4.0 → sendly-4.5.1}/sendly/utils/http.py +1 -1
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_whatsapp.py +15 -0
- {sendly-4.4.0 → sendly-4.5.1}/.github/header.svg +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/.gitignore +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/examples/async_example.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/examples/error_handling.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/examples/send_sms.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/client.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/errors.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/py.typed +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/__init__.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/account.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/business_upgrade.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/calls.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/campaigns.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/contacts.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/conversations.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/drafts.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/enterprise.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/labels.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/links.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/media.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/numbers.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/rcs.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/rules.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/templates.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/tendlc.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/verify.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/voice.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/resources/webhooks.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/utils/__init__.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/utils/validation.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/sendly/webhooks.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/conftest.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_account.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_async_client.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_calls.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_campaigns.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_client.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_contacts.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_conversations.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_enterprise.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_errors.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_http_body_kwarg.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_http_retry.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_idempotency.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_media.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_messages.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_messages.py.bak +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_messages_batch.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_messages_schedule.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_messages_wire.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_numbers.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_path_ids.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_rcs.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_rcs_registration.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_rules.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_templates.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_tendlc.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_validation.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_voice.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_webhooks.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_webhooks_resource.py +0 -0
- {sendly-4.4.0 → sendly-4.5.1}/tests/test_whatsapp_extras.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.
|
|
3
|
+
Version: 4.5.1
|
|
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
|
|
@@ -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 ($
|
|
1358
|
+
Connect a number you own to WhatsApp ($9.99 one-time, 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
|
|
@@ -1388,9 +1388,10 @@ print(f'Open {signup.connect_url} and log in with Facebook')
|
|
|
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
1390
|
# with registration_timeout and the fee is refunded ('failed', with
|
|
1391
|
-
# signup.failure_reasons explaining why).
|
|
1392
|
-
#
|
|
1393
|
-
#
|
|
1391
|
+
# signup.failure_reasons explaining why). The one-time $9.99 connection fee is
|
|
1392
|
+
# charged to your card when you start connecting the number and refunded
|
|
1393
|
+
# automatically if the connection fails; it is not refundable once the number
|
|
1394
|
+
# is connected.
|
|
1394
1395
|
|
|
1395
1396
|
# 2. Create a template (Meta reviews it, usually 24-48h). category is required
|
|
1396
1397
|
# (UTILITY, AUTHENTICATION or MARKETING) with no default, and an update can't
|
|
@@ -1474,7 +1475,7 @@ components = client.whatsapp.senders.get_conversational_components('+15555550123
|
|
|
1474
1475
|
client.whatsapp.senders.set_calling('+15555550123', enabled=True)
|
|
1475
1476
|
|
|
1476
1477
|
# Add another number to an account you already connected: no Facebook step.
|
|
1477
|
-
# Same $
|
|
1478
|
+
# Same $9.99 fee, refunded if it fails. WhatsApp sends the number a 6-digit code
|
|
1478
1479
|
senders = client.whatsapp.senders.list().senders
|
|
1479
1480
|
account_id = next(
|
|
1480
1481
|
s.business_account_id for s in senders if s.status == 'active' and s.business_account_id
|
|
@@ -1505,7 +1506,7 @@ codes it fails with `verification_failed` and the fee is refunded. Calls carry
|
|
|
1505
1506
|
submitted, it is the newest code that has arrived since the signup started, so
|
|
1506
1507
|
after a resend it still shows the earlier code until the new one arrives. Once
|
|
1507
1508
|
WhatsApp has checked a code, only a code that arrived after the last
|
|
1508
|
-
submission or resend is returned. A submission answered with
|
|
1509
|
+
submission or resend is returned. A submission answered with 503
|
|
1509
1510
|
`whatsapp_verification_unavailable` is not counted, so the same unchecked code
|
|
1510
1511
|
can come back, and submitting it again is safe.
|
|
1511
1512
|
|
|
@@ -1535,10 +1536,10 @@ Errors specific to WhatsApp:
|
|
|
1535
1536
|
24 hours. It is final and not retried; try again the next day.
|
|
1536
1537
|
- `whatsapp_send_failed` on a send: HTTP 422 when WhatsApp refused the message,
|
|
1537
1538
|
which is final (cached under the idempotency key and replayed for 24 hours),
|
|
1538
|
-
or HTTP
|
|
1539
|
-
|
|
1540
|
-
retries it
|
|
1541
|
-
charged.
|
|
1539
|
+
or HTTP 503 when the message provably never reached WhatsApp, so it was not
|
|
1540
|
+
sent and is safe to resend under the same idempotency key. A 503 is never
|
|
1541
|
+
cached, and the SDK retries it automatically, up to 3 times under the same
|
|
1542
|
+
key, before raising. Either way the message was not charged.
|
|
1542
1543
|
- `whatsapp_send_unconfirmed` (HTTP 409) on a send: the outcome is unknown.
|
|
1543
1544
|
The message was marked failed and refunded but may still be delivered, so
|
|
1544
1545
|
check before sending it again (it could arrive twice). It is not retried
|
|
@@ -1556,18 +1557,18 @@ Errors specific to WhatsApp:
|
|
|
1556
1557
|
marketing template without an opt-out button only gets a warning.
|
|
1557
1558
|
- Profile photo: `file_required` and `whatsapp_profile_photo_invalid` (HTTP
|
|
1558
1559
|
400), `whatsapp_profile_photo_too_large` (HTTP 413),
|
|
1559
|
-
`whatsapp_profile_update_failed` (HTTP
|
|
1560
|
+
`whatsapp_profile_update_failed` (HTTP 503). Conversational components:
|
|
1560
1561
|
`invalid_request` (HTTP 400) with the reason,
|
|
1561
1562
|
`whatsapp_conversational_components_fetch_failed` and
|
|
1562
|
-
`whatsapp_conversational_components_update_failed` (HTTP
|
|
1563
|
+
`whatsapp_conversational_components_update_failed` (HTTP 503). Calling:
|
|
1563
1564
|
`voice_not_enabled` (HTTP 409), `whatsapp_calling_unavailable` (HTTP 422;
|
|
1564
1565
|
WhatsApp only allows calling once the account may message 2,000 people a
|
|
1565
1566
|
day and the display name is approved), `whatsapp_calling_update_failed`
|
|
1566
|
-
(HTTP
|
|
1567
|
+
(HTTP 503).
|
|
1567
1568
|
- Adding a number by code: `whatsapp_business_account_not_found` (HTTP 404),
|
|
1568
1569
|
`display_name_required` (HTTP 400), `whatsapp_signup_in_progress` and
|
|
1569
1570
|
`whatsapp_already_enabled` (HTTP 409), and
|
|
1570
|
-
`whatsapp_verification_start_failed` (HTTP 422 refused,
|
|
1571
|
+
`whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable; the
|
|
1571
1572
|
fee is refunded either way). A Facebook `signup.create()` for a number being
|
|
1572
1573
|
added by code gets `whatsapp_verification_in_progress` (HTTP 409).
|
|
1573
1574
|
`verify()`: `invalid_verification_code` (HTTP 400, not 6 digits),
|
|
@@ -1575,9 +1576,9 @@ Errors specific to WhatsApp:
|
|
|
1575
1576
|
`e.response.attempts_remaining`), `whatsapp_verification_failed` (HTTP 409,
|
|
1576
1577
|
too many wrong codes), `whatsapp_verification_busy` (HTTP 409, retry),
|
|
1577
1578
|
`whatsapp_verification_unavailable` and `whatsapp_activation_pending` (HTTP
|
|
1578
|
-
|
|
1579
|
+
503). `resend()`: `whatsapp_verification_resend_too_soon` (HTTP 429, wait
|
|
1579
1580
|
`e.response.retry_after` seconds) and `whatsapp_verification_resend_failed`
|
|
1580
|
-
(HTTP 422 or
|
|
1581
|
+
(HTTP 422 or 503). Both raise `signup_not_active` (HTTP 409) once the signup
|
|
1581
1582
|
stops waiting for a code, and `signup_not_found` (HTTP 404). The SDK never
|
|
1582
1583
|
retries a `5xx`, a timeout or a network error from `signup.create()` with a
|
|
1583
1584
|
`business_account_id` or from `verify()`; it retries `resend()` like any
|
|
@@ -1585,6 +1586,78 @@ Errors specific to WhatsApp:
|
|
|
1585
1586
|
|
|
1586
1587
|
`message.whatsapp.message_id` is `None` until the first delivery report lands.
|
|
1587
1588
|
|
|
1589
|
+
### SMS fallback
|
|
1590
|
+
|
|
1591
|
+
Pass `fallback_to_sms=True` and a WhatsApp message that can't be delivered
|
|
1592
|
+
goes as an SMS from the same number instead.
|
|
1593
|
+
|
|
1594
|
+
```python
|
|
1595
|
+
from sendly import Sendly, SendlyError, WhatsAppSmsFallback
|
|
1596
|
+
|
|
1597
|
+
client = Sendly('sk_live_v1_YOUR_API_KEY')
|
|
1598
|
+
|
|
1599
|
+
try:
|
|
1600
|
+
message = client.messages.send(
|
|
1601
|
+
channel='whatsapp',
|
|
1602
|
+
to='+15555550100',
|
|
1603
|
+
from_='+15555550123',
|
|
1604
|
+
text='Your table is ready!',
|
|
1605
|
+
fallback_to_sms=True,
|
|
1606
|
+
)
|
|
1607
|
+
print(message.fallback.status) # 'pending', 'sent', 'failed', 'unavailable' or 'skipped'
|
|
1608
|
+
print(message.fallback.reason) # 'window_closed', 'whatsapp_refused' or 'whatsapp_failed'
|
|
1609
|
+
if message.fell_back and message.fallback.message_id:
|
|
1610
|
+
# The SMS went instead (message.status is 'failed')
|
|
1611
|
+
sms = client.messages.get(message.fallback.message_id)
|
|
1612
|
+
print(sms.fallback_of) # the WhatsApp message's id
|
|
1613
|
+
except SendlyError as e:
|
|
1614
|
+
extra = (e.response.model_extra or {}) if e.response else {}
|
|
1615
|
+
if e.code == 'sms_fallback_unavailable':
|
|
1616
|
+
pass # 403: this number can't send SMS to the recipient; nothing was sent
|
|
1617
|
+
elif e.code == 'sms_fallback_not_supported':
|
|
1618
|
+
print(extra.get('reason')) # marketing_template, authentication_template or media_without_caption
|
|
1619
|
+
elif extra.get('fallback'):
|
|
1620
|
+
# 422 whatsapp_window_closed or whatsapp_send_failed: the SMS couldn't go
|
|
1621
|
+
# either. A closed window also carries the WhatsApp message's id.
|
|
1622
|
+
fallback = WhatsAppSmsFallback.model_validate(extra['fallback'])
|
|
1623
|
+
print(e.code, extra.get('id'), fallback.status, fallback.error)
|
|
1624
|
+
else:
|
|
1625
|
+
raise
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
- It falls back when the 24-hour window is closed (the SMS goes at once:
|
|
1629
|
+
free-form text and media go only within 24 hours of the customer's last
|
|
1630
|
+
message, templates any time), when WhatsApp refuses the message, or when
|
|
1631
|
+
WhatsApp reports it undeliverable later. Never after WhatsApp reports it
|
|
1632
|
+
delivered or read, and at most once per message.
|
|
1633
|
+
- The number must already be able to send SMS to the recipient (a verified
|
|
1634
|
+
toll-free number, or a local number on a 10DLC campaign, to a US or Canada
|
|
1635
|
+
number), or the send is refused with HTTP 403 `sms_fallback_unavailable`
|
|
1636
|
+
before anything is sent.
|
|
1637
|
+
- Marketing and authentication templates, and media without a caption, can't
|
|
1638
|
+
fall back: HTTP 422 `sms_fallback_not_supported`. A utility template falls
|
|
1639
|
+
back as its text header, body and footer with the variables filled in, and
|
|
1640
|
+
its URL and phone buttons as `label: target`; quick replies are dropped and
|
|
1641
|
+
listed in `fallback.content_dropped`.
|
|
1642
|
+
- Billing: the WhatsApp charge is refunded (a closed window is never charged)
|
|
1643
|
+
and the SMS is billed as an SMS. A message that fell back at once comes
|
|
1644
|
+
back with `status == 'failed'`, `credits_used == 0` and
|
|
1645
|
+
`fallback.status == 'sent'` (`message.fell_back` is `True`);
|
|
1646
|
+
`fallback.credits_used` is what the SMS cost.
|
|
1647
|
+
- No SMS goes on an HTTP 409 `whatsapp_send_unconfirmed` or an HTTP 503
|
|
1648
|
+
`whatsapp_send_failed`. `fallback.status` is `'unavailable'` when SMS rules
|
|
1649
|
+
stop it (the contact opted out, a promotional-looking text in the
|
|
1650
|
+
recipient's quiet hours, restricted content), with the reason in
|
|
1651
|
+
`fallback.error`, and `'skipped'` for a WhatsApp error an SMS can't fix.
|
|
1652
|
+
- There is no time-based fallback: a message WhatsApp accepted but hasn't
|
|
1653
|
+
delivered, for example to a phone that is off, stays `'pending'`, and no
|
|
1654
|
+
SMS goes unless WhatsApp later reports a failure.
|
|
1655
|
+
- `messages.get()` and `messages.list()` return the same `fallback` on the
|
|
1656
|
+
WhatsApp message, so check a pending one later. The SMS carries
|
|
1657
|
+
`fallback_of`, the WhatsApp message's id.
|
|
1658
|
+
|
|
1659
|
+
Guide: [Send WhatsApp with an SMS Fallback](https://sendly.live/docs/whatsapp/send-with-fallback).
|
|
1660
|
+
|
|
1588
1661
|
## RCS
|
|
1589
1662
|
|
|
1590
1663
|
Send branded, verified-sender messages on Android: rich cards, suggestion
|
|
@@ -1946,8 +2019,8 @@ default, or `CA`) are optional.
|
|
|
1946
2019
|
|
|
1947
2020
|
Refusals: `agent_required` (agent mode with no agent), `agent_not_found`,
|
|
1948
2021
|
`agent_disabled` (turn the agent on first), `agent_limit`, `agent_in_use`,
|
|
1949
|
-
`number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP
|
|
1950
|
-
retry shortly), `carrier_refused` (HTTP
|
|
2022
|
+
`number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 503,
|
|
2023
|
+
retry shortly), `carrier_refused` (HTTP 503, retry shortly unless the message
|
|
1951
2024
|
says the number couldn't be found for emergency registration: contact
|
|
1952
2025
|
support), `voice_unavailable` (HTTP 503),
|
|
1953
2026
|
`e911_not_applicable` (only US and Canadian numbers take an emergency
|
|
@@ -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 ($
|
|
1323
|
+
Connect a number you own to WhatsApp ($9.99 one-time, 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
|
|
@@ -1353,9 +1353,10 @@ print(f'Open {signup.connect_url} and log in with Facebook')
|
|
|
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
1355
|
# with registration_timeout and the fee is refunded ('failed', with
|
|
1356
|
-
# signup.failure_reasons explaining why).
|
|
1357
|
-
#
|
|
1358
|
-
#
|
|
1356
|
+
# signup.failure_reasons explaining why). The one-time $9.99 connection fee is
|
|
1357
|
+
# charged to your card when you start connecting the number and refunded
|
|
1358
|
+
# automatically if the connection fails; it is not refundable once the number
|
|
1359
|
+
# is connected.
|
|
1359
1360
|
|
|
1360
1361
|
# 2. Create a template (Meta reviews it, usually 24-48h). category is required
|
|
1361
1362
|
# (UTILITY, AUTHENTICATION or MARKETING) with no default, and an update can't
|
|
@@ -1439,7 +1440,7 @@ components = client.whatsapp.senders.get_conversational_components('+15555550123
|
|
|
1439
1440
|
client.whatsapp.senders.set_calling('+15555550123', enabled=True)
|
|
1440
1441
|
|
|
1441
1442
|
# Add another number to an account you already connected: no Facebook step.
|
|
1442
|
-
# Same $
|
|
1443
|
+
# Same $9.99 fee, refunded if it fails. WhatsApp sends the number a 6-digit code
|
|
1443
1444
|
senders = client.whatsapp.senders.list().senders
|
|
1444
1445
|
account_id = next(
|
|
1445
1446
|
s.business_account_id for s in senders if s.status == 'active' and s.business_account_id
|
|
@@ -1470,7 +1471,7 @@ codes it fails with `verification_failed` and the fee is refunded. Calls carry
|
|
|
1470
1471
|
submitted, it is the newest code that has arrived since the signup started, so
|
|
1471
1472
|
after a resend it still shows the earlier code until the new one arrives. Once
|
|
1472
1473
|
WhatsApp has checked a code, only a code that arrived after the last
|
|
1473
|
-
submission or resend is returned. A submission answered with
|
|
1474
|
+
submission or resend is returned. A submission answered with 503
|
|
1474
1475
|
`whatsapp_verification_unavailable` is not counted, so the same unchecked code
|
|
1475
1476
|
can come back, and submitting it again is safe.
|
|
1476
1477
|
|
|
@@ -1500,10 +1501,10 @@ Errors specific to WhatsApp:
|
|
|
1500
1501
|
24 hours. It is final and not retried; try again the next day.
|
|
1501
1502
|
- `whatsapp_send_failed` on a send: HTTP 422 when WhatsApp refused the message,
|
|
1502
1503
|
which is final (cached under the idempotency key and replayed for 24 hours),
|
|
1503
|
-
or HTTP
|
|
1504
|
-
|
|
1505
|
-
retries it
|
|
1506
|
-
charged.
|
|
1504
|
+
or HTTP 503 when the message provably never reached WhatsApp, so it was not
|
|
1505
|
+
sent and is safe to resend under the same idempotency key. A 503 is never
|
|
1506
|
+
cached, and the SDK retries it automatically, up to 3 times under the same
|
|
1507
|
+
key, before raising. Either way the message was not charged.
|
|
1507
1508
|
- `whatsapp_send_unconfirmed` (HTTP 409) on a send: the outcome is unknown.
|
|
1508
1509
|
The message was marked failed and refunded but may still be delivered, so
|
|
1509
1510
|
check before sending it again (it could arrive twice). It is not retried
|
|
@@ -1521,18 +1522,18 @@ Errors specific to WhatsApp:
|
|
|
1521
1522
|
marketing template without an opt-out button only gets a warning.
|
|
1522
1523
|
- Profile photo: `file_required` and `whatsapp_profile_photo_invalid` (HTTP
|
|
1523
1524
|
400), `whatsapp_profile_photo_too_large` (HTTP 413),
|
|
1524
|
-
`whatsapp_profile_update_failed` (HTTP
|
|
1525
|
+
`whatsapp_profile_update_failed` (HTTP 503). Conversational components:
|
|
1525
1526
|
`invalid_request` (HTTP 400) with the reason,
|
|
1526
1527
|
`whatsapp_conversational_components_fetch_failed` and
|
|
1527
|
-
`whatsapp_conversational_components_update_failed` (HTTP
|
|
1528
|
+
`whatsapp_conversational_components_update_failed` (HTTP 503). Calling:
|
|
1528
1529
|
`voice_not_enabled` (HTTP 409), `whatsapp_calling_unavailable` (HTTP 422;
|
|
1529
1530
|
WhatsApp only allows calling once the account may message 2,000 people a
|
|
1530
1531
|
day and the display name is approved), `whatsapp_calling_update_failed`
|
|
1531
|
-
(HTTP
|
|
1532
|
+
(HTTP 503).
|
|
1532
1533
|
- Adding a number by code: `whatsapp_business_account_not_found` (HTTP 404),
|
|
1533
1534
|
`display_name_required` (HTTP 400), `whatsapp_signup_in_progress` and
|
|
1534
1535
|
`whatsapp_already_enabled` (HTTP 409), and
|
|
1535
|
-
`whatsapp_verification_start_failed` (HTTP 422 refused,
|
|
1536
|
+
`whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable; the
|
|
1536
1537
|
fee is refunded either way). A Facebook `signup.create()` for a number being
|
|
1537
1538
|
added by code gets `whatsapp_verification_in_progress` (HTTP 409).
|
|
1538
1539
|
`verify()`: `invalid_verification_code` (HTTP 400, not 6 digits),
|
|
@@ -1540,9 +1541,9 @@ Errors specific to WhatsApp:
|
|
|
1540
1541
|
`e.response.attempts_remaining`), `whatsapp_verification_failed` (HTTP 409,
|
|
1541
1542
|
too many wrong codes), `whatsapp_verification_busy` (HTTP 409, retry),
|
|
1542
1543
|
`whatsapp_verification_unavailable` and `whatsapp_activation_pending` (HTTP
|
|
1543
|
-
|
|
1544
|
+
503). `resend()`: `whatsapp_verification_resend_too_soon` (HTTP 429, wait
|
|
1544
1545
|
`e.response.retry_after` seconds) and `whatsapp_verification_resend_failed`
|
|
1545
|
-
(HTTP 422 or
|
|
1546
|
+
(HTTP 422 or 503). Both raise `signup_not_active` (HTTP 409) once the signup
|
|
1546
1547
|
stops waiting for a code, and `signup_not_found` (HTTP 404). The SDK never
|
|
1547
1548
|
retries a `5xx`, a timeout or a network error from `signup.create()` with a
|
|
1548
1549
|
`business_account_id` or from `verify()`; it retries `resend()` like any
|
|
@@ -1550,6 +1551,78 @@ Errors specific to WhatsApp:
|
|
|
1550
1551
|
|
|
1551
1552
|
`message.whatsapp.message_id` is `None` until the first delivery report lands.
|
|
1552
1553
|
|
|
1554
|
+
### SMS fallback
|
|
1555
|
+
|
|
1556
|
+
Pass `fallback_to_sms=True` and a WhatsApp message that can't be delivered
|
|
1557
|
+
goes as an SMS from the same number instead.
|
|
1558
|
+
|
|
1559
|
+
```python
|
|
1560
|
+
from sendly import Sendly, SendlyError, WhatsAppSmsFallback
|
|
1561
|
+
|
|
1562
|
+
client = Sendly('sk_live_v1_YOUR_API_KEY')
|
|
1563
|
+
|
|
1564
|
+
try:
|
|
1565
|
+
message = client.messages.send(
|
|
1566
|
+
channel='whatsapp',
|
|
1567
|
+
to='+15555550100',
|
|
1568
|
+
from_='+15555550123',
|
|
1569
|
+
text='Your table is ready!',
|
|
1570
|
+
fallback_to_sms=True,
|
|
1571
|
+
)
|
|
1572
|
+
print(message.fallback.status) # 'pending', 'sent', 'failed', 'unavailable' or 'skipped'
|
|
1573
|
+
print(message.fallback.reason) # 'window_closed', 'whatsapp_refused' or 'whatsapp_failed'
|
|
1574
|
+
if message.fell_back and message.fallback.message_id:
|
|
1575
|
+
# The SMS went instead (message.status is 'failed')
|
|
1576
|
+
sms = client.messages.get(message.fallback.message_id)
|
|
1577
|
+
print(sms.fallback_of) # the WhatsApp message's id
|
|
1578
|
+
except SendlyError as e:
|
|
1579
|
+
extra = (e.response.model_extra or {}) if e.response else {}
|
|
1580
|
+
if e.code == 'sms_fallback_unavailable':
|
|
1581
|
+
pass # 403: this number can't send SMS to the recipient; nothing was sent
|
|
1582
|
+
elif e.code == 'sms_fallback_not_supported':
|
|
1583
|
+
print(extra.get('reason')) # marketing_template, authentication_template or media_without_caption
|
|
1584
|
+
elif extra.get('fallback'):
|
|
1585
|
+
# 422 whatsapp_window_closed or whatsapp_send_failed: the SMS couldn't go
|
|
1586
|
+
# either. A closed window also carries the WhatsApp message's id.
|
|
1587
|
+
fallback = WhatsAppSmsFallback.model_validate(extra['fallback'])
|
|
1588
|
+
print(e.code, extra.get('id'), fallback.status, fallback.error)
|
|
1589
|
+
else:
|
|
1590
|
+
raise
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
- It falls back when the 24-hour window is closed (the SMS goes at once:
|
|
1594
|
+
free-form text and media go only within 24 hours of the customer's last
|
|
1595
|
+
message, templates any time), when WhatsApp refuses the message, or when
|
|
1596
|
+
WhatsApp reports it undeliverable later. Never after WhatsApp reports it
|
|
1597
|
+
delivered or read, and at most once per message.
|
|
1598
|
+
- The number must already be able to send SMS to the recipient (a verified
|
|
1599
|
+
toll-free number, or a local number on a 10DLC campaign, to a US or Canada
|
|
1600
|
+
number), or the send is refused with HTTP 403 `sms_fallback_unavailable`
|
|
1601
|
+
before anything is sent.
|
|
1602
|
+
- Marketing and authentication templates, and media without a caption, can't
|
|
1603
|
+
fall back: HTTP 422 `sms_fallback_not_supported`. A utility template falls
|
|
1604
|
+
back as its text header, body and footer with the variables filled in, and
|
|
1605
|
+
its URL and phone buttons as `label: target`; quick replies are dropped and
|
|
1606
|
+
listed in `fallback.content_dropped`.
|
|
1607
|
+
- Billing: the WhatsApp charge is refunded (a closed window is never charged)
|
|
1608
|
+
and the SMS is billed as an SMS. A message that fell back at once comes
|
|
1609
|
+
back with `status == 'failed'`, `credits_used == 0` and
|
|
1610
|
+
`fallback.status == 'sent'` (`message.fell_back` is `True`);
|
|
1611
|
+
`fallback.credits_used` is what the SMS cost.
|
|
1612
|
+
- No SMS goes on an HTTP 409 `whatsapp_send_unconfirmed` or an HTTP 503
|
|
1613
|
+
`whatsapp_send_failed`. `fallback.status` is `'unavailable'` when SMS rules
|
|
1614
|
+
stop it (the contact opted out, a promotional-looking text in the
|
|
1615
|
+
recipient's quiet hours, restricted content), with the reason in
|
|
1616
|
+
`fallback.error`, and `'skipped'` for a WhatsApp error an SMS can't fix.
|
|
1617
|
+
- There is no time-based fallback: a message WhatsApp accepted but hasn't
|
|
1618
|
+
delivered, for example to a phone that is off, stays `'pending'`, and no
|
|
1619
|
+
SMS goes unless WhatsApp later reports a failure.
|
|
1620
|
+
- `messages.get()` and `messages.list()` return the same `fallback` on the
|
|
1621
|
+
WhatsApp message, so check a pending one later. The SMS carries
|
|
1622
|
+
`fallback_of`, the WhatsApp message's id.
|
|
1623
|
+
|
|
1624
|
+
Guide: [Send WhatsApp with an SMS Fallback](https://sendly.live/docs/whatsapp/send-with-fallback).
|
|
1625
|
+
|
|
1553
1626
|
## RCS
|
|
1554
1627
|
|
|
1555
1628
|
Send branded, verified-sender messages on Android: rich cards, suggestion
|
|
@@ -1911,8 +1984,8 @@ default, or `CA`) are optional.
|
|
|
1911
1984
|
|
|
1912
1985
|
Refusals: `agent_required` (agent mode with no agent), `agent_not_found`,
|
|
1913
1986
|
`agent_disabled` (turn the agent on first), `agent_limit`, `agent_in_use`,
|
|
1914
|
-
`number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP
|
|
1915
|
-
retry shortly), `carrier_refused` (HTTP
|
|
1987
|
+
`number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 503,
|
|
1988
|
+
retry shortly), `carrier_refused` (HTTP 503, retry shortly unless the message
|
|
1916
1989
|
says the number couldn't be found for emergency registration: contact
|
|
1917
1990
|
support), `voice_unavailable` (HTTP 503),
|
|
1918
1991
|
`e911_not_applicable` (only US and Canadian numbers take an emergency
|
|
@@ -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
|
-
|
|
156
|
-
was not sent and is safe to send again. A
|
|
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
|
|
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
|
|
8
|
+
Connecting a number costs a one-time $9.99 fee (no 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,7 +99,8 @@ class WhatsAppSignupResource:
|
|
|
99
99
|
) -> Union[WhatsAppSignupSession, WhatsAppSignup]:
|
|
100
100
|
"""Start connecting a number to WhatsApp.
|
|
101
101
|
|
|
102
|
-
Charges a one-time $
|
|
102
|
+
Charges a one-time $9.99 connection fee per number (no monthly fee) to
|
|
103
|
+
your card when the connection starts, and returns a
|
|
103
104
|
``connect_url``. Completing the connection requires a human: hand the
|
|
104
105
|
URL to your user - they open it in a browser and log in with Facebook
|
|
105
106
|
to link their WhatsApp Business Account. Then poll :meth:`get` until
|
|
@@ -113,13 +114,13 @@ class WhatsAppSignupResource:
|
|
|
113
114
|
activates the number. Activation usually takes a few minutes but can
|
|
114
115
|
take hours. If it hasn't finished about 6 hours after the session
|
|
115
116
|
began, the session fails with ``registration_timeout`` and the fee is
|
|
116
|
-
refunded. If the connection fails, the $
|
|
117
|
-
automatically; once the number has connected
|
|
117
|
+
refunded. If the connection fails, the $9.99 fee is refunded
|
|
118
|
+
automatically; once the number has connected it is not refundable, and
|
|
118
119
|
a later disconnect gets nothing back.
|
|
119
120
|
|
|
120
121
|
Pass ``business_account_id`` to add the number to a WhatsApp
|
|
121
122
|
Business Account this workspace has already connected, without the
|
|
122
|
-
Facebook step. The same $
|
|
123
|
+
Facebook step. The same $9.99 fee applies (refunded automatically if the
|
|
123
124
|
connection fails), then WhatsApp sends the number a 6-digit code by
|
|
124
125
|
text or voice call. This returns a :class:`WhatsAppSignup` with status
|
|
125
126
|
``verifying`` and no ``connect_url``: submit the code with
|
|
@@ -174,8 +175,8 @@ class WhatsAppSignupResource:
|
|
|
174
175
|
``Retry-After: 3600`` header. The SDK retries it like any 5xx
|
|
175
176
|
before raising.
|
|
176
177
|
SendlyError: ``whatsapp_signup_limit_reached`` (429) after 5
|
|
177
|
-
failed, charged signups in 24 hours. Not retried;
|
|
178
|
-
the next day.
|
|
178
|
+
failed, charged signups (or 20 cancelled) in 24 hours. Not retried;
|
|
179
|
+
try again the next day.
|
|
179
180
|
SendlyError: ``whatsapp_verification_in_progress`` (409) without
|
|
180
181
|
``business_account_id`` for a number being added by code;
|
|
181
182
|
``e.response.model_extra['id']`` is that signup.
|
|
@@ -188,7 +189,7 @@ class WhatsAppSignupResource:
|
|
|
188
189
|
(``e.response.model_extra['id']`` is that signup);
|
|
189
190
|
``whatsapp_already_enabled`` (409) when it is connected.
|
|
190
191
|
SendlyError: ``whatsapp_verification_start_failed``: 422 when
|
|
191
|
-
WhatsApp refused to send a code (final),
|
|
192
|
+
WhatsApp refused to send a code (final), 503 when it couldn't
|
|
192
193
|
be reached. Either way the signup failed and the fee is
|
|
193
194
|
refunded; start again. Not retried automatically.
|
|
194
195
|
"""
|
|
@@ -218,7 +219,7 @@ class WhatsAppSignupResource:
|
|
|
218
219
|
(``settings:write``).
|
|
219
220
|
|
|
220
221
|
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
|
|
222
|
+
every submission uses one of the 5 attempts, and a 503
|
|
222
223
|
``whatsapp_activation_pending`` means WhatsApp already accepted the
|
|
223
224
|
code. Only a 429 the API never ran is retried.
|
|
224
225
|
|
|
@@ -238,9 +239,9 @@ class WhatsAppSignupResource:
|
|
|
238
239
|
code for the number is being checked; try again in a moment.
|
|
239
240
|
SendlyError: ``signup_not_active`` (409) when the signup isn't
|
|
240
241
|
waiting for a code, or is more than 3 hours old.
|
|
241
|
-
SendlyError: ``whatsapp_verification_unavailable`` (
|
|
242
|
+
SendlyError: ``whatsapp_verification_unavailable`` (503) when
|
|
242
243
|
WhatsApp couldn't check the code (the attempt isn't counted),
|
|
243
|
-
or ``whatsapp_activation_pending`` (
|
|
244
|
+
or ``whatsapp_activation_pending`` (503) when the code was
|
|
244
245
|
accepted but the connection didn't finish; Sendly is alerted,
|
|
245
246
|
so check back with :meth:`get`. Neither is retried
|
|
246
247
|
automatically.
|
|
@@ -280,7 +281,7 @@ class WhatsAppSignupResource:
|
|
|
280
281
|
``e.response.retry_after`` is the wait in seconds. Not
|
|
281
282
|
retried.
|
|
282
283
|
SendlyError: ``whatsapp_verification_resend_failed``: 422 when
|
|
283
|
-
WhatsApp wouldn't send another code yet,
|
|
284
|
+
WhatsApp wouldn't send another code yet, 503 when it couldn't
|
|
284
285
|
be reached.
|
|
285
286
|
SendlyError: ``signup_not_active`` (409) when the signup isn't
|
|
286
287
|
waiting for a code, or is more than 3 hours old.
|
|
@@ -444,7 +445,7 @@ class WhatsAppSendersResource:
|
|
|
444
445
|
5 MB.
|
|
445
446
|
SendlyError: ``whatsapp_sender_not_connected`` (404) when the
|
|
446
447
|
number isn't connected to WhatsApp.
|
|
447
|
-
SendlyError: ``whatsapp_profile_update_failed`` (
|
|
448
|
+
SendlyError: ``whatsapp_profile_update_failed`` (503) when
|
|
448
449
|
WhatsApp refused the photo or couldn't be reached; check the
|
|
449
450
|
image is square and at least 192 pixels wide. A 5xx, a
|
|
450
451
|
timeout or a network error is raised at once, not retried
|
|
@@ -482,7 +483,7 @@ class WhatsAppSendersResource:
|
|
|
482
483
|
Raises:
|
|
483
484
|
SendlyError: ``whatsapp_sender_not_connected`` (404) when the
|
|
484
485
|
number isn't connected to WhatsApp.
|
|
485
|
-
SendlyError: ``whatsapp_profile_update_failed`` (
|
|
486
|
+
SendlyError: ``whatsapp_profile_update_failed`` (503) when
|
|
486
487
|
WhatsApp couldn't remove it.
|
|
487
488
|
"""
|
|
488
489
|
validate_phone_number(phone_number)
|
|
@@ -512,7 +513,7 @@ class WhatsAppSendersResource:
|
|
|
512
513
|
SendlyError: ``whatsapp_sender_not_connected`` (404) when the
|
|
513
514
|
number isn't connected to WhatsApp.
|
|
514
515
|
SendlyError: ``whatsapp_conversational_components_fetch_failed``
|
|
515
|
-
(
|
|
516
|
+
(503) when WhatsApp couldn't be reached.
|
|
516
517
|
"""
|
|
517
518
|
validate_phone_number(phone_number)
|
|
518
519
|
data = self._http.request(
|
|
@@ -555,7 +556,7 @@ class WhatsAppSendersResource:
|
|
|
555
556
|
SendlyError: ``whatsapp_sender_not_connected`` (404) when the
|
|
556
557
|
number isn't connected to WhatsApp.
|
|
557
558
|
SendlyError: ``whatsapp_conversational_components_update_failed``
|
|
558
|
-
(
|
|
559
|
+
(503) when WhatsApp couldn't save them.
|
|
559
560
|
|
|
560
561
|
Example:
|
|
561
562
|
>>> client.whatsapp.senders.update_conversational_components(
|
|
@@ -599,7 +600,7 @@ class WhatsAppSendersResource:
|
|
|
599
600
|
at least 2,000 people a day and the display name is approved.
|
|
600
601
|
SendlyError: ``whatsapp_sender_not_connected`` (404) when the
|
|
601
602
|
number isn't connected to WhatsApp.
|
|
602
|
-
SendlyError: ``whatsapp_calling_update_failed`` (
|
|
603
|
+
SendlyError: ``whatsapp_calling_update_failed`` (503) when
|
|
603
604
|
WhatsApp couldn't be reached.
|
|
604
605
|
|
|
605
606
|
Example:
|
|
@@ -771,7 +772,7 @@ class WhatsAppResource:
|
|
|
771
772
|
"""WhatsApp API resource (sync)
|
|
772
773
|
|
|
773
774
|
Example:
|
|
774
|
-
>>> # 1. Connect a number ($
|
|
775
|
+
>>> # 1. Connect a number ($9.99 one-time, no monthly fee). The connect
|
|
775
776
|
>>> # URL must be opened by a human - they log in with Facebook in
|
|
776
777
|
>>> # a browser to link their WhatsApp Business Account.
|
|
777
778
|
>>> 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
|
-
"
|
|
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
|
|
@@ -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:
|
|
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
|
|
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
|