ody-sdk 0.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.
- ody_sdk-0.6.0/PKG-INFO +241 -0
- ody_sdk-0.6.0/README.md +221 -0
- ody_sdk-0.6.0/ody/__init__.py +39 -0
- ody_sdk-0.6.0/ody/client.py +1152 -0
- ody_sdk-0.6.0/ody/webhooks.py +196 -0
- ody_sdk-0.6.0/ody_sdk.egg-info/PKG-INFO +241 -0
- ody_sdk-0.6.0/ody_sdk.egg-info/SOURCES.txt +9 -0
- ody_sdk-0.6.0/ody_sdk.egg-info/dependency_links.txt +1 -0
- ody_sdk-0.6.0/ody_sdk.egg-info/top_level.txt +1 -0
- ody_sdk-0.6.0/pyproject.toml +31 -0
- ody_sdk-0.6.0/setup.cfg +4 -0
ody_sdk-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ody-sdk
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Official Python SDK for the Ody REST API (contacts, conversations, messaging, calls, numbers, AI agent, webhooks) with webhook verification and event parsing.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://ody.co
|
|
7
|
+
Project-URL: Documentation, https://dev.ody.co/sdks/python
|
|
8
|
+
Project-URL: API reference, https://dev.ody.co/api/overview
|
|
9
|
+
Project-URL: Source, https://github.com/CosmicHero2/ody-platform/tree/main/sdks/python
|
|
10
|
+
Keywords: ody,sms,mms,voice,ai-agent,contacts,webhooks,api,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Communications :: Telephony
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# ody-sdk
|
|
22
|
+
|
|
23
|
+
Official Python SDK for the [Ody REST API](https://dev.ody.co/api/overview) — contacts, conversations, SMS/MMS sending, calls and the AI agent, phone numbers, and outbound webhooks. Docs: [dev.ody.co/sdks/python](https://dev.ody.co/sdks/python).
|
|
24
|
+
|
|
25
|
+
- **Standard library only** — no dependencies (`urllib.request`, `hmac`, `hashlib`).
|
|
26
|
+
- Python ≥ 3.9, type hints throughout.
|
|
27
|
+
- Auto-pagination generators, typed errors, idempotent sends, Standard-Webhooks verification and webhook event parsing built in.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install ody-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Until the first PyPI release is live (see `sdks/README.md` → Publishing), install from this repo instead:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install /path/to/ody-platform/sdks/python
|
|
39
|
+
# or for development:
|
|
40
|
+
pip install -e sdks/python
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from ody import Ody, OdyError
|
|
47
|
+
|
|
48
|
+
# Keys come from Settings → Developer API. ody_test_... keys behave identically
|
|
49
|
+
# but message sends are simulated (no real SMS).
|
|
50
|
+
client = Ody("ody_live_...")
|
|
51
|
+
# Override the origin if you call the Cloud Run URL directly:
|
|
52
|
+
# client = Ody(key, base_url="https://ody-api-rtvqk5tg2a-uc.a.run.app")
|
|
53
|
+
|
|
54
|
+
# Contacts
|
|
55
|
+
page = client.contacts.list(q="acme", limit=50) # {"contacts": [...], "nextCursor": ...}
|
|
56
|
+
contact = client.contacts.create(
|
|
57
|
+
first_name="Ada",
|
|
58
|
+
last_name="Lovelace",
|
|
59
|
+
phone="+15125550123",
|
|
60
|
+
tags=["vip"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Auto-pagination: iterate every contact, following nextCursor for you.
|
|
64
|
+
for c in client.contacts.list_all():
|
|
65
|
+
print(c["id"], c["firstName"])
|
|
66
|
+
|
|
67
|
+
# Conversations
|
|
68
|
+
open_page = client.conversations.list(status="open")
|
|
69
|
+
thread = client.conversations.get(open_page["conversations"][0]["id"])
|
|
70
|
+
client.conversations.set_status(thread["conversation"]["id"], "done")
|
|
71
|
+
|
|
72
|
+
# Send an SMS (idempotent — safe to retry with the same key for 24h)
|
|
73
|
+
sent = client.messages.send(
|
|
74
|
+
to="+15125550123",
|
|
75
|
+
body="Your order shipped!",
|
|
76
|
+
idempotency_key="order-1042-shipped",
|
|
77
|
+
)
|
|
78
|
+
print(sent["conversationId"], sent.get("simulated", False))
|
|
79
|
+
|
|
80
|
+
# Search, calls, numbers
|
|
81
|
+
hits = client.messages.search("refund", limit=10)
|
|
82
|
+
calls = client.calls.list(limit=100)
|
|
83
|
+
numbers = client.numbers.list()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Error handling
|
|
87
|
+
|
|
88
|
+
Every non-2xx response raises `OdyError` carrying the structured body plus HTTP status:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
try:
|
|
92
|
+
client.contacts.get("00000000-0000-0000-0000-000000000000")
|
|
93
|
+
except OdyError as err:
|
|
94
|
+
print(err.code) # "not_found", "resource_exhausted", ...
|
|
95
|
+
print(err.status) # 404, 429, ...
|
|
96
|
+
print(err.request_id) # quote this to support
|
|
97
|
+
if err.status == 429:
|
|
98
|
+
print(f"retry in {err.retry_after}s") # from Retry-After
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Rate limit: 600 requests/minute per key; every response carries `RateLimit-*` headers and a 429 adds `Retry-After` (surfaced as `err.retry_after`).
|
|
102
|
+
|
|
103
|
+
## Webhooks
|
|
104
|
+
|
|
105
|
+
Register endpoints and verify deliveries per [Standard Webhooks](https://www.standardwebhooks.com). The signing secret (`whsec_...`) is returned **only** by `create` and `rotate` — store it immediately.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
catalog = client.webhooks.event_types()
|
|
109
|
+
hook = client.webhooks.create(
|
|
110
|
+
url="https://example.com/ody-webhook",
|
|
111
|
+
events=["*"],
|
|
112
|
+
label="prod",
|
|
113
|
+
)
|
|
114
|
+
print(hook["secret"]) # whsec_... — shown once!
|
|
115
|
+
|
|
116
|
+
client.webhooks.test(hook["id"]) # signed {"type": "ping"} delivery
|
|
117
|
+
deliveries = client.webhooks.deliveries(hook["id"]) # debugging view
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Verifying deliveries
|
|
121
|
+
|
|
122
|
+
`verify_webhook_signature` checks the `v1,<base64 HMAC-SHA256>` signature over `<webhook-id>.<webhook-timestamp>.<raw body>` in constant time and rejects timestamps older than 5 minutes (replay guard). **Pass the raw body bytes** — re-serializing parsed JSON breaks the signature.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import json
|
|
126
|
+
import os
|
|
127
|
+
|
|
128
|
+
from flask import Flask, request
|
|
129
|
+
from ody import verify_webhook_signature
|
|
130
|
+
|
|
131
|
+
app = Flask(__name__)
|
|
132
|
+
|
|
133
|
+
@app.post("/ody-webhook")
|
|
134
|
+
def ody_webhook():
|
|
135
|
+
ok = verify_webhook_signature(
|
|
136
|
+
os.environ["ODY_WEBHOOK_SECRET"],
|
|
137
|
+
request.headers, # any case-insensitive mapping works
|
|
138
|
+
request.get_data(), # RAW bytes, not request.json
|
|
139
|
+
)
|
|
140
|
+
if not ok:
|
|
141
|
+
return "", 401
|
|
142
|
+
|
|
143
|
+
event = json.loads(request.get_data())
|
|
144
|
+
print("verified event:", event["type"])
|
|
145
|
+
return "", 200
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### One step: `parse_webhook_event`
|
|
149
|
+
|
|
150
|
+
Verify and parse together. Returns the envelope dict (`id`, `apiVersion`, `createdAt`, `type`, `data`); raises `WebhookVerificationError` on a bad or stale signature or a malformed body.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from ody import parse_webhook_event, WebhookVerificationError, WEBHOOK_EVENT_TYPES, is_known_webhook_event_type
|
|
154
|
+
|
|
155
|
+
@app.post("/ody-webhook")
|
|
156
|
+
def ody_webhook():
|
|
157
|
+
try:
|
|
158
|
+
event = parse_webhook_event(os.environ["ODY_WEBHOOK_SECRET"], request.headers, request.get_data())
|
|
159
|
+
except WebhookVerificationError:
|
|
160
|
+
return "", 401
|
|
161
|
+
if event["type"] == "call.ringing":
|
|
162
|
+
screen_pop(event["data"]["from"], event["data"]["callerName"])
|
|
163
|
+
elif event["type"] in ("contact.updated", "contact.deleted"):
|
|
164
|
+
sync_contact(event["data"]["contactId"])
|
|
165
|
+
elif not is_known_webhook_event_type(event["type"]):
|
|
166
|
+
app.logger.info("new Ody event type %s", event["type"])
|
|
167
|
+
return "", 200
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`WEBHOOK_EVENT_TYPES` is the server catalog as a tuple (kept in step by `tests/events/sdkCatalogParity.test.ts` in the platform repo). Every event and its `data` fields: [dev.ody.co/webhooks/events](https://dev.ody.co/webhooks/events).
|
|
171
|
+
|
|
172
|
+
## API surface
|
|
173
|
+
|
|
174
|
+
| Group | Methods |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| `client.contacts` | `list`, `list_all` (generator), `create`, `get`, `update`, `delete` |
|
|
177
|
+
| `client.conversations` | `list`, `list_all`, `get`, `set_status` |
|
|
178
|
+
| `client.messages` | `send` (supports `idempotency_key`), `search` |
|
|
179
|
+
| `client.calls` | `list`, `place` (outbound AI call, scope `calls:place`) |
|
|
180
|
+
| `client.numbers` | `list`, `search_available`, `buy` (supports `idempotency_key`), `connect_agent`, `disconnect_agent` |
|
|
181
|
+
| `client.messaging` | `status`, `register` (10DLC brand + campaign), `refresh` |
|
|
182
|
+
| `client.agent` | `get`, `update`, `publish` |
|
|
183
|
+
| `client.knowledge` | `get`, `search`, `list_articles`, `get_article`, `create_article`, `update_article`, `delete_article`, `import_website`, `refresh_article`, `create_folder`, `update_folder`, `delete_folder` |
|
|
184
|
+
| `client.webhooks` | `list`, `create`, `get`, `update`, `delete`, `rotate`, `test`, `deliveries`, `event_types` |
|
|
185
|
+
|
|
186
|
+
### Agent Phone
|
|
187
|
+
|
|
188
|
+
Provision a working AI phone line end to end — everything an agent buys or
|
|
189
|
+
configures through the API appears in the customer's account exactly as if
|
|
190
|
+
they did it in the app (same billing, same gates, same timeline), and a human
|
|
191
|
+
can take over at any moment:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
candidate = client.numbers.search_available(starts_with="512")[0]
|
|
195
|
+
client.numbers.buy(candidate["phone_number"], idempotency_key="onboard-1")
|
|
196
|
+
|
|
197
|
+
client.messaging.register(business={
|
|
198
|
+
"displayName": "Acme Plumbing", "email": "ops@acme.com", "vertical": "PROFESSIONAL",
|
|
199
|
+
"companyName": "Acme Plumbing LLC", "ein": "12-3456789",
|
|
200
|
+
"street": "600 Congress Ave", "city": "Austin", "state": "TX", "postalCode": "78701",
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
client.agent.update(name="Front Desk", greeting="Thanks for calling Acme!")
|
|
204
|
+
client.agent.publish()
|
|
205
|
+
client.numbers.connect_agent(candidate["phone_number"])
|
|
206
|
+
|
|
207
|
+
placed = client.calls.place(to="+15125550123")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use an ``ody_test_...`` key to dry-run the whole flow: purchases, filings, and
|
|
211
|
+
calls are simulated — nothing is bought, filed, or dialed.
|
|
212
|
+
|
|
213
|
+
### Knowledge hub
|
|
214
|
+
|
|
215
|
+
Everything the AI agents (and the inbox's AI suggested replies) answer from
|
|
216
|
+
lives in the workspace's Knowledge hub — Markdown articles in nested folders.
|
|
217
|
+
Articles are live the moment they are saved; published agents are updated
|
|
218
|
+
automatically. Scopes ``knowledge:read`` / ``knowledge:write``.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
tree = client.knowledge.get() # folders, articles, consumers
|
|
222
|
+
sales = client.knowledge.create_folder("Sales line")
|
|
223
|
+
|
|
224
|
+
client.knowledge.import_website("https://acme.com", folder_id=sales["id"])
|
|
225
|
+
client.knowledge.create_article(
|
|
226
|
+
title="Pricing",
|
|
227
|
+
content="## Hourly rate\n$120/h, first hour minimum…",
|
|
228
|
+
folder_id=sales["id"],
|
|
229
|
+
audience="selected",
|
|
230
|
+
consumers=["texting"] + [c["key"] for c in tree["consumers"] if c["name"] == "Sales desk"],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
hits = client.knowledge.search("how much per hour", consumer="texting")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
python3 -m py_compile ody/__init__.py ody/client.py ody/webhooks.py # syntax check
|
|
240
|
+
python3 -m build # build sdist + wheel
|
|
241
|
+
```
|
ody_sdk-0.6.0/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ody-sdk
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Ody REST API](https://dev.ody.co/api/overview) — contacts, conversations, SMS/MMS sending, calls and the AI agent, phone numbers, and outbound webhooks. Docs: [dev.ody.co/sdks/python](https://dev.ody.co/sdks/python).
|
|
4
|
+
|
|
5
|
+
- **Standard library only** — no dependencies (`urllib.request`, `hmac`, `hashlib`).
|
|
6
|
+
- Python ≥ 3.9, type hints throughout.
|
|
7
|
+
- Auto-pagination generators, typed errors, idempotent sends, Standard-Webhooks verification and webhook event parsing built in.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ody-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Until the first PyPI release is live (see `sdks/README.md` → Publishing), install from this repo instead:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install /path/to/ody-platform/sdks/python
|
|
19
|
+
# or for development:
|
|
20
|
+
pip install -e sdks/python
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from ody import Ody, OdyError
|
|
27
|
+
|
|
28
|
+
# Keys come from Settings → Developer API. ody_test_... keys behave identically
|
|
29
|
+
# but message sends are simulated (no real SMS).
|
|
30
|
+
client = Ody("ody_live_...")
|
|
31
|
+
# Override the origin if you call the Cloud Run URL directly:
|
|
32
|
+
# client = Ody(key, base_url="https://ody-api-rtvqk5tg2a-uc.a.run.app")
|
|
33
|
+
|
|
34
|
+
# Contacts
|
|
35
|
+
page = client.contacts.list(q="acme", limit=50) # {"contacts": [...], "nextCursor": ...}
|
|
36
|
+
contact = client.contacts.create(
|
|
37
|
+
first_name="Ada",
|
|
38
|
+
last_name="Lovelace",
|
|
39
|
+
phone="+15125550123",
|
|
40
|
+
tags=["vip"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Auto-pagination: iterate every contact, following nextCursor for you.
|
|
44
|
+
for c in client.contacts.list_all():
|
|
45
|
+
print(c["id"], c["firstName"])
|
|
46
|
+
|
|
47
|
+
# Conversations
|
|
48
|
+
open_page = client.conversations.list(status="open")
|
|
49
|
+
thread = client.conversations.get(open_page["conversations"][0]["id"])
|
|
50
|
+
client.conversations.set_status(thread["conversation"]["id"], "done")
|
|
51
|
+
|
|
52
|
+
# Send an SMS (idempotent — safe to retry with the same key for 24h)
|
|
53
|
+
sent = client.messages.send(
|
|
54
|
+
to="+15125550123",
|
|
55
|
+
body="Your order shipped!",
|
|
56
|
+
idempotency_key="order-1042-shipped",
|
|
57
|
+
)
|
|
58
|
+
print(sent["conversationId"], sent.get("simulated", False))
|
|
59
|
+
|
|
60
|
+
# Search, calls, numbers
|
|
61
|
+
hits = client.messages.search("refund", limit=10)
|
|
62
|
+
calls = client.calls.list(limit=100)
|
|
63
|
+
numbers = client.numbers.list()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Error handling
|
|
67
|
+
|
|
68
|
+
Every non-2xx response raises `OdyError` carrying the structured body plus HTTP status:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
try:
|
|
72
|
+
client.contacts.get("00000000-0000-0000-0000-000000000000")
|
|
73
|
+
except OdyError as err:
|
|
74
|
+
print(err.code) # "not_found", "resource_exhausted", ...
|
|
75
|
+
print(err.status) # 404, 429, ...
|
|
76
|
+
print(err.request_id) # quote this to support
|
|
77
|
+
if err.status == 429:
|
|
78
|
+
print(f"retry in {err.retry_after}s") # from Retry-After
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Rate limit: 600 requests/minute per key; every response carries `RateLimit-*` headers and a 429 adds `Retry-After` (surfaced as `err.retry_after`).
|
|
82
|
+
|
|
83
|
+
## Webhooks
|
|
84
|
+
|
|
85
|
+
Register endpoints and verify deliveries per [Standard Webhooks](https://www.standardwebhooks.com). The signing secret (`whsec_...`) is returned **only** by `create` and `rotate` — store it immediately.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
catalog = client.webhooks.event_types()
|
|
89
|
+
hook = client.webhooks.create(
|
|
90
|
+
url="https://example.com/ody-webhook",
|
|
91
|
+
events=["*"],
|
|
92
|
+
label="prod",
|
|
93
|
+
)
|
|
94
|
+
print(hook["secret"]) # whsec_... — shown once!
|
|
95
|
+
|
|
96
|
+
client.webhooks.test(hook["id"]) # signed {"type": "ping"} delivery
|
|
97
|
+
deliveries = client.webhooks.deliveries(hook["id"]) # debugging view
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Verifying deliveries
|
|
101
|
+
|
|
102
|
+
`verify_webhook_signature` checks the `v1,<base64 HMAC-SHA256>` signature over `<webhook-id>.<webhook-timestamp>.<raw body>` in constant time and rejects timestamps older than 5 minutes (replay guard). **Pass the raw body bytes** — re-serializing parsed JSON breaks the signature.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import json
|
|
106
|
+
import os
|
|
107
|
+
|
|
108
|
+
from flask import Flask, request
|
|
109
|
+
from ody import verify_webhook_signature
|
|
110
|
+
|
|
111
|
+
app = Flask(__name__)
|
|
112
|
+
|
|
113
|
+
@app.post("/ody-webhook")
|
|
114
|
+
def ody_webhook():
|
|
115
|
+
ok = verify_webhook_signature(
|
|
116
|
+
os.environ["ODY_WEBHOOK_SECRET"],
|
|
117
|
+
request.headers, # any case-insensitive mapping works
|
|
118
|
+
request.get_data(), # RAW bytes, not request.json
|
|
119
|
+
)
|
|
120
|
+
if not ok:
|
|
121
|
+
return "", 401
|
|
122
|
+
|
|
123
|
+
event = json.loads(request.get_data())
|
|
124
|
+
print("verified event:", event["type"])
|
|
125
|
+
return "", 200
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### One step: `parse_webhook_event`
|
|
129
|
+
|
|
130
|
+
Verify and parse together. Returns the envelope dict (`id`, `apiVersion`, `createdAt`, `type`, `data`); raises `WebhookVerificationError` on a bad or stale signature or a malformed body.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from ody import parse_webhook_event, WebhookVerificationError, WEBHOOK_EVENT_TYPES, is_known_webhook_event_type
|
|
134
|
+
|
|
135
|
+
@app.post("/ody-webhook")
|
|
136
|
+
def ody_webhook():
|
|
137
|
+
try:
|
|
138
|
+
event = parse_webhook_event(os.environ["ODY_WEBHOOK_SECRET"], request.headers, request.get_data())
|
|
139
|
+
except WebhookVerificationError:
|
|
140
|
+
return "", 401
|
|
141
|
+
if event["type"] == "call.ringing":
|
|
142
|
+
screen_pop(event["data"]["from"], event["data"]["callerName"])
|
|
143
|
+
elif event["type"] in ("contact.updated", "contact.deleted"):
|
|
144
|
+
sync_contact(event["data"]["contactId"])
|
|
145
|
+
elif not is_known_webhook_event_type(event["type"]):
|
|
146
|
+
app.logger.info("new Ody event type %s", event["type"])
|
|
147
|
+
return "", 200
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`WEBHOOK_EVENT_TYPES` is the server catalog as a tuple (kept in step by `tests/events/sdkCatalogParity.test.ts` in the platform repo). Every event and its `data` fields: [dev.ody.co/webhooks/events](https://dev.ody.co/webhooks/events).
|
|
151
|
+
|
|
152
|
+
## API surface
|
|
153
|
+
|
|
154
|
+
| Group | Methods |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `client.contacts` | `list`, `list_all` (generator), `create`, `get`, `update`, `delete` |
|
|
157
|
+
| `client.conversations` | `list`, `list_all`, `get`, `set_status` |
|
|
158
|
+
| `client.messages` | `send` (supports `idempotency_key`), `search` |
|
|
159
|
+
| `client.calls` | `list`, `place` (outbound AI call, scope `calls:place`) |
|
|
160
|
+
| `client.numbers` | `list`, `search_available`, `buy` (supports `idempotency_key`), `connect_agent`, `disconnect_agent` |
|
|
161
|
+
| `client.messaging` | `status`, `register` (10DLC brand + campaign), `refresh` |
|
|
162
|
+
| `client.agent` | `get`, `update`, `publish` |
|
|
163
|
+
| `client.knowledge` | `get`, `search`, `list_articles`, `get_article`, `create_article`, `update_article`, `delete_article`, `import_website`, `refresh_article`, `create_folder`, `update_folder`, `delete_folder` |
|
|
164
|
+
| `client.webhooks` | `list`, `create`, `get`, `update`, `delete`, `rotate`, `test`, `deliveries`, `event_types` |
|
|
165
|
+
|
|
166
|
+
### Agent Phone
|
|
167
|
+
|
|
168
|
+
Provision a working AI phone line end to end — everything an agent buys or
|
|
169
|
+
configures through the API appears in the customer's account exactly as if
|
|
170
|
+
they did it in the app (same billing, same gates, same timeline), and a human
|
|
171
|
+
can take over at any moment:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
candidate = client.numbers.search_available(starts_with="512")[0]
|
|
175
|
+
client.numbers.buy(candidate["phone_number"], idempotency_key="onboard-1")
|
|
176
|
+
|
|
177
|
+
client.messaging.register(business={
|
|
178
|
+
"displayName": "Acme Plumbing", "email": "ops@acme.com", "vertical": "PROFESSIONAL",
|
|
179
|
+
"companyName": "Acme Plumbing LLC", "ein": "12-3456789",
|
|
180
|
+
"street": "600 Congress Ave", "city": "Austin", "state": "TX", "postalCode": "78701",
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
client.agent.update(name="Front Desk", greeting="Thanks for calling Acme!")
|
|
184
|
+
client.agent.publish()
|
|
185
|
+
client.numbers.connect_agent(candidate["phone_number"])
|
|
186
|
+
|
|
187
|
+
placed = client.calls.place(to="+15125550123")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use an ``ody_test_...`` key to dry-run the whole flow: purchases, filings, and
|
|
191
|
+
calls are simulated — nothing is bought, filed, or dialed.
|
|
192
|
+
|
|
193
|
+
### Knowledge hub
|
|
194
|
+
|
|
195
|
+
Everything the AI agents (and the inbox's AI suggested replies) answer from
|
|
196
|
+
lives in the workspace's Knowledge hub — Markdown articles in nested folders.
|
|
197
|
+
Articles are live the moment they are saved; published agents are updated
|
|
198
|
+
automatically. Scopes ``knowledge:read`` / ``knowledge:write``.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
tree = client.knowledge.get() # folders, articles, consumers
|
|
202
|
+
sales = client.knowledge.create_folder("Sales line")
|
|
203
|
+
|
|
204
|
+
client.knowledge.import_website("https://acme.com", folder_id=sales["id"])
|
|
205
|
+
client.knowledge.create_article(
|
|
206
|
+
title="Pricing",
|
|
207
|
+
content="## Hourly rate\n$120/h, first hour minimum…",
|
|
208
|
+
folder_id=sales["id"],
|
|
209
|
+
audience="selected",
|
|
210
|
+
consumers=["texting"] + [c["key"] for c in tree["consumers"] if c["name"] == "Sales desk"],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
hits = client.knowledge.search("how much per hour", consumer="texting")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python3 -m py_compile ody/__init__.py ody/client.py ody/webhooks.py # syntax check
|
|
220
|
+
python3 -m build # build sdist + wheel
|
|
221
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Official Python SDK for the Ody REST API.
|
|
2
|
+
|
|
3
|
+
Hand-written against the OpenAPI 3.1 contract served at
|
|
4
|
+
``GET https://api.ody.co/v1/api/openapi.json``. Standard library only —
|
|
5
|
+
no third-party dependencies.
|
|
6
|
+
|
|
7
|
+
Docs: https://dev.ody.co/sdks/python · Events: https://dev.ody.co/webhooks/events
|
|
8
|
+
|
|
9
|
+
Quickstart::
|
|
10
|
+
|
|
11
|
+
from ody import Ody
|
|
12
|
+
|
|
13
|
+
client = Ody("ody_live_...")
|
|
14
|
+
page = client.contacts.list(q="acme")
|
|
15
|
+
for contact in page["contacts"]:
|
|
16
|
+
print(contact["id"])
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import Ody, OdyError
|
|
20
|
+
from .webhooks import (
|
|
21
|
+
WEBHOOK_EVENT_TYPES,
|
|
22
|
+
WebhookVerificationError,
|
|
23
|
+
is_known_webhook_event_type,
|
|
24
|
+
parse_webhook_event,
|
|
25
|
+
verify_webhook_signature,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "0.6.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Ody",
|
|
32
|
+
"OdyError",
|
|
33
|
+
"WEBHOOK_EVENT_TYPES",
|
|
34
|
+
"WebhookVerificationError",
|
|
35
|
+
"is_known_webhook_event_type",
|
|
36
|
+
"parse_webhook_event",
|
|
37
|
+
"verify_webhook_signature",
|
|
38
|
+
"__version__",
|
|
39
|
+
]
|