sendly 4.5.0__tar.gz → 4.6.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.5.0 → sendly-4.6.0}/PKG-INFO +118 -19
- {sendly-4.5.0 → sendly-4.6.0}/README.md +117 -18
- {sendly-4.5.0 → sendly-4.6.0}/pyproject.toml +1 -1
- {sendly-4.5.0 → sendly-4.6.0}/sendly/__init__.py +37 -1
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/calls.py +104 -6
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/voice.py +244 -16
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/whatsapp.py +23 -17
- {sendly-4.5.0 → sendly-4.6.0}/sendly/types.py +467 -4
- {sendly-4.5.0 → sendly-4.6.0}/sendly/utils/http.py +1 -1
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_calls.py +305 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_voice.py +633 -1
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_whatsapp_extras.py +4 -2
- {sendly-4.5.0 → sendly-4.6.0}/.github/header.svg +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/.gitignore +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/CHANGELOG.md +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/examples/async_example.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/examples/error_handling.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/examples/send_sms.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/client.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/errors.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/py.typed +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/__init__.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/account.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/business_upgrade.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/campaigns.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/contacts.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/conversations.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/drafts.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/enterprise.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/labels.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/links.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/media.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/messages.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/numbers.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/rcs.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/rules.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/templates.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/tendlc.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/verify.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/resources/webhooks.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/utils/__init__.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/utils/validation.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/sendly/webhooks.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/conftest.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_account.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_async_client.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_campaigns.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_client.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_contacts.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_conversations.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_enterprise.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_errors.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_http_body_kwarg.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_http_retry.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_idempotency.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_media.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_messages.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_messages.py.bak +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_messages_batch.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_messages_schedule.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_messages_wire.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_numbers.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_path_ids.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_rcs.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_rcs_registration.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_rules.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_templates.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_tendlc.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_validation.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_webhooks.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_webhooks_resource.py +0 -0
- {sendly-4.5.0 → sendly-4.6.0}/tests/test_whatsapp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sendly
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.6.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
|
|
@@ -448,7 +448,7 @@ File uploads (`media.upload()`, `business_upgrade.start()` and `resubmit()`,
|
|
|
448
448
|
make is the `too_many_concurrent_verifications` wait. `whatsapp.signup.verify()`
|
|
449
449
|
and `whatsapp.signup.create()` with a `business_account_id` also raise a
|
|
450
450
|
timeout, a network error or a `5xx` at once, because repeating them can use up
|
|
451
|
-
a verification attempt or start a new signup; they still wait out a
|
|
451
|
+
a verification attempt or start a new charged signup; they still wait out a
|
|
452
452
|
`429` the API never ran.
|
|
453
453
|
|
|
454
454
|
After the last attempt the SDK raises the error it last saw (`TimeoutError`,
|
|
@@ -552,8 +552,8 @@ contents.
|
|
|
552
552
|
|
|
553
553
|
Write methods that accept `idempotency_key` include `messages.send`,
|
|
554
554
|
`messages.schedule`, `messages.send_batch`, `messages.send_group`,
|
|
555
|
-
`calls.create`, `calls.hangup`, every `voice.*` write
|
|
556
|
-
`rcs.brands` / `rcs.agents` write.
|
|
555
|
+
`calls.create`, `calls.hangup`, `calls.transfer`, every `voice.*` write but
|
|
556
|
+
`voice.agents.test`, and every `rcs.brands` / `rcs.agents` write.
|
|
557
557
|
|
|
558
558
|
Full details: https://sendly.live/docs/idempotency
|
|
559
559
|
|
|
@@ -1355,7 +1355,7 @@ client.links.update(link.code, True)
|
|
|
1355
1355
|
|
|
1356
1356
|
## WhatsApp
|
|
1357
1357
|
|
|
1358
|
-
Connect a number you own to WhatsApp (
|
|
1358
|
+
Connect a number you own to WhatsApp ($9.99 one-time, no monthly fee), create
|
|
1359
1359
|
Meta-reviewed message templates, and send on the `whatsapp` channel. Connecting
|
|
1360
1360
|
the first number always ends with a human step: hand the `connect_url` to your
|
|
1361
1361
|
user - they open it in a browser and log in with Facebook to link their
|
|
@@ -1387,8 +1387,11 @@ print(f'Open {signup.connect_url} and log in with Facebook')
|
|
|
1387
1387
|
# After the Facebook step it stays 'registering' while WhatsApp activates the
|
|
1388
1388
|
# number. Activation usually takes a few minutes but can take hours. If it
|
|
1389
1389
|
# hasn't finished about 6 hours after the session began, the session fails
|
|
1390
|
-
# with registration_timeout ('failed', with
|
|
1391
|
-
# why).
|
|
1390
|
+
# with registration_timeout and the fee is refunded ('failed', with
|
|
1391
|
+
# signup.failure_reasons explaining why). The one-time $9.99 connection fee is
|
|
1392
|
+
# charged to your card when you start connecting the number and refunded
|
|
1393
|
+
# automatically if the connection fails; it is not refundable once the number
|
|
1394
|
+
# is connected.
|
|
1392
1395
|
|
|
1393
1396
|
# 2. Create a template (Meta reviews it, usually 24-48h). category is required
|
|
1394
1397
|
# (UTILITY, AUTHENTICATION or MARKETING) with no default, and an update can't
|
|
@@ -1472,7 +1475,7 @@ components = client.whatsapp.senders.get_conversational_components('+15555550123
|
|
|
1472
1475
|
client.whatsapp.senders.set_calling('+15555550123', enabled=True)
|
|
1473
1476
|
|
|
1474
1477
|
# Add another number to an account you already connected: no Facebook step.
|
|
1475
|
-
#
|
|
1478
|
+
# Same $9.99 fee, refunded if it fails. WhatsApp sends the number a 6-digit code
|
|
1476
1479
|
senders = client.whatsapp.senders.list().senders
|
|
1477
1480
|
account_id = next(
|
|
1478
1481
|
s.business_account_id for s in senders if s.status == 'active' and s.business_account_id
|
|
@@ -1496,7 +1499,7 @@ Senders also carry `business_account_id` (None while `pending`),
|
|
|
1496
1499
|
name on file), `calling_enabled`, and `outbound_calling_allowed`, which is
|
|
1497
1500
|
False for +1, +20, +84 and +234 numbers. A signup that is `verifying` carries
|
|
1498
1501
|
`verification_method` and `verification_attempts_remaining`; after 5 wrong
|
|
1499
|
-
codes it fails with `verification_failed
|
|
1502
|
+
codes it fails with `verification_failed` and the fee is refunded. Calls carry
|
|
1500
1503
|
`channel` (`phone`, `whatsapp` or `browser`; see `CallChannel`).
|
|
1501
1504
|
|
|
1502
1505
|
`verification_code` comes only from `signup.get()`. Until a code has been
|
|
@@ -1529,7 +1532,7 @@ Errors specific to WhatsApp:
|
|
|
1529
1532
|
body carries `retryAfter: 3600` and the response a `Retry-After: 3600`
|
|
1530
1533
|
header. The SDK retries it like any `5xx` before raising;
|
|
1531
1534
|
`e.response.retry_after` says how many seconds to wait before trying again.
|
|
1532
|
-
- `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed signups in
|
|
1535
|
+
- `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed, charged signups in
|
|
1533
1536
|
24 hours. It is final and not retried; try again the next day.
|
|
1534
1537
|
- `whatsapp_send_failed` on a send: HTTP 422 when WhatsApp refused the message,
|
|
1535
1538
|
which is final (cached under the idempotency key and replayed for 24 hours),
|
|
@@ -1565,8 +1568,8 @@ Errors specific to WhatsApp:
|
|
|
1565
1568
|
- Adding a number by code: `whatsapp_business_account_not_found` (HTTP 404),
|
|
1566
1569
|
`display_name_required` (HTTP 400), `whatsapp_signup_in_progress` and
|
|
1567
1570
|
`whatsapp_already_enabled` (HTTP 409), and
|
|
1568
|
-
`whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable
|
|
1569
|
-
Facebook `signup.create()` for a number being
|
|
1571
|
+
`whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable; the
|
|
1572
|
+
fee is refunded either way). A Facebook `signup.create()` for a number being
|
|
1570
1573
|
added by code gets `whatsapp_verification_in_progress` (HTTP 409).
|
|
1571
1574
|
`verify()`: `invalid_verification_code` (HTTP 400, not 6 digits),
|
|
1572
1575
|
`whatsapp_verification_code_invalid` (HTTP 422, with
|
|
@@ -1871,9 +1874,9 @@ if event.type.startswith('short_code.'):
|
|
|
1871
1874
|
## Voice Calls
|
|
1872
1875
|
|
|
1873
1876
|
Place phone calls that one of your AI agents handles, follow them while they
|
|
1874
|
-
ring and after they end, end
|
|
1875
|
-
API key with the `calls:read` / `calls:write` scopes;
|
|
1876
|
-
needs a live key. Voice is enabled workspace by workspace: until it is enabled
|
|
1877
|
+
ring and after they end, transfer callers, end calls early, and download
|
|
1878
|
+
recordings. Requires an API key with the `calls:read` / `calls:write` scopes;
|
|
1879
|
+
placing, transferring and ending calls needs a live key. Voice is enabled workspace by workspace: until it is enabled
|
|
1877
1880
|
for yours, every call method raises `SendlyError` with code `voice_not_enabled`
|
|
1878
1881
|
(HTTP 404).
|
|
1879
1882
|
|
|
@@ -1912,6 +1915,15 @@ for c in page.data:
|
|
|
1912
1915
|
if page.pagination.has_more:
|
|
1913
1916
|
page = client.calls.list(status='completed', direction='outbound', limit=20, offset=20)
|
|
1914
1917
|
|
|
1918
|
+
# Only calls answered by one kind of agent (sendly, custom_llm, sip, websocket)
|
|
1919
|
+
page = client.calls.list(agent_kind='websocket')
|
|
1920
|
+
|
|
1921
|
+
# Transfer the caller on an incoming phone call; without `to`, the agent's
|
|
1922
|
+
# transfer number (tools.transfer_to) is used
|
|
1923
|
+
incoming = client.calls.list(direction='inbound', status='active').data[0]
|
|
1924
|
+
incoming = client.calls.transfer(incoming.id, to='+15551234500')
|
|
1925
|
+
print(incoming.transfer.status) # 'pending', then 'completed' or 'failed'
|
|
1926
|
+
|
|
1915
1927
|
# End a call early (idempotent: an ended call is returned unchanged)
|
|
1916
1928
|
client.calls.hangup(call.id)
|
|
1917
1929
|
|
|
@@ -1935,10 +1947,23 @@ Refusals come back as `SendlyError` with a code that says what to do:
|
|
|
1935
1947
|
`agent_not_found` / `agent_disabled`, `from_number_required` / `no_voice_number`,
|
|
1936
1948
|
`from_number_not_supported` (calls are placed from US and Canadian numbers only),
|
|
1937
1949
|
`number_not_found`, `destination_not_supported`, `outbound_calls_not_enabled`,
|
|
1938
|
-
`live_key_required
|
|
1950
|
+
`live_key_required`, and `agent_kind_unsupported` (`sip` and `websocket`
|
|
1951
|
+
agents answer incoming calls only). Webhooks `call.started`, `call.completed`
|
|
1939
1952
|
and `call.recording.ready` carry the same call object in snake_case, including
|
|
1940
1953
|
`billing` and your `metadata`.
|
|
1941
1954
|
|
|
1955
|
+
Each call carries `agent_kind` (the kind of agent that answered, or None),
|
|
1956
|
+
`agent_fallback` (True when your own agent could not be reached, so Sendly
|
|
1957
|
+
said a short line and transferred the caller or took a message) and
|
|
1958
|
+
`transfer` (None, or `to`, `status` and `failure_reason`). A transfer answers
|
|
1959
|
+
at once with `transfer.status` `pending`; once the caller is handed over the
|
|
1960
|
+
call ends `completed` with `hangup_class` `transferred`, and a failed transfer
|
|
1961
|
+
leaves the caller on the call. Transfer refusals: `transfer_to_required`,
|
|
1962
|
+
`outbound_not_enabled` (a number other than the agent's needs calling out
|
|
1963
|
+
switched on), `insufficient_credits`, `call_not_active`,
|
|
1964
|
+
`call_not_transferable` (not an incoming phone call) and
|
|
1965
|
+
`transfer_in_progress`.
|
|
1966
|
+
|
|
1942
1967
|
### Configure voice
|
|
1943
1968
|
|
|
1944
1969
|
Everything a call depends on can be set up from code: switch voice on for a
|
|
@@ -2022,9 +2047,83 @@ says the number couldn't be found for emergency registration: contact
|
|
|
2022
2047
|
support), `voice_unavailable` (HTTP 503),
|
|
2023
2048
|
`e911_not_applicable` (only US and Canadian numbers take an emergency
|
|
2024
2049
|
address) and `invalid_address`; on HTTP 422 the closest valid address is in
|
|
2025
|
-
`e.response.model_extra['suggested']`. An agent's `tools.transfer_to` is
|
|
2026
|
-
|
|
2027
|
-
|
|
2050
|
+
`e.response.model_extra['suggested']`. An agent's `tools.transfer_to` is the
|
|
2051
|
+
number it transfers a caller to when asked for a person.
|
|
2052
|
+
|
|
2053
|
+
### Bring your own agent
|
|
2054
|
+
|
|
2055
|
+
An agent's `kind` says who writes the replies: `sendly` (the default, from the
|
|
2056
|
+
agent's `instructions`), `custom_llm` (Sendly listens and speaks, and your chat
|
|
2057
|
+
completions endpoint writes each reply), `sip` (Sendly connects the caller to
|
|
2058
|
+
your SIP address) or `websocket` (Sendly streams the call audio to your
|
|
2059
|
+
`wss://` server). Your own agents need the workspace's bring-your-own-agent
|
|
2060
|
+
feature, which support switches on (`byo_agents_not_enabled` otherwise).
|
|
2061
|
+
|
|
2062
|
+
```python
|
|
2063
|
+
from sendly import VoiceAgentWebsocketConnectionInput
|
|
2064
|
+
|
|
2065
|
+
agent = client.voice.agents.create(
|
|
2066
|
+
'Support line',
|
|
2067
|
+
kind='websocket',
|
|
2068
|
+
connection=VoiceAgentWebsocketConnectionInput(
|
|
2069
|
+
url='wss://agent.example.com/calls', auth_token='tok_123'
|
|
2070
|
+
),
|
|
2071
|
+
)
|
|
2072
|
+
secret = agent.signing_secret # save it now; it is never shown again
|
|
2073
|
+
print(agent.connection.url, agent.connection.has_auth_token, agent.has_signing_secret)
|
|
2074
|
+
|
|
2075
|
+
# Same kind: the connection is merged, so only what you pass changes
|
|
2076
|
+
client.voice.agents.update(agent.id, connection={'custom_parameters': {'tenant': 'acme'}})
|
|
2077
|
+
|
|
2078
|
+
# A new signing secret; the old one stops working at once
|
|
2079
|
+
secret = client.voice.agents.update(agent.id, rotate_signing_secret=True).signing_secret
|
|
2080
|
+
```
|
|
2081
|
+
|
|
2082
|
+
`connection` is a `VoiceAgentCustomLlmConnectionInput` (`base_url`, `model`,
|
|
2083
|
+
`api_key`, `headers`), `VoiceAgentSipConnectionInput` (`uri`, `transport`,
|
|
2084
|
+
`media_encryption`, `username`, `password`, `headers`),
|
|
2085
|
+
`VoiceAgentWebsocketConnectionInput` (`url`, `audio_format`, `auth_token`,
|
|
2086
|
+
`custom_parameters`) or a dict. A secret you leave out keeps its saved value
|
|
2087
|
+
and None removes it. Secrets are never returned: `agent.connection` reports
|
|
2088
|
+
`has_api_key`, `has_password` or `has_auth_token` instead. Sendly signs every
|
|
2089
|
+
request to your agent with the signing secret
|
|
2090
|
+
(`X-Sendly-Signature: t=<unix>,v1=<hex HMAC-SHA256>`); `signing_secret` is set
|
|
2091
|
+
only on the create or update response that made it. A `connection` that breaks
|
|
2092
|
+
a rule raises `ValidationError` with a message like
|
|
2093
|
+
`connection.url: <reason>`.
|
|
2094
|
+
|
|
2095
|
+
#### Test an agent's connection
|
|
2096
|
+
|
|
2097
|
+
`voice.agents.test` reaches your own agent the way a call would, without
|
|
2098
|
+
placing one: `custom_llm` gets a signed, streamed chat completion with a test
|
|
2099
|
+
message, `websocket` gets a signed connection with a `start` carrying
|
|
2100
|
+
`test: true`, half a second of silence and a `stop`, and `sip` gets an
|
|
2101
|
+
`OPTIONS` request. Pass `kind`, `connection` or `instructions` to test settings
|
|
2102
|
+
before saving them; nothing is saved.
|
|
2103
|
+
|
|
2104
|
+
```python
|
|
2105
|
+
result = client.voice.agents.test(agent.id)
|
|
2106
|
+
if not result.ok:
|
|
2107
|
+
print(result.outcome, result.message) # e.g. 'auth_failed'
|
|
2108
|
+
for step in result.steps:
|
|
2109
|
+
print(step.step, step.ok, step.detail)
|
|
2110
|
+
|
|
2111
|
+
# Try a new SIP address before saving it
|
|
2112
|
+
client.voice.agents.test(
|
|
2113
|
+
agent.id,
|
|
2114
|
+
kind='sip',
|
|
2115
|
+
connection={'uri': 'sip:+15125550100@sip.example.com', 'transport': 'tls'},
|
|
2116
|
+
)
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
A failed test is still a successful request, so check `ok`. `outcome` is
|
|
2120
|
+
`ok`, `blocked_destination`, `unreachable`, `timeout`, `tls_failed`,
|
|
2121
|
+
`auth_required`, `auth_failed`, `not_found`, `rejected` or `bad_reply` (see
|
|
2122
|
+
`VoiceAgentTestOutcome`; a value added later comes through as a plain string),
|
|
2123
|
+
and each step is `address`, `connect`, `login` or `reply`. The result also
|
|
2124
|
+
carries `status` (the HTTP or SIP status, or None), `latency_ms` and `signed`.
|
|
2125
|
+
It needs the `calls:write` scope and a live key, takes no `idempotency_key`,
|
|
2126
|
+
and a workspace can run 10 tests a minute (`RateLimitError` after that).
|
|
2028
2127
|
|
|
2029
2128
|
## Error Handling
|
|
2030
2129
|
|
|
@@ -413,7 +413,7 @@ File uploads (`media.upload()`, `business_upgrade.start()` and `resubmit()`,
|
|
|
413
413
|
make is the `too_many_concurrent_verifications` wait. `whatsapp.signup.verify()`
|
|
414
414
|
and `whatsapp.signup.create()` with a `business_account_id` also raise a
|
|
415
415
|
timeout, a network error or a `5xx` at once, because repeating them can use up
|
|
416
|
-
a verification attempt or start a new signup; they still wait out a
|
|
416
|
+
a verification attempt or start a new charged signup; they still wait out a
|
|
417
417
|
`429` the API never ran.
|
|
418
418
|
|
|
419
419
|
After the last attempt the SDK raises the error it last saw (`TimeoutError`,
|
|
@@ -517,8 +517,8 @@ contents.
|
|
|
517
517
|
|
|
518
518
|
Write methods that accept `idempotency_key` include `messages.send`,
|
|
519
519
|
`messages.schedule`, `messages.send_batch`, `messages.send_group`,
|
|
520
|
-
`calls.create`, `calls.hangup`, every `voice.*` write
|
|
521
|
-
`rcs.brands` / `rcs.agents` write.
|
|
520
|
+
`calls.create`, `calls.hangup`, `calls.transfer`, every `voice.*` write but
|
|
521
|
+
`voice.agents.test`, and every `rcs.brands` / `rcs.agents` write.
|
|
522
522
|
|
|
523
523
|
Full details: https://sendly.live/docs/idempotency
|
|
524
524
|
|
|
@@ -1320,7 +1320,7 @@ client.links.update(link.code, True)
|
|
|
1320
1320
|
|
|
1321
1321
|
## WhatsApp
|
|
1322
1322
|
|
|
1323
|
-
Connect a number you own to WhatsApp (
|
|
1323
|
+
Connect a number you own to WhatsApp ($9.99 one-time, no monthly fee), create
|
|
1324
1324
|
Meta-reviewed message templates, and send on the `whatsapp` channel. Connecting
|
|
1325
1325
|
the first number always ends with a human step: hand the `connect_url` to your
|
|
1326
1326
|
user - they open it in a browser and log in with Facebook to link their
|
|
@@ -1352,8 +1352,11 @@ print(f'Open {signup.connect_url} and log in with Facebook')
|
|
|
1352
1352
|
# After the Facebook step it stays 'registering' while WhatsApp activates the
|
|
1353
1353
|
# number. Activation usually takes a few minutes but can take hours. If it
|
|
1354
1354
|
# hasn't finished about 6 hours after the session began, the session fails
|
|
1355
|
-
# with registration_timeout ('failed', with
|
|
1356
|
-
# why).
|
|
1355
|
+
# with registration_timeout and the fee is refunded ('failed', with
|
|
1356
|
+
# signup.failure_reasons explaining why). The one-time $9.99 connection fee is
|
|
1357
|
+
# charged to your card when you start connecting the number and refunded
|
|
1358
|
+
# automatically if the connection fails; it is not refundable once the number
|
|
1359
|
+
# is connected.
|
|
1357
1360
|
|
|
1358
1361
|
# 2. Create a template (Meta reviews it, usually 24-48h). category is required
|
|
1359
1362
|
# (UTILITY, AUTHENTICATION or MARKETING) with no default, and an update can't
|
|
@@ -1437,7 +1440,7 @@ components = client.whatsapp.senders.get_conversational_components('+15555550123
|
|
|
1437
1440
|
client.whatsapp.senders.set_calling('+15555550123', enabled=True)
|
|
1438
1441
|
|
|
1439
1442
|
# Add another number to an account you already connected: no Facebook step.
|
|
1440
|
-
#
|
|
1443
|
+
# Same $9.99 fee, refunded if it fails. WhatsApp sends the number a 6-digit code
|
|
1441
1444
|
senders = client.whatsapp.senders.list().senders
|
|
1442
1445
|
account_id = next(
|
|
1443
1446
|
s.business_account_id for s in senders if s.status == 'active' and s.business_account_id
|
|
@@ -1461,7 +1464,7 @@ Senders also carry `business_account_id` (None while `pending`),
|
|
|
1461
1464
|
name on file), `calling_enabled`, and `outbound_calling_allowed`, which is
|
|
1462
1465
|
False for +1, +20, +84 and +234 numbers. A signup that is `verifying` carries
|
|
1463
1466
|
`verification_method` and `verification_attempts_remaining`; after 5 wrong
|
|
1464
|
-
codes it fails with `verification_failed
|
|
1467
|
+
codes it fails with `verification_failed` and the fee is refunded. Calls carry
|
|
1465
1468
|
`channel` (`phone`, `whatsapp` or `browser`; see `CallChannel`).
|
|
1466
1469
|
|
|
1467
1470
|
`verification_code` comes only from `signup.get()`. Until a code has been
|
|
@@ -1494,7 +1497,7 @@ Errors specific to WhatsApp:
|
|
|
1494
1497
|
body carries `retryAfter: 3600` and the response a `Retry-After: 3600`
|
|
1495
1498
|
header. The SDK retries it like any `5xx` before raising;
|
|
1496
1499
|
`e.response.retry_after` says how many seconds to wait before trying again.
|
|
1497
|
-
- `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed signups in
|
|
1500
|
+
- `whatsapp_signup_limit_reached` (HTTP 429) after 5 failed, charged signups in
|
|
1498
1501
|
24 hours. It is final and not retried; try again the next day.
|
|
1499
1502
|
- `whatsapp_send_failed` on a send: HTTP 422 when WhatsApp refused the message,
|
|
1500
1503
|
which is final (cached under the idempotency key and replayed for 24 hours),
|
|
@@ -1530,8 +1533,8 @@ Errors specific to WhatsApp:
|
|
|
1530
1533
|
- Adding a number by code: `whatsapp_business_account_not_found` (HTTP 404),
|
|
1531
1534
|
`display_name_required` (HTTP 400), `whatsapp_signup_in_progress` and
|
|
1532
1535
|
`whatsapp_already_enabled` (HTTP 409), and
|
|
1533
|
-
`whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable
|
|
1534
|
-
Facebook `signup.create()` for a number being
|
|
1536
|
+
`whatsapp_verification_start_failed` (HTTP 422 refused, 503 unreachable; the
|
|
1537
|
+
fee is refunded either way). A Facebook `signup.create()` for a number being
|
|
1535
1538
|
added by code gets `whatsapp_verification_in_progress` (HTTP 409).
|
|
1536
1539
|
`verify()`: `invalid_verification_code` (HTTP 400, not 6 digits),
|
|
1537
1540
|
`whatsapp_verification_code_invalid` (HTTP 422, with
|
|
@@ -1836,9 +1839,9 @@ if event.type.startswith('short_code.'):
|
|
|
1836
1839
|
## Voice Calls
|
|
1837
1840
|
|
|
1838
1841
|
Place phone calls that one of your AI agents handles, follow them while they
|
|
1839
|
-
ring and after they end, end
|
|
1840
|
-
API key with the `calls:read` / `calls:write` scopes;
|
|
1841
|
-
needs a live key. Voice is enabled workspace by workspace: until it is enabled
|
|
1842
|
+
ring and after they end, transfer callers, end calls early, and download
|
|
1843
|
+
recordings. Requires an API key with the `calls:read` / `calls:write` scopes;
|
|
1844
|
+
placing, transferring and ending calls needs a live key. Voice is enabled workspace by workspace: until it is enabled
|
|
1842
1845
|
for yours, every call method raises `SendlyError` with code `voice_not_enabled`
|
|
1843
1846
|
(HTTP 404).
|
|
1844
1847
|
|
|
@@ -1877,6 +1880,15 @@ for c in page.data:
|
|
|
1877
1880
|
if page.pagination.has_more:
|
|
1878
1881
|
page = client.calls.list(status='completed', direction='outbound', limit=20, offset=20)
|
|
1879
1882
|
|
|
1883
|
+
# Only calls answered by one kind of agent (sendly, custom_llm, sip, websocket)
|
|
1884
|
+
page = client.calls.list(agent_kind='websocket')
|
|
1885
|
+
|
|
1886
|
+
# Transfer the caller on an incoming phone call; without `to`, the agent's
|
|
1887
|
+
# transfer number (tools.transfer_to) is used
|
|
1888
|
+
incoming = client.calls.list(direction='inbound', status='active').data[0]
|
|
1889
|
+
incoming = client.calls.transfer(incoming.id, to='+15551234500')
|
|
1890
|
+
print(incoming.transfer.status) # 'pending', then 'completed' or 'failed'
|
|
1891
|
+
|
|
1880
1892
|
# End a call early (idempotent: an ended call is returned unchanged)
|
|
1881
1893
|
client.calls.hangup(call.id)
|
|
1882
1894
|
|
|
@@ -1900,10 +1912,23 @@ Refusals come back as `SendlyError` with a code that says what to do:
|
|
|
1900
1912
|
`agent_not_found` / `agent_disabled`, `from_number_required` / `no_voice_number`,
|
|
1901
1913
|
`from_number_not_supported` (calls are placed from US and Canadian numbers only),
|
|
1902
1914
|
`number_not_found`, `destination_not_supported`, `outbound_calls_not_enabled`,
|
|
1903
|
-
`live_key_required
|
|
1915
|
+
`live_key_required`, and `agent_kind_unsupported` (`sip` and `websocket`
|
|
1916
|
+
agents answer incoming calls only). Webhooks `call.started`, `call.completed`
|
|
1904
1917
|
and `call.recording.ready` carry the same call object in snake_case, including
|
|
1905
1918
|
`billing` and your `metadata`.
|
|
1906
1919
|
|
|
1920
|
+
Each call carries `agent_kind` (the kind of agent that answered, or None),
|
|
1921
|
+
`agent_fallback` (True when your own agent could not be reached, so Sendly
|
|
1922
|
+
said a short line and transferred the caller or took a message) and
|
|
1923
|
+
`transfer` (None, or `to`, `status` and `failure_reason`). A transfer answers
|
|
1924
|
+
at once with `transfer.status` `pending`; once the caller is handed over the
|
|
1925
|
+
call ends `completed` with `hangup_class` `transferred`, and a failed transfer
|
|
1926
|
+
leaves the caller on the call. Transfer refusals: `transfer_to_required`,
|
|
1927
|
+
`outbound_not_enabled` (a number other than the agent's needs calling out
|
|
1928
|
+
switched on), `insufficient_credits`, `call_not_active`,
|
|
1929
|
+
`call_not_transferable` (not an incoming phone call) and
|
|
1930
|
+
`transfer_in_progress`.
|
|
1931
|
+
|
|
1907
1932
|
### Configure voice
|
|
1908
1933
|
|
|
1909
1934
|
Everything a call depends on can be set up from code: switch voice on for a
|
|
@@ -1987,9 +2012,83 @@ says the number couldn't be found for emergency registration: contact
|
|
|
1987
2012
|
support), `voice_unavailable` (HTTP 503),
|
|
1988
2013
|
`e911_not_applicable` (only US and Canadian numbers take an emergency
|
|
1989
2014
|
address) and `invalid_address`; on HTTP 422 the closest valid address is in
|
|
1990
|
-
`e.response.model_extra['suggested']`. An agent's `tools.transfer_to` is
|
|
1991
|
-
|
|
1992
|
-
|
|
2015
|
+
`e.response.model_extra['suggested']`. An agent's `tools.transfer_to` is the
|
|
2016
|
+
number it transfers a caller to when asked for a person.
|
|
2017
|
+
|
|
2018
|
+
### Bring your own agent
|
|
2019
|
+
|
|
2020
|
+
An agent's `kind` says who writes the replies: `sendly` (the default, from the
|
|
2021
|
+
agent's `instructions`), `custom_llm` (Sendly listens and speaks, and your chat
|
|
2022
|
+
completions endpoint writes each reply), `sip` (Sendly connects the caller to
|
|
2023
|
+
your SIP address) or `websocket` (Sendly streams the call audio to your
|
|
2024
|
+
`wss://` server). Your own agents need the workspace's bring-your-own-agent
|
|
2025
|
+
feature, which support switches on (`byo_agents_not_enabled` otherwise).
|
|
2026
|
+
|
|
2027
|
+
```python
|
|
2028
|
+
from sendly import VoiceAgentWebsocketConnectionInput
|
|
2029
|
+
|
|
2030
|
+
agent = client.voice.agents.create(
|
|
2031
|
+
'Support line',
|
|
2032
|
+
kind='websocket',
|
|
2033
|
+
connection=VoiceAgentWebsocketConnectionInput(
|
|
2034
|
+
url='wss://agent.example.com/calls', auth_token='tok_123'
|
|
2035
|
+
),
|
|
2036
|
+
)
|
|
2037
|
+
secret = agent.signing_secret # save it now; it is never shown again
|
|
2038
|
+
print(agent.connection.url, agent.connection.has_auth_token, agent.has_signing_secret)
|
|
2039
|
+
|
|
2040
|
+
# Same kind: the connection is merged, so only what you pass changes
|
|
2041
|
+
client.voice.agents.update(agent.id, connection={'custom_parameters': {'tenant': 'acme'}})
|
|
2042
|
+
|
|
2043
|
+
# A new signing secret; the old one stops working at once
|
|
2044
|
+
secret = client.voice.agents.update(agent.id, rotate_signing_secret=True).signing_secret
|
|
2045
|
+
```
|
|
2046
|
+
|
|
2047
|
+
`connection` is a `VoiceAgentCustomLlmConnectionInput` (`base_url`, `model`,
|
|
2048
|
+
`api_key`, `headers`), `VoiceAgentSipConnectionInput` (`uri`, `transport`,
|
|
2049
|
+
`media_encryption`, `username`, `password`, `headers`),
|
|
2050
|
+
`VoiceAgentWebsocketConnectionInput` (`url`, `audio_format`, `auth_token`,
|
|
2051
|
+
`custom_parameters`) or a dict. A secret you leave out keeps its saved value
|
|
2052
|
+
and None removes it. Secrets are never returned: `agent.connection` reports
|
|
2053
|
+
`has_api_key`, `has_password` or `has_auth_token` instead. Sendly signs every
|
|
2054
|
+
request to your agent with the signing secret
|
|
2055
|
+
(`X-Sendly-Signature: t=<unix>,v1=<hex HMAC-SHA256>`); `signing_secret` is set
|
|
2056
|
+
only on the create or update response that made it. A `connection` that breaks
|
|
2057
|
+
a rule raises `ValidationError` with a message like
|
|
2058
|
+
`connection.url: <reason>`.
|
|
2059
|
+
|
|
2060
|
+
#### Test an agent's connection
|
|
2061
|
+
|
|
2062
|
+
`voice.agents.test` reaches your own agent the way a call would, without
|
|
2063
|
+
placing one: `custom_llm` gets a signed, streamed chat completion with a test
|
|
2064
|
+
message, `websocket` gets a signed connection with a `start` carrying
|
|
2065
|
+
`test: true`, half a second of silence and a `stop`, and `sip` gets an
|
|
2066
|
+
`OPTIONS` request. Pass `kind`, `connection` or `instructions` to test settings
|
|
2067
|
+
before saving them; nothing is saved.
|
|
2068
|
+
|
|
2069
|
+
```python
|
|
2070
|
+
result = client.voice.agents.test(agent.id)
|
|
2071
|
+
if not result.ok:
|
|
2072
|
+
print(result.outcome, result.message) # e.g. 'auth_failed'
|
|
2073
|
+
for step in result.steps:
|
|
2074
|
+
print(step.step, step.ok, step.detail)
|
|
2075
|
+
|
|
2076
|
+
# Try a new SIP address before saving it
|
|
2077
|
+
client.voice.agents.test(
|
|
2078
|
+
agent.id,
|
|
2079
|
+
kind='sip',
|
|
2080
|
+
connection={'uri': 'sip:+15125550100@sip.example.com', 'transport': 'tls'},
|
|
2081
|
+
)
|
|
2082
|
+
```
|
|
2083
|
+
|
|
2084
|
+
A failed test is still a successful request, so check `ok`. `outcome` is
|
|
2085
|
+
`ok`, `blocked_destination`, `unreachable`, `timeout`, `tls_failed`,
|
|
2086
|
+
`auth_required`, `auth_failed`, `not_found`, `rejected` or `bad_reply` (see
|
|
2087
|
+
`VoiceAgentTestOutcome`; a value added later comes through as a plain string),
|
|
2088
|
+
and each step is `address`, `connect`, `login` or `reply`. The result also
|
|
2089
|
+
carries `status` (the HTTP or SIP status, or None), `latency_ms` and `signed`.
|
|
2090
|
+
It needs the `calls:write` scope and a live key, takes no `idempotency_key`,
|
|
2091
|
+
and a workspace can run 10 tests a minute (`RateLimitError` after that).
|
|
1993
2092
|
|
|
1994
2093
|
## Error Handling
|
|
1995
2094
|
|
|
@@ -29,7 +29,7 @@ Async Example:
|
|
|
29
29
|
>>> asyncio.run(main())
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
__version__ = "4.
|
|
32
|
+
__version__ = "4.6.0"
|
|
33
33
|
|
|
34
34
|
# Main clients
|
|
35
35
|
from .client import AsyncSendly, Sendly
|
|
@@ -216,24 +216,42 @@ from .types import (
|
|
|
216
216
|
CallRecordingStatus,
|
|
217
217
|
CallStatus,
|
|
218
218
|
CallTranscriptLine,
|
|
219
|
+
CallTransfer,
|
|
220
|
+
CallTransferFailureReason,
|
|
221
|
+
CallTransferStatus,
|
|
219
222
|
CreateCallRequest,
|
|
220
223
|
ListCallsOptions,
|
|
224
|
+
TransferCallRequest,
|
|
221
225
|
# Voice configuration types
|
|
222
226
|
CreateVoiceAgentRequest,
|
|
223
227
|
DeletedVoiceAgent,
|
|
224
228
|
EmergencyAddress,
|
|
229
|
+
SipMediaEncryption,
|
|
230
|
+
SipTransport,
|
|
231
|
+
TestVoiceAgentRequest,
|
|
225
232
|
UpdateVoiceAgentRequest,
|
|
226
233
|
UpdateVoiceNumberRequest,
|
|
227
234
|
Voice,
|
|
228
235
|
VoiceAgent,
|
|
236
|
+
VoiceAgentConnection,
|
|
237
|
+
VoiceAgentConnectionInput,
|
|
238
|
+
VoiceAgentCustomLlmConnectionInput,
|
|
239
|
+
VoiceAgentKind,
|
|
229
240
|
VoiceAgentListResponse,
|
|
241
|
+
VoiceAgentSipConnectionInput,
|
|
242
|
+
VoiceAgentTest,
|
|
243
|
+
VoiceAgentTestOutcome,
|
|
244
|
+
VoiceAgentTestStep,
|
|
245
|
+
VoiceAgentTestStepName,
|
|
230
246
|
VoiceAgentTools,
|
|
247
|
+
VoiceAgentWebsocketConnectionInput,
|
|
231
248
|
VoiceListResponse,
|
|
232
249
|
VoiceMode,
|
|
233
250
|
VoiceNumber,
|
|
234
251
|
VoiceNumberEmergencyAddress,
|
|
235
252
|
VoiceNumberListResponse,
|
|
236
253
|
VoiceNumberRates,
|
|
254
|
+
WebsocketAudioFormat,
|
|
237
255
|
OptInPage,
|
|
238
256
|
PricingTier,
|
|
239
257
|
QuotaSettings,
|
|
@@ -466,24 +484,42 @@ __all__ = [
|
|
|
466
484
|
"CallRecordingStatus",
|
|
467
485
|
"CallStatus",
|
|
468
486
|
"CallTranscriptLine",
|
|
487
|
+
"CallTransfer",
|
|
488
|
+
"CallTransferFailureReason",
|
|
489
|
+
"CallTransferStatus",
|
|
469
490
|
"CreateCallRequest",
|
|
470
491
|
"ListCallsOptions",
|
|
492
|
+
"TransferCallRequest",
|
|
471
493
|
# Voice configuration types
|
|
472
494
|
"CreateVoiceAgentRequest",
|
|
473
495
|
"DeletedVoiceAgent",
|
|
474
496
|
"EmergencyAddress",
|
|
497
|
+
"SipMediaEncryption",
|
|
498
|
+
"SipTransport",
|
|
499
|
+
"TestVoiceAgentRequest",
|
|
475
500
|
"UpdateVoiceAgentRequest",
|
|
476
501
|
"UpdateVoiceNumberRequest",
|
|
477
502
|
"Voice",
|
|
478
503
|
"VoiceAgent",
|
|
504
|
+
"VoiceAgentConnection",
|
|
505
|
+
"VoiceAgentConnectionInput",
|
|
506
|
+
"VoiceAgentCustomLlmConnectionInput",
|
|
507
|
+
"VoiceAgentKind",
|
|
479
508
|
"VoiceAgentListResponse",
|
|
509
|
+
"VoiceAgentSipConnectionInput",
|
|
510
|
+
"VoiceAgentTest",
|
|
511
|
+
"VoiceAgentTestOutcome",
|
|
512
|
+
"VoiceAgentTestStep",
|
|
513
|
+
"VoiceAgentTestStepName",
|
|
480
514
|
"VoiceAgentTools",
|
|
515
|
+
"VoiceAgentWebsocketConnectionInput",
|
|
481
516
|
"VoiceListResponse",
|
|
482
517
|
"VoiceMode",
|
|
483
518
|
"VoiceNumber",
|
|
484
519
|
"VoiceNumberEmergencyAddress",
|
|
485
520
|
"VoiceNumberListResponse",
|
|
486
521
|
"VoiceNumberRates",
|
|
522
|
+
"WebsocketAudioFormat",
|
|
487
523
|
# Links resources
|
|
488
524
|
"LinksResource",
|
|
489
525
|
"AsyncLinksResource",
|