sendly 4.1.0__tar.gz → 4.2.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 (60) hide show
  1. {sendly-4.1.0 → sendly-4.2.0}/CHANGELOG.md +11 -0
  2. {sendly-4.1.0 → sendly-4.2.0}/PKG-INFO +138 -10
  3. {sendly-4.1.0 → sendly-4.2.0}/README.md +137 -9
  4. {sendly-4.1.0 → sendly-4.2.0}/pyproject.toml +1 -1
  5. {sendly-4.1.0 → sendly-4.2.0}/sendly/__init__.py +37 -1
  6. {sendly-4.1.0 → sendly-4.2.0}/sendly/client.py +3 -0
  7. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/__init__.py +3 -0
  8. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/calls.py +6 -4
  9. sendly-4.2.0/sendly/resources/voice.py +642 -0
  10. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/whatsapp.py +1 -1
  11. {sendly-4.1.0 → sendly-4.2.0}/sendly/types.py +211 -0
  12. {sendly-4.1.0 → sendly-4.2.0}/sendly/utils/http.py +1 -1
  13. sendly-4.2.0/tests/test_voice.py +1001 -0
  14. {sendly-4.1.0 → sendly-4.2.0}/.github/header.svg +0 -0
  15. {sendly-4.1.0 → sendly-4.2.0}/.gitignore +0 -0
  16. {sendly-4.1.0 → sendly-4.2.0}/examples/async_example.py +0 -0
  17. {sendly-4.1.0 → sendly-4.2.0}/examples/error_handling.py +0 -0
  18. {sendly-4.1.0 → sendly-4.2.0}/examples/send_sms.py +0 -0
  19. {sendly-4.1.0 → sendly-4.2.0}/sendly/errors.py +0 -0
  20. {sendly-4.1.0 → sendly-4.2.0}/sendly/py.typed +0 -0
  21. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/account.py +0 -0
  22. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/business_upgrade.py +0 -0
  23. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/campaigns.py +0 -0
  24. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/contacts.py +0 -0
  25. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/conversations.py +0 -0
  26. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/drafts.py +0 -0
  27. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/enterprise.py +0 -0
  28. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/labels.py +0 -0
  29. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/links.py +0 -0
  30. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/media.py +0 -0
  31. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/messages.py +0 -0
  32. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/numbers.py +0 -0
  33. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/rcs.py +0 -0
  34. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/rules.py +0 -0
  35. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/templates.py +0 -0
  36. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/tendlc.py +0 -0
  37. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/verify.py +0 -0
  38. {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/webhooks.py +0 -0
  39. {sendly-4.1.0 → sendly-4.2.0}/sendly/utils/__init__.py +0 -0
  40. {sendly-4.1.0 → sendly-4.2.0}/sendly/utils/validation.py +0 -0
  41. {sendly-4.1.0 → sendly-4.2.0}/sendly/webhooks.py +0 -0
  42. {sendly-4.1.0 → sendly-4.2.0}/tests/conftest.py +0 -0
  43. {sendly-4.1.0 → sendly-4.2.0}/tests/test_account.py +0 -0
  44. {sendly-4.1.0 → sendly-4.2.0}/tests/test_async_client.py +0 -0
  45. {sendly-4.1.0 → sendly-4.2.0}/tests/test_calls.py +0 -0
  46. {sendly-4.1.0 → sendly-4.2.0}/tests/test_client.py +0 -0
  47. {sendly-4.1.0 → sendly-4.2.0}/tests/test_errors.py +0 -0
  48. {sendly-4.1.0 → sendly-4.2.0}/tests/test_http_body_kwarg.py +0 -0
  49. {sendly-4.1.0 → sendly-4.2.0}/tests/test_idempotency.py +0 -0
  50. {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages.py +0 -0
  51. {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages.py.bak +0 -0
  52. {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages_batch.py +0 -0
  53. {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages_schedule.py +0 -0
  54. {sendly-4.1.0 → sendly-4.2.0}/tests/test_numbers.py +0 -0
  55. {sendly-4.1.0 → sendly-4.2.0}/tests/test_rcs.py +0 -0
  56. {sendly-4.1.0 → sendly-4.2.0}/tests/test_rcs_registration.py +0 -0
  57. {sendly-4.1.0 → sendly-4.2.0}/tests/test_tendlc.py +0 -0
  58. {sendly-4.1.0 → sendly-4.2.0}/tests/test_validation.py +0 -0
  59. {sendly-4.1.0 → sendly-4.2.0}/tests/test_webhooks.py +0 -0
  60. {sendly-4.1.0 → sendly-4.2.0}/tests/test_whatsapp.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # sendly (Python)
2
2
 
3
+ ## 4.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Voice configuration: `client.voice`.** Everything a phone call depends on is now configurable from code. `voice.numbers.list()`, `get(number)`, `update(number, voice_enabled=..., voice_mode=..., agent_id=...)` and `register_emergency_address(number, street=..., unit=..., city=..., state=..., zip=..., country=...)` switch voice on for a number, choose how it answers and register the emergency address it needs before it can place calls; `number` is the number's id or its E.164 phone number. `voice.agents.list()`, `create(name, ...)`, `get(id)`, `update(id, ...)` and `delete(id)` manage the AI agents that talk, and `voice.voices.list()` lists the voices they can use. Sync and async clients. Reads need the `calls:read` scope, writes `calls:write` and a live API key. Deleting an agent that still answers a number raises `SendlyError` with code `agent_in_use` (HTTP 409) and the numbers in `e.response.model_extra['numbers']`.
8
+ - New types: `VoiceNumber`, `VoiceNumberListResponse`, `VoiceNumberEmergencyAddress`, `EmergencyAddress`, `VoiceNumberRates`, `VoiceMode`, `VoiceAgent`, `VoiceAgentListResponse`, `VoiceAgentTools`, `Voice`, `VoiceListResponse`, `CreateVoiceAgentRequest`, `UpdateVoiceAgentRequest`, `UpdateVoiceNumberRequest` and `DeletedVoiceAgent`.
9
+
10
+ ### Patch Changes
11
+
12
+ - The `calls.recording()` docstring had the channels the wrong way round. Agent calls are recorded with the agent on the left channel and the other party on the right.
13
+
3
14
  ## 4.1.0
4
15
 
5
16
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sendly
3
- Version: 4.1.0
3
+ Version: 4.2.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
@@ -884,21 +884,28 @@ print(registration.stage) # 'in_review' -> 'brand_verification' -> 'agent_revie
884
884
  # 5. Once in testing: invite devices, describe the campaign, then request launch
885
885
  if registration.stage == RcsCustomerStage.TESTING:
886
886
  client.rcs.agents.set_test_devices(agent.id, [
887
- '+15551234567',
888
- {'phone_number': '+15557654321', 'label': 'Sam'},
887
+ '+15125550142',
888
+ {'phone_number': '+15125550177', 'label': 'Sam'},
889
889
  ])
890
890
  client.rcs.agents.update(
891
891
  agent.id,
892
892
  campaign={
893
+ 'company_overview': 'Acme Coffee runs 12 cafes and an online store.',
893
894
  'agent_overview': 'Order updates and support replies',
894
895
  'interactions': [{'interaction_type': 'TRANSACTIONAL_UPDATES',
895
896
  'description': 'Shipping and delivery updates'}],
896
- 'message_examples': ['Your order #123 has shipped!',
897
- 'Your table is ready!',
898
- 'Reply HELP for help'],
897
+ 'message_examples': ['Acme Coffee: order #123 has shipped. Reply STOP to opt out.',
898
+ 'Acme Coffee: your table is ready!',
899
+ 'Acme Coffee: reply HELP for help.'],
899
900
  'consent_settings': {
900
901
  'opt_in_methods': [{'method_type': 'WEBSITE',
901
902
  'description': 'Checkout checkbox'}],
903
+ 'call_to_action': 'Get order updates by message',
904
+ 'call_to_action_url': 'https://acme.example/checkout',
905
+ 'call_to_action_media_url': 'https://acme.example/rcs/opt-in.png',
906
+ 'double_opt_in': False,
907
+ 'opt_in_message': 'Acme Coffee: you are subscribed. Reply HELP for help, STOP to opt out.',
908
+ 'help_response': 'Acme Coffee: email help@acme.example for help.',
902
909
  'opt_out_response': 'You are unsubscribed.',
903
910
  },
904
911
  },
@@ -925,13 +932,14 @@ for yours, every call method raises `SendlyError` with code `voice_not_enabled`
925
932
  Calls are prepaid from your balance per started minute: 2 credits a minute
926
933
  outbound plus 8 a minute while an AI agent is on the line (10 credits a minute
927
934
  for an agent-handled outbound call); unanswered calls cost nothing. The number
928
- you call from must be voice-enabled in the dashboard (Calls, then Settings),
929
- with an emergency address registered; `client.numbers.list()` shows each
930
- number's `voice_enabled` and `voice_mode`. Destinations are US and Canada.
935
+ you call from must be voice-enabled, with an emergency address registered (see
936
+ [Configure voice](#configure-voice)); `client.voice.numbers.list()` shows each
937
+ number's `voice_enabled`, `voice_mode` and `emergency_address`. Destinations
938
+ are US and Canada.
931
939
 
932
940
  ```python
933
941
  # Pick a voice-enabled number (optional when exactly one is voice-enabled)
934
- voice_numbers = [n for n in client.numbers.list().numbers if n.voice_enabled]
942
+ voice_numbers = [n for n in client.voice.numbers.list().data if n.voice_enabled]
935
943
 
936
944
  # Place a call; the agent speaks first, using the context you pass
937
945
  call = client.calls.create(
@@ -974,6 +982,84 @@ Refusals come back as `SendlyError` with a code that says what to do:
974
982
  `call.completed` and `call.recording.ready` carry the same call object in
975
983
  snake_case, including `billing` and your `metadata`.
976
984
 
985
+ ### Configure voice
986
+
987
+ Everything a call depends on can be set up from code: switch voice on for a
988
+ number and choose how it answers, register its emergency address, and create
989
+ the AI agents that talk. Reads need `calls:read`; writes need `calls:write` and
990
+ a live key. In a team workspace, changing numbers and managing agents needs an
991
+ owner or admin role (`forbidden` otherwise). These settings change how real
992
+ phone calls to your numbers are answered.
993
+
994
+ ```python
995
+ from sendly import SendlyError
996
+
997
+ # The voices an agent can speak with
998
+ for v in client.voice.voices.list().data:
999
+ print(v.id, v.label, v.language)
1000
+
1001
+ # Create an agent (up to 20 per workspace); it gets its own key for texting
1002
+ agent = client.voice.agents.create(
1003
+ name='Front desk',
1004
+ voice='ashley',
1005
+ greeting='Thanks for calling Acme, how can I help?',
1006
+ instructions='Answer questions about opening hours and take a message for anything else.',
1007
+ tools={'send_sms': True},
1008
+ )
1009
+ print(agent.id, agent.voice_label, agent.can_send_sms)
1010
+
1011
+ # Edit it later; only the fields you pass change
1012
+ client.voice.agents.update(agent.id, greeting='Hi, you have reached Acme.')
1013
+
1014
+ # Numbers with their voice settings; pass either the id or the E.164 number
1015
+ for n in client.voice.numbers.list().data:
1016
+ e911 = n.emergency_address.status if n.emergency_address else 'none'
1017
+ print(n.phone_number, n.voice_mode, n.agent_id, e911)
1018
+
1019
+ # Register the emergency address a number needs before it can place calls
1020
+ # (US and Canadian numbers; the first registration adds 1.50 USD a month)
1021
+ number = client.voice.numbers.register_emergency_address(
1022
+ '+15555550188',
1023
+ street='500 Example Ave',
1024
+ unit='Suite 2',
1025
+ city='Austin',
1026
+ state='TX',
1027
+ zip='78701',
1028
+ )
1029
+
1030
+ # Let the agent answer the number (voice_mode='ring_dashboard' rings your team)
1031
+ number = client.voice.numbers.update(
1032
+ '+15555550188', voice_enabled=True, voice_mode='agent', agent_id=agent.id
1033
+ )
1034
+ print(number.voice_mode, number.rate_per_minute.agent) # credits a minute
1035
+
1036
+ # Switch voice off (the same as voice_mode='none')
1037
+ client.voice.numbers.update('+15555550188', voice_enabled=False)
1038
+
1039
+ # Deleting an agent revokes its key; refused while a number still points at it
1040
+ try:
1041
+ client.voice.agents.delete(agent.id)
1042
+ except SendlyError as e:
1043
+ if e.code == 'agent_in_use':
1044
+ print(e.response.model_extra['numbers'])
1045
+ ```
1046
+
1047
+ Without `voice_enabled`, `voice_mode='ring_dashboard'` or `'agent'` switches
1048
+ voice on and `voice_mode='none'` switches it off; `voice_enabled=False` always
1049
+ switches voice off.
1050
+
1051
+ Refusals: `agent_required` (agent mode with no agent), `agent_not_found`,
1052
+ `agent_disabled` (turn the agent on first), `agent_limit`, `agent_in_use`,
1053
+ `number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 502,
1054
+ retry shortly), `carrier_refused` (HTTP 502, retry shortly unless the message
1055
+ says the number couldn't be found for emergency registration: contact
1056
+ support), `voice_unavailable` (HTTP 503),
1057
+ `e911_not_applicable` (only US and Canadian numbers take an emergency
1058
+ address) and `invalid_address`; on HTTP 422 the closest valid address is in
1059
+ `e.response.model_extra['suggested']`. An agent's `tools.transfer_to` is
1060
+ stored, but agents do not transfer calls yet: when a caller asks for a person,
1061
+ the agent offers to pass a message on and takes their name and number.
1062
+
977
1063
  ## Error Handling
978
1064
 
979
1065
  The SDK provides typed exception classes:
@@ -1290,6 +1376,48 @@ End a call (live key, `calls:write`). `ringing` becomes `cancelled`, `active` be
1290
1376
 
1291
1377
  Where to download the recording. `url` and `expires_at` are set only while `status` is `ready`; the link is valid for 5 minutes.
1292
1378
 
1379
+ ### `client.voice`
1380
+
1381
+ #### `numbers.list() -> VoiceNumberListResponse`
1382
+
1383
+ Active numbers in the workspace with `voice_enabled`, `voice_mode`, `agent_id`, `emergency_address` and `rate_per_minute`.
1384
+
1385
+ #### `numbers.get(number) -> VoiceNumber`
1386
+
1387
+ One number's voice settings. `number` is the number's id or its E.164 phone number.
1388
+
1389
+ #### `numbers.update(number, voice_enabled=None, voice_mode=None, agent_id=None, idempotency_key=None) -> VoiceNumber`
1390
+
1391
+ Change how a number answers calls (live key, `calls:write`). Without `voice_enabled`, `ring_dashboard` or `agent` switches voice on and `none` switches it off; `voice_enabled=False` always switches it off. `agent` needs an enabled agent.
1392
+
1393
+ #### `numbers.register_emergency_address(number, street, city, state, zip, unit=None, country=None, idempotency_key=None) -> VoiceNumber`
1394
+
1395
+ Register the emergency address a number needs before it can place calls (live key, `calls:write`). US and Canadian numbers only.
1396
+
1397
+ #### `agents.list() -> VoiceAgentListResponse`
1398
+
1399
+ The workspace's AI agents.
1400
+
1401
+ #### `agents.create(name, enabled=None, voice=None, language=None, greeting=None, instructions=None, tools=None, idempotency_key=None) -> VoiceAgent`
1402
+
1403
+ Create an AI agent (live key, `calls:write`). Up to 20 per workspace.
1404
+
1405
+ #### `agents.get(id) -> VoiceAgent`
1406
+
1407
+ Fetch one agent.
1408
+
1409
+ #### `agents.update(id, name=None, enabled=None, voice=None, language=None, greeting=None, instructions=None, tools=None, idempotency_key=None) -> VoiceAgent`
1410
+
1411
+ Edit an agent; only the fields you pass change, and `tools` may be partial.
1412
+
1413
+ #### `agents.delete(id, idempotency_key=None) -> DeletedVoiceAgent`
1414
+
1415
+ Delete an agent and revoke its sending key. Raises `agent_in_use` (HTTP 409) while a number still points at it.
1416
+
1417
+ #### `voices.list() -> VoiceListResponse`
1418
+
1419
+ The voices an agent can speak with; pass a voice's `id` as `voice`.
1420
+
1293
1421
  ## Enterprise
1294
1422
 
1295
1423
  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_*`).
@@ -849,21 +849,28 @@ print(registration.stage) # 'in_review' -> 'brand_verification' -> 'agent_revie
849
849
  # 5. Once in testing: invite devices, describe the campaign, then request launch
850
850
  if registration.stage == RcsCustomerStage.TESTING:
851
851
  client.rcs.agents.set_test_devices(agent.id, [
852
- '+15551234567',
853
- {'phone_number': '+15557654321', 'label': 'Sam'},
852
+ '+15125550142',
853
+ {'phone_number': '+15125550177', 'label': 'Sam'},
854
854
  ])
855
855
  client.rcs.agents.update(
856
856
  agent.id,
857
857
  campaign={
858
+ 'company_overview': 'Acme Coffee runs 12 cafes and an online store.',
858
859
  'agent_overview': 'Order updates and support replies',
859
860
  'interactions': [{'interaction_type': 'TRANSACTIONAL_UPDATES',
860
861
  'description': 'Shipping and delivery updates'}],
861
- 'message_examples': ['Your order #123 has shipped!',
862
- 'Your table is ready!',
863
- 'Reply HELP for help'],
862
+ 'message_examples': ['Acme Coffee: order #123 has shipped. Reply STOP to opt out.',
863
+ 'Acme Coffee: your table is ready!',
864
+ 'Acme Coffee: reply HELP for help.'],
864
865
  'consent_settings': {
865
866
  'opt_in_methods': [{'method_type': 'WEBSITE',
866
867
  'description': 'Checkout checkbox'}],
868
+ 'call_to_action': 'Get order updates by message',
869
+ 'call_to_action_url': 'https://acme.example/checkout',
870
+ 'call_to_action_media_url': 'https://acme.example/rcs/opt-in.png',
871
+ 'double_opt_in': False,
872
+ 'opt_in_message': 'Acme Coffee: you are subscribed. Reply HELP for help, STOP to opt out.',
873
+ 'help_response': 'Acme Coffee: email help@acme.example for help.',
867
874
  'opt_out_response': 'You are unsubscribed.',
868
875
  },
869
876
  },
@@ -890,13 +897,14 @@ for yours, every call method raises `SendlyError` with code `voice_not_enabled`
890
897
  Calls are prepaid from your balance per started minute: 2 credits a minute
891
898
  outbound plus 8 a minute while an AI agent is on the line (10 credits a minute
892
899
  for an agent-handled outbound call); unanswered calls cost nothing. The number
893
- you call from must be voice-enabled in the dashboard (Calls, then Settings),
894
- with an emergency address registered; `client.numbers.list()` shows each
895
- number's `voice_enabled` and `voice_mode`. Destinations are US and Canada.
900
+ you call from must be voice-enabled, with an emergency address registered (see
901
+ [Configure voice](#configure-voice)); `client.voice.numbers.list()` shows each
902
+ number's `voice_enabled`, `voice_mode` and `emergency_address`. Destinations
903
+ are US and Canada.
896
904
 
897
905
  ```python
898
906
  # Pick a voice-enabled number (optional when exactly one is voice-enabled)
899
- voice_numbers = [n for n in client.numbers.list().numbers if n.voice_enabled]
907
+ voice_numbers = [n for n in client.voice.numbers.list().data if n.voice_enabled]
900
908
 
901
909
  # Place a call; the agent speaks first, using the context you pass
902
910
  call = client.calls.create(
@@ -939,6 +947,84 @@ Refusals come back as `SendlyError` with a code that says what to do:
939
947
  `call.completed` and `call.recording.ready` carry the same call object in
940
948
  snake_case, including `billing` and your `metadata`.
941
949
 
950
+ ### Configure voice
951
+
952
+ Everything a call depends on can be set up from code: switch voice on for a
953
+ number and choose how it answers, register its emergency address, and create
954
+ the AI agents that talk. Reads need `calls:read`; writes need `calls:write` and
955
+ a live key. In a team workspace, changing numbers and managing agents needs an
956
+ owner or admin role (`forbidden` otherwise). These settings change how real
957
+ phone calls to your numbers are answered.
958
+
959
+ ```python
960
+ from sendly import SendlyError
961
+
962
+ # The voices an agent can speak with
963
+ for v in client.voice.voices.list().data:
964
+ print(v.id, v.label, v.language)
965
+
966
+ # Create an agent (up to 20 per workspace); it gets its own key for texting
967
+ agent = client.voice.agents.create(
968
+ name='Front desk',
969
+ voice='ashley',
970
+ greeting='Thanks for calling Acme, how can I help?',
971
+ instructions='Answer questions about opening hours and take a message for anything else.',
972
+ tools={'send_sms': True},
973
+ )
974
+ print(agent.id, agent.voice_label, agent.can_send_sms)
975
+
976
+ # Edit it later; only the fields you pass change
977
+ client.voice.agents.update(agent.id, greeting='Hi, you have reached Acme.')
978
+
979
+ # Numbers with their voice settings; pass either the id or the E.164 number
980
+ for n in client.voice.numbers.list().data:
981
+ e911 = n.emergency_address.status if n.emergency_address else 'none'
982
+ print(n.phone_number, n.voice_mode, n.agent_id, e911)
983
+
984
+ # Register the emergency address a number needs before it can place calls
985
+ # (US and Canadian numbers; the first registration adds 1.50 USD a month)
986
+ number = client.voice.numbers.register_emergency_address(
987
+ '+15555550188',
988
+ street='500 Example Ave',
989
+ unit='Suite 2',
990
+ city='Austin',
991
+ state='TX',
992
+ zip='78701',
993
+ )
994
+
995
+ # Let the agent answer the number (voice_mode='ring_dashboard' rings your team)
996
+ number = client.voice.numbers.update(
997
+ '+15555550188', voice_enabled=True, voice_mode='agent', agent_id=agent.id
998
+ )
999
+ print(number.voice_mode, number.rate_per_minute.agent) # credits a minute
1000
+
1001
+ # Switch voice off (the same as voice_mode='none')
1002
+ client.voice.numbers.update('+15555550188', voice_enabled=False)
1003
+
1004
+ # Deleting an agent revokes its key; refused while a number still points at it
1005
+ try:
1006
+ client.voice.agents.delete(agent.id)
1007
+ except SendlyError as e:
1008
+ if e.code == 'agent_in_use':
1009
+ print(e.response.model_extra['numbers'])
1010
+ ```
1011
+
1012
+ Without `voice_enabled`, `voice_mode='ring_dashboard'` or `'agent'` switches
1013
+ voice on and `voice_mode='none'` switches it off; `voice_enabled=False` always
1014
+ switches voice off.
1015
+
1016
+ Refusals: `agent_required` (agent mode with no agent), `agent_not_found`,
1017
+ `agent_disabled` (turn the agent on first), `agent_limit`, `agent_in_use`,
1018
+ `number_not_found`, `invalid_voice_mode`, `voice_attach_failed` (HTTP 502,
1019
+ retry shortly), `carrier_refused` (HTTP 502, retry shortly unless the message
1020
+ says the number couldn't be found for emergency registration: contact
1021
+ support), `voice_unavailable` (HTTP 503),
1022
+ `e911_not_applicable` (only US and Canadian numbers take an emergency
1023
+ address) and `invalid_address`; on HTTP 422 the closest valid address is in
1024
+ `e.response.model_extra['suggested']`. An agent's `tools.transfer_to` is
1025
+ stored, but agents do not transfer calls yet: when a caller asks for a person,
1026
+ the agent offers to pass a message on and takes their name and number.
1027
+
942
1028
  ## Error Handling
943
1029
 
944
1030
  The SDK provides typed exception classes:
@@ -1255,6 +1341,48 @@ End a call (live key, `calls:write`). `ringing` becomes `cancelled`, `active` be
1255
1341
 
1256
1342
  Where to download the recording. `url` and `expires_at` are set only while `status` is `ready`; the link is valid for 5 minutes.
1257
1343
 
1344
+ ### `client.voice`
1345
+
1346
+ #### `numbers.list() -> VoiceNumberListResponse`
1347
+
1348
+ Active numbers in the workspace with `voice_enabled`, `voice_mode`, `agent_id`, `emergency_address` and `rate_per_minute`.
1349
+
1350
+ #### `numbers.get(number) -> VoiceNumber`
1351
+
1352
+ One number's voice settings. `number` is the number's id or its E.164 phone number.
1353
+
1354
+ #### `numbers.update(number, voice_enabled=None, voice_mode=None, agent_id=None, idempotency_key=None) -> VoiceNumber`
1355
+
1356
+ Change how a number answers calls (live key, `calls:write`). Without `voice_enabled`, `ring_dashboard` or `agent` switches voice on and `none` switches it off; `voice_enabled=False` always switches it off. `agent` needs an enabled agent.
1357
+
1358
+ #### `numbers.register_emergency_address(number, street, city, state, zip, unit=None, country=None, idempotency_key=None) -> VoiceNumber`
1359
+
1360
+ Register the emergency address a number needs before it can place calls (live key, `calls:write`). US and Canadian numbers only.
1361
+
1362
+ #### `agents.list() -> VoiceAgentListResponse`
1363
+
1364
+ The workspace's AI agents.
1365
+
1366
+ #### `agents.create(name, enabled=None, voice=None, language=None, greeting=None, instructions=None, tools=None, idempotency_key=None) -> VoiceAgent`
1367
+
1368
+ Create an AI agent (live key, `calls:write`). Up to 20 per workspace.
1369
+
1370
+ #### `agents.get(id) -> VoiceAgent`
1371
+
1372
+ Fetch one agent.
1373
+
1374
+ #### `agents.update(id, name=None, enabled=None, voice=None, language=None, greeting=None, instructions=None, tools=None, idempotency_key=None) -> VoiceAgent`
1375
+
1376
+ Edit an agent; only the fields you pass change, and `tools` may be partial.
1377
+
1378
+ #### `agents.delete(id, idempotency_key=None) -> DeletedVoiceAgent`
1379
+
1380
+ Delete an agent and revoke its sending key. Raises `agent_in_use` (HTTP 409) while a number still points at it.
1381
+
1382
+ #### `voices.list() -> VoiceListResponse`
1383
+
1384
+ The voices an agent can speak with; pass a voice's `id` as `voice`.
1385
+
1258
1386
  ## Enterprise
1259
1387
 
1260
1388
  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 = "4.1.0"
7
+ version = "4.2.0"
8
8
  description = "Official Sendly Python SDK for SMS messaging"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -29,7 +29,7 @@ Async Example:
29
29
  >>> asyncio.run(main())
30
30
  """
31
31
 
32
- __version__ = "4.1.0"
32
+ __version__ = "4.2.0"
33
33
 
34
34
  # Main clients
35
35
  from .client import AsyncSendly, Sendly
@@ -57,6 +57,7 @@ from .resources.media import AsyncMediaResource, MediaResource
57
57
  from .resources.numbers import AsyncNumbersResource, NumbersResource
58
58
  from .resources.rcs import AsyncRcsResource, RcsResource
59
59
  from .resources.tendlc import AsyncTenDlcResource, TenDlcResource
60
+ from .resources.voice import AsyncVoiceResource, VoiceResource
60
61
  from .resources.whatsapp import AsyncWhatsAppResource, WhatsAppResource
61
62
 
62
63
  # Types
@@ -209,6 +210,22 @@ from .types import (
209
210
  CallTranscriptLine,
210
211
  CreateCallRequest,
211
212
  ListCallsOptions,
213
+ # Voice configuration types
214
+ CreateVoiceAgentRequest,
215
+ DeletedVoiceAgent,
216
+ EmergencyAddress,
217
+ UpdateVoiceAgentRequest,
218
+ UpdateVoiceNumberRequest,
219
+ Voice,
220
+ VoiceAgent,
221
+ VoiceAgentListResponse,
222
+ VoiceAgentTools,
223
+ VoiceListResponse,
224
+ VoiceMode,
225
+ VoiceNumber,
226
+ VoiceNumberEmergencyAddress,
227
+ VoiceNumberListResponse,
228
+ VoiceNumberRates,
212
229
  OptInPage,
213
230
  PricingTier,
214
231
  QuotaSettings,
@@ -435,6 +452,22 @@ __all__ = [
435
452
  "CallTranscriptLine",
436
453
  "CreateCallRequest",
437
454
  "ListCallsOptions",
455
+ # Voice configuration types
456
+ "CreateVoiceAgentRequest",
457
+ "DeletedVoiceAgent",
458
+ "EmergencyAddress",
459
+ "UpdateVoiceAgentRequest",
460
+ "UpdateVoiceNumberRequest",
461
+ "Voice",
462
+ "VoiceAgent",
463
+ "VoiceAgentListResponse",
464
+ "VoiceAgentTools",
465
+ "VoiceListResponse",
466
+ "VoiceMode",
467
+ "VoiceNumber",
468
+ "VoiceNumberEmergencyAddress",
469
+ "VoiceNumberListResponse",
470
+ "VoiceNumberRates",
438
471
  # Links resources
439
472
  "LinksResource",
440
473
  "AsyncLinksResource",
@@ -459,6 +492,9 @@ __all__ = [
459
492
  # Voice call resources
460
493
  "CallsResource",
461
494
  "AsyncCallsResource",
495
+ # Voice configuration resources
496
+ "VoiceResource",
497
+ "AsyncVoiceResource",
462
498
  # Errors
463
499
  "SendlyError",
464
500
  "AuthenticationError",
@@ -27,6 +27,7 @@ from .resources.rules import AsyncRulesResource, RulesResource
27
27
  from .resources.templates import AsyncTemplatesResource, TemplatesResource
28
28
  from .resources.tendlc import AsyncTenDlcResource, TenDlcResource
29
29
  from .resources.verify import AsyncVerifyResource, VerifyResource
30
+ from .resources.voice import AsyncVoiceResource, VoiceResource
30
31
  from .resources.webhooks import AsyncWebhooksResource, WebhooksResource
31
32
  from .resources.whatsapp import AsyncWhatsAppResource, WhatsAppResource
32
33
  from .types import RateLimitInfo, SendlyConfig
@@ -134,6 +135,7 @@ class Sendly:
134
135
  self.whatsapp = WhatsAppResource(self._http)
135
136
  self.rcs = RcsResource(self._http)
136
137
  self.calls = CallsResource(self._http)
138
+ self.voice = VoiceResource(self._http)
137
139
 
138
140
  def __enter__(self) -> "Sendly":
139
141
  return self
@@ -278,6 +280,7 @@ class AsyncSendly:
278
280
  self.whatsapp = AsyncWhatsAppResource(self._http)
279
281
  self.rcs = AsyncRcsResource(self._http)
280
282
  self.calls = AsyncCallsResource(self._http)
283
+ self.voice = AsyncVoiceResource(self._http)
281
284
 
282
285
  async def __aenter__(self) -> "AsyncSendly":
283
286
  return self
@@ -15,11 +15,14 @@ from .rules import AsyncRulesResource, RulesResource
15
15
  from .templates import AsyncTemplatesResource, TemplatesResource
16
16
  from .tendlc import AsyncTenDlcResource, TenDlcResource
17
17
  from .verify import AsyncVerifyResource, VerifyResource
18
+ from .voice import AsyncVoiceResource, VoiceResource
18
19
  from .whatsapp import AsyncWhatsAppResource, WhatsAppResource
19
20
 
20
21
  __all__ = [
21
22
  "CallsResource",
22
23
  "AsyncCallsResource",
24
+ "VoiceResource",
25
+ "AsyncVoiceResource",
23
26
  "CampaignsResource",
24
27
  "AsyncCampaignsResource",
25
28
  "ContactsResource",
@@ -5,7 +5,7 @@ A workspace phone number can take and place phone calls. Over the API you
5
5
  place an outbound call that one of your AI agents handles, list and inspect
6
6
  calls, end a call, and fetch recordings. Switching voice on for a number,
7
7
  choosing how it answers, registering an emergency address and creating agents
8
- are dashboard steps in this release; ``client.numbers.list()`` reports each
8
+ live in ``client.voice``; ``client.voice.numbers.list()`` reports each
9
9
  number's ``voice_enabled`` and ``voice_mode`` so you can pick a ``from_``.
10
10
 
11
11
  Reads need an API key with the ``calls:read`` scope, writes ``calls:write``.
@@ -96,7 +96,8 @@ class CallsResource:
96
96
  the agent rate (``credits_needed``, ``current_balance``).
97
97
  SendlyError: With ``code`` ``agent_not_found``, ``agent_disabled``,
98
98
  ``no_voice_number``, ``number_not_found``, ``e911_required``
99
- (register an emergency address for the number first),
99
+ (register an emergency address for the number first with
100
+ ``client.voice.numbers.register_emergency_address``),
100
101
  ``lines_busy`` (retry shortly), ``daily_call_limit``,
101
102
  ``outbound_calls_not_enabled`` or ``voice_not_enabled``.
102
103
 
@@ -199,8 +200,9 @@ class CallsResource:
199
200
  ``url`` and ``expires_at`` are set only while ``status`` is ``ready``;
200
201
  the link is signed and valid for 5 minutes, so fetch it right before
201
202
  downloading. Recordings are Ogg/Opus; agent calls are recorded
202
- dual-channel (caller left, agent right). ``status`` is ``none`` when
203
- nothing was recorded (recording off, or the call was never answered).
203
+ dual-channel, the agent on the left channel and the other party on
204
+ the right. ``status`` is ``none`` when nothing was recorded
205
+ (recording off, or the call was never answered).
204
206
 
205
207
  Args:
206
208
  id: Call identifier.