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.
Files changed (58) hide show
  1. {sendly-3.38.0 → sendly-3.40.0}/PKG-INFO +194 -6
  2. {sendly-3.38.0 → sendly-3.40.0}/README.md +193 -5
  3. {sendly-3.38.0 → sendly-3.40.0}/pyproject.toml +1 -1
  4. {sendly-3.38.0 → sendly-3.40.0}/sendly/__init__.py +49 -1
  5. {sendly-3.38.0 → sendly-3.40.0}/sendly/errors.py +14 -1
  6. sendly-3.40.0/sendly/py.typed +0 -0
  7. sendly-3.40.0/sendly/resources/rcs.py +903 -0
  8. {sendly-3.38.0 → sendly-3.40.0}/sendly/types.py +540 -1
  9. {sendly-3.38.0 → sendly-3.40.0}/sendly/utils/http.py +1 -1
  10. sendly-3.40.0/sendly/webhooks.py +495 -0
  11. sendly-3.40.0/tests/test_rcs_registration.py +1415 -0
  12. {sendly-3.38.0 → sendly-3.40.0}/tests/test_webhooks.py +266 -4
  13. sendly-3.38.0/sendly/resources/rcs.py +0 -154
  14. sendly-3.38.0/sendly/webhooks.py +0 -364
  15. {sendly-3.38.0 → sendly-3.40.0}/.github/header.svg +0 -0
  16. {sendly-3.38.0 → sendly-3.40.0}/.gitignore +0 -0
  17. {sendly-3.38.0 → sendly-3.40.0}/CHANGELOG.md +0 -0
  18. {sendly-3.38.0 → sendly-3.40.0}/examples/async_example.py +0 -0
  19. {sendly-3.38.0 → sendly-3.40.0}/examples/error_handling.py +0 -0
  20. {sendly-3.38.0 → sendly-3.40.0}/examples/send_sms.py +0 -0
  21. {sendly-3.38.0 → sendly-3.40.0}/sendly/client.py +0 -0
  22. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/__init__.py +0 -0
  23. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/account.py +0 -0
  24. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/business_upgrade.py +0 -0
  25. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/campaigns.py +0 -0
  26. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/contacts.py +0 -0
  27. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/conversations.py +0 -0
  28. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/drafts.py +0 -0
  29. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/enterprise.py +0 -0
  30. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/labels.py +0 -0
  31. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/links.py +0 -0
  32. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/media.py +0 -0
  33. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/messages.py +0 -0
  34. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/numbers.py +0 -0
  35. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/rules.py +0 -0
  36. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/templates.py +0 -0
  37. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/tendlc.py +0 -0
  38. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/verify.py +0 -0
  39. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/webhooks.py +0 -0
  40. {sendly-3.38.0 → sendly-3.40.0}/sendly/resources/whatsapp.py +0 -0
  41. {sendly-3.38.0 → sendly-3.40.0}/sendly/utils/__init__.py +0 -0
  42. {sendly-3.38.0 → sendly-3.40.0}/sendly/utils/validation.py +0 -0
  43. {sendly-3.38.0 → sendly-3.40.0}/tests/conftest.py +0 -0
  44. {sendly-3.38.0 → sendly-3.40.0}/tests/test_account.py +0 -0
  45. {sendly-3.38.0 → sendly-3.40.0}/tests/test_async_client.py +0 -0
  46. {sendly-3.38.0 → sendly-3.40.0}/tests/test_client.py +0 -0
  47. {sendly-3.38.0 → sendly-3.40.0}/tests/test_errors.py +0 -0
  48. {sendly-3.38.0 → sendly-3.40.0}/tests/test_http_body_kwarg.py +0 -0
  49. {sendly-3.38.0 → sendly-3.40.0}/tests/test_idempotency.py +0 -0
  50. {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages.py +0 -0
  51. {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages.py.bak +0 -0
  52. {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages_batch.py +0 -0
  53. {sendly-3.38.0 → sendly-3.40.0}/tests/test_messages_schedule.py +0 -0
  54. {sendly-3.38.0 → sendly-3.40.0}/tests/test_numbers.py +0 -0
  55. {sendly-3.38.0 → sendly-3.40.0}/tests/test_rcs.py +0 -0
  56. {sendly-3.38.0 → sendly-3.40.0}/tests/test_tendlc.py +0 -0
  57. {sendly-3.38.0 → sendly-3.40.0}/tests/test_validation.py +0 -0
  58. {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.38.0
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), registered per workspace through carrier
694
- review - contact support to register one. Text messages automatically fall back
695
- to SMS when the recipient's device or network doesn't support RCS (billed as
696
- SMS; suggestion chips are dropped); rich cards have no SMS form and respond 422
697
- instead. Requires a live API key.
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), registered per workspace through carrier
659
- review - contact support to register one. Text messages automatically fall back
660
- to SMS when the recipient's device or network doesn't support RCS (billed as
661
- SMS; suggestion chips are dropped); rich cards have no SMS form and respond 422
662
- instead. Requires a live API key.
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_*`).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sendly"
7
- version = "3.38.0"
7
+ version = "3.40.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__ = "3.38.0"
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