sendly 3.38.0__tar.gz → 3.40.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.
- {sendly-3.38.0 → sendly-3.40.0}/PKG-INFO +194 -6
- {sendly-3.38.0 → sendly-3.40.0}/README.md +193 -5
- {sendly-3.38.0 → sendly-3.40.0}/pyproject.toml +1 -1
- {sendly-3.38.0 → sendly-3.40.0}/sendly/__init__.py +49 -1
- {sendly-3.38.0 → sendly-3.40.0}/sendly/errors.py +14 -1
- sendly-3.40.0/sendly/py.typed +0 -0
- sendly-3.40.0/sendly/resources/rcs.py +903 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/types.py +540 -1
- {sendly-3.38.0 → sendly-3.40.0}/sendly/utils/http.py +1 -1
- sendly-3.40.0/sendly/webhooks.py +495 -0
- sendly-3.40.0/tests/test_rcs_registration.py +1415 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_webhooks.py +266 -4
- sendly-3.38.0/sendly/resources/rcs.py +0 -154
- sendly-3.38.0/sendly/webhooks.py +0 -364
- {sendly-3.38.0 → sendly-3.40.0}/.github/header.svg +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/.gitignore +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/CHANGELOG.md +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/examples/async_example.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/examples/error_handling.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/examples/send_sms.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/client.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/__init__.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/account.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/business_upgrade.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/campaigns.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/contacts.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/conversations.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/drafts.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/enterprise.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/labels.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/links.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/media.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/messages.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/numbers.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/rules.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/templates.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/tendlc.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/verify.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/webhooks.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/whatsapp.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/utils/__init__.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/sendly/utils/validation.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/conftest.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_account.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_async_client.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_client.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_errors.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_http_body_kwarg.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_idempotency.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages.py.bak +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages_batch.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages_schedule.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_numbers.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_rcs.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_tendlc.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_validation.py +0 -0
- {sendly-3.38.0 → sendly-3.40.0}/tests/test_whatsapp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sendly
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.40.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
|
|
@@ -404,6 +404,60 @@ def handle_webhook():
|
|
|
404
404
|
return 'Invalid signature', 400
|
|
405
405
|
```
|
|
406
406
|
|
|
407
|
+
### Reading the Event Payload
|
|
408
|
+
|
|
409
|
+
`event.object` is `data.object` exactly as it arrived, for **every** event type.
|
|
410
|
+
Keys are verbatim (`camelCase` stays `camelCase`), JSON `null` stays `None`, and
|
|
411
|
+
nothing the payload did not carry is added.
|
|
412
|
+
|
|
413
|
+
`event.data` is the *message* view. It is `None` for every event that is not a
|
|
414
|
+
`message.*` event, because RCS, WhatsApp, voice (`call.*`), 10DLC
|
|
415
|
+
(`brand.*` / `campaign.*` / `assignment.*`), `number.*`, `port*`, `contact*`,
|
|
416
|
+
`conversation.*`, `draft.*` and `verification.*` payloads are not
|
|
417
|
+
message-shaped.
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
from dataclasses import dataclass
|
|
421
|
+
from sendly import Webhooks
|
|
422
|
+
|
|
423
|
+
event = Webhooks.parse_event(payload, signature, WEBHOOK_SECRET, timestamp=timestamp)
|
|
424
|
+
|
|
425
|
+
if event.data is not None:
|
|
426
|
+
# message.* event
|
|
427
|
+
print(event.data.id, event.data.to, event.data.credits_used)
|
|
428
|
+
else:
|
|
429
|
+
# lifecycle event: read the raw object
|
|
430
|
+
print(event.object)
|
|
431
|
+
|
|
432
|
+
# A contact flagged by a send failure: `id` is the contact, `message_id` is
|
|
433
|
+
# the message that failed. They are never swapped.
|
|
434
|
+
if event.type == 'contact.auto_flagged':
|
|
435
|
+
contact_id = event.object['id']
|
|
436
|
+
failed_message_id = event.object['message_id']
|
|
437
|
+
|
|
438
|
+
# In-app calls legitimately have no numbers; null stays None, never ''
|
|
439
|
+
if event.type == 'call.started':
|
|
440
|
+
# from/to are null for in-app (browser) calls and carry E.164 numbers
|
|
441
|
+
# for PSTN legs, so treat them as optional rather than assuming either.
|
|
442
|
+
caller = event.object.get('from')
|
|
443
|
+
|
|
444
|
+
# Or decode into a shape of your own (dataclass, pydantic model, or dict)
|
|
445
|
+
@dataclass
|
|
446
|
+
class AgentLive:
|
|
447
|
+
agent_id: str = None
|
|
448
|
+
name: str = None
|
|
449
|
+
stage: str = None
|
|
450
|
+
|
|
451
|
+
if event.type == 'rcs_agent.live':
|
|
452
|
+
agent = event.object_as(AgentLive)
|
|
453
|
+
print(agent.agent_id, agent.stage)
|
|
454
|
+
|
|
455
|
+
raw = event.object_as() # plain dict copy
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Event types live in one place, `sendly.types.WebhookEventType`; `sendly.WebhookEventType`
|
|
459
|
+
re-exports that enum, and `sendly.webhooks.WEBHOOK_EVENT_TYPES` is the tuple of its values.
|
|
460
|
+
|
|
407
461
|
## Account & Credits
|
|
408
462
|
|
|
409
463
|
```python
|
|
@@ -690,11 +744,13 @@ client.whatsapp.senders.update_profile(
|
|
|
690
744
|
|
|
691
745
|
Send branded, verified-sender messages on Android: rich cards, suggestion
|
|
692
746
|
chips, and read receipts. Sending as your brand requires an RCS agent (the
|
|
693
|
-
verified identity recipients see)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
747
|
+
verified identity recipients see). Registration is self-serve, from the
|
|
748
|
+
dashboard or from this SDK: you draft a brand and an agent, Sendly reviews
|
|
749
|
+
them, then they go to the carrier network for verification (see
|
|
750
|
+
[Registering an agent](#registering-an-agent) below). Text messages
|
|
751
|
+
automatically fall back to SMS when the recipient's device or network doesn't
|
|
752
|
+
support RCS (billed as SMS; suggestion chips are dropped); rich cards have no
|
|
753
|
+
SMS form and respond 422 instead. Sending requires a live API key.
|
|
698
754
|
|
|
699
755
|
```python
|
|
700
756
|
# Your registered agents ('testing' or 'approved' agents are sendable)
|
|
@@ -749,6 +805,88 @@ client.messages.send(
|
|
|
749
805
|
)
|
|
750
806
|
```
|
|
751
807
|
|
|
808
|
+
### Registering an agent
|
|
809
|
+
|
|
810
|
+
Registration needs an API key with the `rcs:read` / `rcs:write` scopes and is
|
|
811
|
+
open to US businesses. It is rolling out gradually: until it is enabled for
|
|
812
|
+
your account, every registration call raises `SendlyError` with code
|
|
813
|
+
`rcs_not_enabled` (HTTP 404). Logo, hero image and call-to-action media must
|
|
814
|
+
already be public `https://` URLs; uploading assets is a dashboard-only step.
|
|
815
|
+
|
|
816
|
+
```python
|
|
817
|
+
from sendly import RcsCustomerStage
|
|
818
|
+
|
|
819
|
+
# 1. Prefill from what Sendly already knows (your 10DLC brand or toll-free verification)
|
|
820
|
+
dossier = client.rcs.dossier.get()
|
|
821
|
+
|
|
822
|
+
# 2. Draft the brand (nested address/contact accept dicts or RcsBrandAddress/RcsBrandContact)
|
|
823
|
+
brand = client.rcs.brands.create(
|
|
824
|
+
display_name='Acme Coffee',
|
|
825
|
+
legal_name='Acme Holdings LLC',
|
|
826
|
+
legal_entity_type='LIMITED_LIABILITY_COMPANY',
|
|
827
|
+
organization_type='PRIVATE_PROFIT',
|
|
828
|
+
website_url='https://acme.example',
|
|
829
|
+
ein='12-3456789',
|
|
830
|
+
address={'line1': '1 Main St', 'city': 'Austin', 'state': 'TX',
|
|
831
|
+
'postal_code': '78701', 'country_code': 'US'},
|
|
832
|
+
contact={'first_name': 'Sam', 'last_name': 'Lee',
|
|
833
|
+
'email': 'sam@acme.example', 'phone_number': '+15551234567'},
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
# 3. Draft the agent under it
|
|
837
|
+
agent = client.rcs.agents.create(
|
|
838
|
+
brand.id,
|
|
839
|
+
display_name='Acme Coffee',
|
|
840
|
+
use_case='MULTI_USE',
|
|
841
|
+
basics={
|
|
842
|
+
'description': 'Order updates and support for Acme customers',
|
|
843
|
+
'logo_url': 'https://acme.example/logo.png',
|
|
844
|
+
'hero_url': 'https://acme.example/hero.png',
|
|
845
|
+
'brand_color': '#5B3A29',
|
|
846
|
+
'privacy_policy_url': 'https://acme.example/privacy',
|
|
847
|
+
'terms_and_conditions_url': 'https://acme.example/terms',
|
|
848
|
+
'website': {'url': 'https://acme.example', 'label': 'Acme'},
|
|
849
|
+
},
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# 4. Submit for review; poll the stage (SendlyError 'rcs_invalid_content'
|
|
853
|
+
# lists anything missing in e.field_errors)
|
|
854
|
+
client.rcs.agents.submit(agent.id)
|
|
855
|
+
registration = client.rcs.registration.get()
|
|
856
|
+
print(registration.stage) # 'in_review' -> 'brand_verification' -> 'agent_review' -> 'testing'
|
|
857
|
+
|
|
858
|
+
# 5. Once in testing: invite devices, describe the campaign, then request launch
|
|
859
|
+
if registration.stage == RcsCustomerStage.TESTING:
|
|
860
|
+
client.rcs.agents.set_test_devices(agent.id, [
|
|
861
|
+
'+15551234567',
|
|
862
|
+
{'phone_number': '+15557654321', 'label': 'Sam'},
|
|
863
|
+
])
|
|
864
|
+
client.rcs.agents.update(
|
|
865
|
+
agent.id,
|
|
866
|
+
campaign={
|
|
867
|
+
'agent_overview': 'Order updates and support replies',
|
|
868
|
+
'interactions': [{'interaction_type': 'TRANSACTIONAL_UPDATES',
|
|
869
|
+
'description': 'Shipping and delivery updates'}],
|
|
870
|
+
'message_examples': ['Your order #123 has shipped!',
|
|
871
|
+
'Your table is ready!',
|
|
872
|
+
'Reply HELP for help'],
|
|
873
|
+
'consent_settings': {
|
|
874
|
+
'opt_in_methods': [{'method_type': 'WEBSITE',
|
|
875
|
+
'description': 'Checkout checkbox'}],
|
|
876
|
+
'opt_out_response': 'You are unsubscribed.',
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
)
|
|
880
|
+
client.rcs.agents.request_launch(
|
|
881
|
+
agent.id, test_url='https://acme.example/rcs-test-recording'
|
|
882
|
+
)
|
|
883
|
+
# stage moves through 'launch_review' and 'launching' to 'live'
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
Writes accept an `idempotency_key` like the other write methods; `submit()` and
|
|
887
|
+
`request_launch()` replays return the original response without notifying the
|
|
888
|
+
reviewers again.
|
|
889
|
+
|
|
752
890
|
## Error Handling
|
|
753
891
|
|
|
754
892
|
The SDK provides typed exception classes:
|
|
@@ -993,6 +1131,56 @@ Assign a number you own to an active campaign, making the number sendable. Idemp
|
|
|
993
1131
|
|
|
994
1132
|
List your number-to-campaign assignments.
|
|
995
1133
|
|
|
1134
|
+
### `client.rcs`
|
|
1135
|
+
|
|
1136
|
+
#### `agents.list() -> RcsAgentListResponse`
|
|
1137
|
+
|
|
1138
|
+
List your RCS agents, newest first, with `sendable` telling you which can send now.
|
|
1139
|
+
|
|
1140
|
+
#### `capability(to, agent_id=None) -> RcsCapability`
|
|
1141
|
+
|
|
1142
|
+
Check whether a recipient can receive RCS (live key).
|
|
1143
|
+
|
|
1144
|
+
#### `registration.get() -> RcsRegistration`
|
|
1145
|
+
|
|
1146
|
+
The registration at a glance: newest brand, newest agent, its test devices, and the overall `stage` (`RcsCustomerStage`).
|
|
1147
|
+
|
|
1148
|
+
#### `dossier.get() -> RcsDossier`
|
|
1149
|
+
|
|
1150
|
+
Business details Sendly already holds for the workspace, to prefill `brands.create()`.
|
|
1151
|
+
|
|
1152
|
+
#### `brands.create(display_name=None, legal_name=None, ..., address=None, contact=None, idempotency_key=None) -> RcsBrand`
|
|
1153
|
+
|
|
1154
|
+
Draft a brand (US businesses only). Required fields are checked at submit, not here.
|
|
1155
|
+
|
|
1156
|
+
#### `brands.update(id, ..., idempotency_key=None) -> RcsBrand`
|
|
1157
|
+
|
|
1158
|
+
Edit a draft brand; only the fields you pass change. Locked while under review (`rcs_field_locked`).
|
|
1159
|
+
|
|
1160
|
+
#### `agents.create(brand_id, display_name=None, use_case=None, basics=None, campaign=None, testing=None, idempotency_key=None) -> RcsAgentDetail`
|
|
1161
|
+
|
|
1162
|
+
Draft an agent under a brand. Media must be public `https://` URLs.
|
|
1163
|
+
|
|
1164
|
+
#### `agents.get(id) -> RcsAgentDetail`
|
|
1165
|
+
|
|
1166
|
+
Fetch one agent with its `review_status`, `customer_stage`, `review_note` and `test_devices`.
|
|
1167
|
+
|
|
1168
|
+
#### `agents.update(id, display_name=None, use_case=None, basics=None, campaign=None, testing=None, idempotency_key=None) -> RcsAgentDetail`
|
|
1169
|
+
|
|
1170
|
+
Edit an agent; `campaign` and `testing` merge section-wise.
|
|
1171
|
+
|
|
1172
|
+
#### `agents.set_test_devices(id, devices, idempotency_key=None) -> RcsTestDeviceListResponse`
|
|
1173
|
+
|
|
1174
|
+
Replace the invited test devices (up to 20); entries are E.164 strings, dicts, or `RcsTestDeviceInput`.
|
|
1175
|
+
|
|
1176
|
+
#### `agents.submit(id, idempotency_key=None) -> RcsAgentDetail`
|
|
1177
|
+
|
|
1178
|
+
Submit the agent and its brand for review by Sendly, then the carrier network. `rcs_invalid_content` lists what is missing.
|
|
1179
|
+
|
|
1180
|
+
#### `agents.request_launch(id, test_url=None, testing_additional_information=None, idempotency_key=None) -> RcsAgentDetail`
|
|
1181
|
+
|
|
1182
|
+
Ask for the launch review once the agent is in `testing` and has been tried on an invited device.
|
|
1183
|
+
|
|
996
1184
|
## Enterprise
|
|
997
1185
|
|
|
998
1186
|
The Enterprise API lets you programmatically manage workspaces, verification, credits, and API keys for multi-tenant platforms. Requires an enterprise master key (`sk_live_v1_master_*`).
|
|
@@ -369,6 +369,60 @@ def handle_webhook():
|
|
|
369
369
|
return 'Invalid signature', 400
|
|
370
370
|
```
|
|
371
371
|
|
|
372
|
+
### Reading the Event Payload
|
|
373
|
+
|
|
374
|
+
`event.object` is `data.object` exactly as it arrived, for **every** event type.
|
|
375
|
+
Keys are verbatim (`camelCase` stays `camelCase`), JSON `null` stays `None`, and
|
|
376
|
+
nothing the payload did not carry is added.
|
|
377
|
+
|
|
378
|
+
`event.data` is the *message* view. It is `None` for every event that is not a
|
|
379
|
+
`message.*` event, because RCS, WhatsApp, voice (`call.*`), 10DLC
|
|
380
|
+
(`brand.*` / `campaign.*` / `assignment.*`), `number.*`, `port*`, `contact*`,
|
|
381
|
+
`conversation.*`, `draft.*` and `verification.*` payloads are not
|
|
382
|
+
message-shaped.
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
from dataclasses import dataclass
|
|
386
|
+
from sendly import Webhooks
|
|
387
|
+
|
|
388
|
+
event = Webhooks.parse_event(payload, signature, WEBHOOK_SECRET, timestamp=timestamp)
|
|
389
|
+
|
|
390
|
+
if event.data is not None:
|
|
391
|
+
# message.* event
|
|
392
|
+
print(event.data.id, event.data.to, event.data.credits_used)
|
|
393
|
+
else:
|
|
394
|
+
# lifecycle event: read the raw object
|
|
395
|
+
print(event.object)
|
|
396
|
+
|
|
397
|
+
# A contact flagged by a send failure: `id` is the contact, `message_id` is
|
|
398
|
+
# the message that failed. They are never swapped.
|
|
399
|
+
if event.type == 'contact.auto_flagged':
|
|
400
|
+
contact_id = event.object['id']
|
|
401
|
+
failed_message_id = event.object['message_id']
|
|
402
|
+
|
|
403
|
+
# In-app calls legitimately have no numbers; null stays None, never ''
|
|
404
|
+
if event.type == 'call.started':
|
|
405
|
+
# from/to are null for in-app (browser) calls and carry E.164 numbers
|
|
406
|
+
# for PSTN legs, so treat them as optional rather than assuming either.
|
|
407
|
+
caller = event.object.get('from')
|
|
408
|
+
|
|
409
|
+
# Or decode into a shape of your own (dataclass, pydantic model, or dict)
|
|
410
|
+
@dataclass
|
|
411
|
+
class AgentLive:
|
|
412
|
+
agent_id: str = None
|
|
413
|
+
name: str = None
|
|
414
|
+
stage: str = None
|
|
415
|
+
|
|
416
|
+
if event.type == 'rcs_agent.live':
|
|
417
|
+
agent = event.object_as(AgentLive)
|
|
418
|
+
print(agent.agent_id, agent.stage)
|
|
419
|
+
|
|
420
|
+
raw = event.object_as() # plain dict copy
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Event types live in one place, `sendly.types.WebhookEventType`; `sendly.WebhookEventType`
|
|
424
|
+
re-exports that enum, and `sendly.webhooks.WEBHOOK_EVENT_TYPES` is the tuple of its values.
|
|
425
|
+
|
|
372
426
|
## Account & Credits
|
|
373
427
|
|
|
374
428
|
```python
|
|
@@ -655,11 +709,13 @@ client.whatsapp.senders.update_profile(
|
|
|
655
709
|
|
|
656
710
|
Send branded, verified-sender messages on Android: rich cards, suggestion
|
|
657
711
|
chips, and read receipts. Sending as your brand requires an RCS agent (the
|
|
658
|
-
verified identity recipients see)
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
712
|
+
verified identity recipients see). Registration is self-serve, from the
|
|
713
|
+
dashboard or from this SDK: you draft a brand and an agent, Sendly reviews
|
|
714
|
+
them, then they go to the carrier network for verification (see
|
|
715
|
+
[Registering an agent](#registering-an-agent) below). Text messages
|
|
716
|
+
automatically fall back to SMS when the recipient's device or network doesn't
|
|
717
|
+
support RCS (billed as SMS; suggestion chips are dropped); rich cards have no
|
|
718
|
+
SMS form and respond 422 instead. Sending requires a live API key.
|
|
663
719
|
|
|
664
720
|
```python
|
|
665
721
|
# Your registered agents ('testing' or 'approved' agents are sendable)
|
|
@@ -714,6 +770,88 @@ client.messages.send(
|
|
|
714
770
|
)
|
|
715
771
|
```
|
|
716
772
|
|
|
773
|
+
### Registering an agent
|
|
774
|
+
|
|
775
|
+
Registration needs an API key with the `rcs:read` / `rcs:write` scopes and is
|
|
776
|
+
open to US businesses. It is rolling out gradually: until it is enabled for
|
|
777
|
+
your account, every registration call raises `SendlyError` with code
|
|
778
|
+
`rcs_not_enabled` (HTTP 404). Logo, hero image and call-to-action media must
|
|
779
|
+
already be public `https://` URLs; uploading assets is a dashboard-only step.
|
|
780
|
+
|
|
781
|
+
```python
|
|
782
|
+
from sendly import RcsCustomerStage
|
|
783
|
+
|
|
784
|
+
# 1. Prefill from what Sendly already knows (your 10DLC brand or toll-free verification)
|
|
785
|
+
dossier = client.rcs.dossier.get()
|
|
786
|
+
|
|
787
|
+
# 2. Draft the brand (nested address/contact accept dicts or RcsBrandAddress/RcsBrandContact)
|
|
788
|
+
brand = client.rcs.brands.create(
|
|
789
|
+
display_name='Acme Coffee',
|
|
790
|
+
legal_name='Acme Holdings LLC',
|
|
791
|
+
legal_entity_type='LIMITED_LIABILITY_COMPANY',
|
|
792
|
+
organization_type='PRIVATE_PROFIT',
|
|
793
|
+
website_url='https://acme.example',
|
|
794
|
+
ein='12-3456789',
|
|
795
|
+
address={'line1': '1 Main St', 'city': 'Austin', 'state': 'TX',
|
|
796
|
+
'postal_code': '78701', 'country_code': 'US'},
|
|
797
|
+
contact={'first_name': 'Sam', 'last_name': 'Lee',
|
|
798
|
+
'email': 'sam@acme.example', 'phone_number': '+15551234567'},
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
# 3. Draft the agent under it
|
|
802
|
+
agent = client.rcs.agents.create(
|
|
803
|
+
brand.id,
|
|
804
|
+
display_name='Acme Coffee',
|
|
805
|
+
use_case='MULTI_USE',
|
|
806
|
+
basics={
|
|
807
|
+
'description': 'Order updates and support for Acme customers',
|
|
808
|
+
'logo_url': 'https://acme.example/logo.png',
|
|
809
|
+
'hero_url': 'https://acme.example/hero.png',
|
|
810
|
+
'brand_color': '#5B3A29',
|
|
811
|
+
'privacy_policy_url': 'https://acme.example/privacy',
|
|
812
|
+
'terms_and_conditions_url': 'https://acme.example/terms',
|
|
813
|
+
'website': {'url': 'https://acme.example', 'label': 'Acme'},
|
|
814
|
+
},
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# 4. Submit for review; poll the stage (SendlyError 'rcs_invalid_content'
|
|
818
|
+
# lists anything missing in e.field_errors)
|
|
819
|
+
client.rcs.agents.submit(agent.id)
|
|
820
|
+
registration = client.rcs.registration.get()
|
|
821
|
+
print(registration.stage) # 'in_review' -> 'brand_verification' -> 'agent_review' -> 'testing'
|
|
822
|
+
|
|
823
|
+
# 5. Once in testing: invite devices, describe the campaign, then request launch
|
|
824
|
+
if registration.stage == RcsCustomerStage.TESTING:
|
|
825
|
+
client.rcs.agents.set_test_devices(agent.id, [
|
|
826
|
+
'+15551234567',
|
|
827
|
+
{'phone_number': '+15557654321', 'label': 'Sam'},
|
|
828
|
+
])
|
|
829
|
+
client.rcs.agents.update(
|
|
830
|
+
agent.id,
|
|
831
|
+
campaign={
|
|
832
|
+
'agent_overview': 'Order updates and support replies',
|
|
833
|
+
'interactions': [{'interaction_type': 'TRANSACTIONAL_UPDATES',
|
|
834
|
+
'description': 'Shipping and delivery updates'}],
|
|
835
|
+
'message_examples': ['Your order #123 has shipped!',
|
|
836
|
+
'Your table is ready!',
|
|
837
|
+
'Reply HELP for help'],
|
|
838
|
+
'consent_settings': {
|
|
839
|
+
'opt_in_methods': [{'method_type': 'WEBSITE',
|
|
840
|
+
'description': 'Checkout checkbox'}],
|
|
841
|
+
'opt_out_response': 'You are unsubscribed.',
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
)
|
|
845
|
+
client.rcs.agents.request_launch(
|
|
846
|
+
agent.id, test_url='https://acme.example/rcs-test-recording'
|
|
847
|
+
)
|
|
848
|
+
# stage moves through 'launch_review' and 'launching' to 'live'
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
Writes accept an `idempotency_key` like the other write methods; `submit()` and
|
|
852
|
+
`request_launch()` replays return the original response without notifying the
|
|
853
|
+
reviewers again.
|
|
854
|
+
|
|
717
855
|
## Error Handling
|
|
718
856
|
|
|
719
857
|
The SDK provides typed exception classes:
|
|
@@ -958,6 +1096,56 @@ Assign a number you own to an active campaign, making the number sendable. Idemp
|
|
|
958
1096
|
|
|
959
1097
|
List your number-to-campaign assignments.
|
|
960
1098
|
|
|
1099
|
+
### `client.rcs`
|
|
1100
|
+
|
|
1101
|
+
#### `agents.list() -> RcsAgentListResponse`
|
|
1102
|
+
|
|
1103
|
+
List your RCS agents, newest first, with `sendable` telling you which can send now.
|
|
1104
|
+
|
|
1105
|
+
#### `capability(to, agent_id=None) -> RcsCapability`
|
|
1106
|
+
|
|
1107
|
+
Check whether a recipient can receive RCS (live key).
|
|
1108
|
+
|
|
1109
|
+
#### `registration.get() -> RcsRegistration`
|
|
1110
|
+
|
|
1111
|
+
The registration at a glance: newest brand, newest agent, its test devices, and the overall `stage` (`RcsCustomerStage`).
|
|
1112
|
+
|
|
1113
|
+
#### `dossier.get() -> RcsDossier`
|
|
1114
|
+
|
|
1115
|
+
Business details Sendly already holds for the workspace, to prefill `brands.create()`.
|
|
1116
|
+
|
|
1117
|
+
#### `brands.create(display_name=None, legal_name=None, ..., address=None, contact=None, idempotency_key=None) -> RcsBrand`
|
|
1118
|
+
|
|
1119
|
+
Draft a brand (US businesses only). Required fields are checked at submit, not here.
|
|
1120
|
+
|
|
1121
|
+
#### `brands.update(id, ..., idempotency_key=None) -> RcsBrand`
|
|
1122
|
+
|
|
1123
|
+
Edit a draft brand; only the fields you pass change. Locked while under review (`rcs_field_locked`).
|
|
1124
|
+
|
|
1125
|
+
#### `agents.create(brand_id, display_name=None, use_case=None, basics=None, campaign=None, testing=None, idempotency_key=None) -> RcsAgentDetail`
|
|
1126
|
+
|
|
1127
|
+
Draft an agent under a brand. Media must be public `https://` URLs.
|
|
1128
|
+
|
|
1129
|
+
#### `agents.get(id) -> RcsAgentDetail`
|
|
1130
|
+
|
|
1131
|
+
Fetch one agent with its `review_status`, `customer_stage`, `review_note` and `test_devices`.
|
|
1132
|
+
|
|
1133
|
+
#### `agents.update(id, display_name=None, use_case=None, basics=None, campaign=None, testing=None, idempotency_key=None) -> RcsAgentDetail`
|
|
1134
|
+
|
|
1135
|
+
Edit an agent; `campaign` and `testing` merge section-wise.
|
|
1136
|
+
|
|
1137
|
+
#### `agents.set_test_devices(id, devices, idempotency_key=None) -> RcsTestDeviceListResponse`
|
|
1138
|
+
|
|
1139
|
+
Replace the invited test devices (up to 20); entries are E.164 strings, dicts, or `RcsTestDeviceInput`.
|
|
1140
|
+
|
|
1141
|
+
#### `agents.submit(id, idempotency_key=None) -> RcsAgentDetail`
|
|
1142
|
+
|
|
1143
|
+
Submit the agent and its brand for review by Sendly, then the carrier network. `rcs_invalid_content` lists what is missing.
|
|
1144
|
+
|
|
1145
|
+
#### `agents.request_launch(id, test_url=None, testing_additional_information=None, idempotency_key=None) -> RcsAgentDetail`
|
|
1146
|
+
|
|
1147
|
+
Ask for the launch review once the agent is in `testing` and has been tried on an invited device.
|
|
1148
|
+
|
|
961
1149
|
## Enterprise
|
|
962
1150
|
|
|
963
1151
|
The Enterprise API lets you programmatically manage workspaces, verification, credits, and API keys for multi-tenant platforms. Requires an enterprise master key (`sk_live_v1_master_*`).
|
|
@@ -29,7 +29,7 @@ Async Example:
|
|
|
29
29
|
>>> asyncio.run(main())
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
__version__ = "3.
|
|
32
|
+
__version__ = "3.40.0"
|
|
33
33
|
|
|
34
34
|
# Main clients
|
|
35
35
|
from .client import AsyncSendly, Sendly
|
|
@@ -172,6 +172,28 @@ from .types import (
|
|
|
172
172
|
RcsCapability,
|
|
173
173
|
RcsMessage,
|
|
174
174
|
RcsMessageDetails,
|
|
175
|
+
# RCS registration types
|
|
176
|
+
RcsAgentBasics,
|
|
177
|
+
RcsAgentDetail,
|
|
178
|
+
RcsAgentEmail,
|
|
179
|
+
RcsAgentPhoneNumber,
|
|
180
|
+
RcsAgentWebsite,
|
|
181
|
+
RcsBrand,
|
|
182
|
+
RcsBrandAddress,
|
|
183
|
+
RcsBrandContact,
|
|
184
|
+
RcsBrandPrefill,
|
|
185
|
+
RcsCampaign,
|
|
186
|
+
RcsConsentSettings,
|
|
187
|
+
RcsCustomerStage,
|
|
188
|
+
RcsDossier,
|
|
189
|
+
RcsInteraction,
|
|
190
|
+
RcsOptInMethod,
|
|
191
|
+
RcsRegistration,
|
|
192
|
+
RcsReviewStatus,
|
|
193
|
+
RcsTestDevice,
|
|
194
|
+
RcsTestDeviceInput,
|
|
195
|
+
RcsTestDeviceListResponse,
|
|
196
|
+
RcsTesting,
|
|
175
197
|
OptInPage,
|
|
176
198
|
PricingTier,
|
|
177
199
|
QuotaSettings,
|
|
@@ -216,6 +238,8 @@ from .webhooks import (
|
|
|
216
238
|
WebhookMessageStatus,
|
|
217
239
|
Webhooks,
|
|
218
240
|
WebhookSignatureError,
|
|
241
|
+
WebhookVerificationData,
|
|
242
|
+
is_message_event,
|
|
219
243
|
)
|
|
220
244
|
|
|
221
245
|
__all__ = [
|
|
@@ -360,6 +384,28 @@ __all__ = [
|
|
|
360
384
|
"RcsCapability",
|
|
361
385
|
"RcsMessage",
|
|
362
386
|
"RcsMessageDetails",
|
|
387
|
+
# RCS registration types
|
|
388
|
+
"RcsAgentBasics",
|
|
389
|
+
"RcsAgentDetail",
|
|
390
|
+
"RcsAgentEmail",
|
|
391
|
+
"RcsAgentPhoneNumber",
|
|
392
|
+
"RcsAgentWebsite",
|
|
393
|
+
"RcsBrand",
|
|
394
|
+
"RcsBrandAddress",
|
|
395
|
+
"RcsBrandContact",
|
|
396
|
+
"RcsBrandPrefill",
|
|
397
|
+
"RcsCampaign",
|
|
398
|
+
"RcsConsentSettings",
|
|
399
|
+
"RcsCustomerStage",
|
|
400
|
+
"RcsDossier",
|
|
401
|
+
"RcsInteraction",
|
|
402
|
+
"RcsOptInMethod",
|
|
403
|
+
"RcsRegistration",
|
|
404
|
+
"RcsReviewStatus",
|
|
405
|
+
"RcsTestDevice",
|
|
406
|
+
"RcsTestDeviceInput",
|
|
407
|
+
"RcsTestDeviceListResponse",
|
|
408
|
+
"RcsTesting",
|
|
363
409
|
# Links resources
|
|
364
410
|
"LinksResource",
|
|
365
411
|
"AsyncLinksResource",
|
|
@@ -404,4 +450,6 @@ __all__ = [
|
|
|
404
450
|
"WebhookEventType",
|
|
405
451
|
"WebhookMessageData",
|
|
406
452
|
"WebhookMessageStatus",
|
|
453
|
+
"WebhookVerificationData",
|
|
454
|
+
"is_message_event",
|
|
407
455
|
]
|
|
@@ -4,7 +4,7 @@ Sendly SDK Error Classes
|
|
|
4
4
|
Custom exceptions for different error scenarios.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
from .types import ApiErrorResponse
|
|
10
10
|
|
|
@@ -33,6 +33,19 @@ class SendlyError(Exception):
|
|
|
33
33
|
def __repr__(self) -> str:
|
|
34
34
|
return f"{self.__class__.__name__}(code={self.code!r}, message={self.message!r})"
|
|
35
35
|
|
|
36
|
+
@property
|
|
37
|
+
def field_errors(self) -> List[Dict[str, Any]]:
|
|
38
|
+
"""Per-field problems the API attached to this error, as
|
|
39
|
+
``[{"path": ..., "message": ...}]``. Empty unless the response carried
|
|
40
|
+
an ``errors`` list (for example ``rcs_invalid_content``)."""
|
|
41
|
+
if self.response is None:
|
|
42
|
+
return []
|
|
43
|
+
extra = self.response.model_extra or {}
|
|
44
|
+
errors = extra.get("errors")
|
|
45
|
+
if not isinstance(errors, list):
|
|
46
|
+
return []
|
|
47
|
+
return [e for e in errors if isinstance(e, dict)]
|
|
48
|
+
|
|
36
49
|
@classmethod
|
|
37
50
|
def from_response(cls, status_code: int, response_data: Dict[str, Any]) -> "SendlyError":
|
|
38
51
|
"""Create a SendlyError from an API response"""
|
|
File without changes
|