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.
- {sendly-4.1.0 → sendly-4.2.0}/CHANGELOG.md +11 -0
- {sendly-4.1.0 → sendly-4.2.0}/PKG-INFO +138 -10
- {sendly-4.1.0 → sendly-4.2.0}/README.md +137 -9
- {sendly-4.1.0 → sendly-4.2.0}/pyproject.toml +1 -1
- {sendly-4.1.0 → sendly-4.2.0}/sendly/__init__.py +37 -1
- {sendly-4.1.0 → sendly-4.2.0}/sendly/client.py +3 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/__init__.py +3 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/calls.py +6 -4
- sendly-4.2.0/sendly/resources/voice.py +642 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/whatsapp.py +1 -1
- {sendly-4.1.0 → sendly-4.2.0}/sendly/types.py +211 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/utils/http.py +1 -1
- sendly-4.2.0/tests/test_voice.py +1001 -0
- {sendly-4.1.0 → sendly-4.2.0}/.github/header.svg +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/.gitignore +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/examples/async_example.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/examples/error_handling.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/examples/send_sms.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/errors.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/py.typed +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/account.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/business_upgrade.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/campaigns.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/contacts.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/conversations.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/drafts.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/enterprise.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/labels.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/links.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/media.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/messages.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/numbers.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/rcs.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/rules.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/templates.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/tendlc.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/verify.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/resources/webhooks.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/utils/__init__.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/utils/validation.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/sendly/webhooks.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/conftest.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_account.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_async_client.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_calls.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_client.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_errors.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_http_body_kwarg.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_idempotency.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages.py.bak +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages_batch.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_messages_schedule.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_numbers.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_rcs.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_rcs_registration.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_tendlc.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_validation.py +0 -0
- {sendly-4.1.0 → sendly-4.2.0}/tests/test_webhooks.py +0 -0
- {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.
|
|
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
|
-
'+
|
|
888
|
-
{'phone_number': '+
|
|
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': ['
|
|
897
|
-
'
|
|
898
|
-
'
|
|
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
|
|
929
|
-
|
|
930
|
-
number's `voice_enabled` and `
|
|
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().
|
|
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
|
-
'+
|
|
853
|
-
{'phone_number': '+
|
|
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': ['
|
|
862
|
-
'
|
|
863
|
-
'
|
|
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
|
|
894
|
-
|
|
895
|
-
number's `voice_enabled` and `
|
|
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().
|
|
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_*`).
|
|
@@ -29,7 +29,7 @@ Async Example:
|
|
|
29
29
|
>>> asyncio.run(main())
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
__version__ = "4.
|
|
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
|
-
|
|
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
|
|
203
|
-
|
|
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.
|