Python SDKpreview
Use the ody Python package: setup, pagination, errors, idempotency and webhook verification. Standard library only.
ody-sdk isn't on PyPI yet; see availability. The API it wraps is stable and live.
#Setup
pip install ody-sdk # once publishedimport os
from ody import Ody
client = Ody(os.environ["ODY_API_KEY"])
# Options: Ody(api_key, base_url="https://api.ody.co", timeout=30.0)Python 3.9+, no third-party dependencies (it uses urllib). The client is synchronous. In async code, run calls in a thread (await asyncio.to_thread(client.messages.send, ...)).
#Usage
# Contacts
contact = client.contacts.create(first_name="Ada", phone="+15125550123", tags=["vip"])
for c in client.contacts.list_all(q="ada"):
print(c["id"])
# Conversations
page = client.conversations.list(status="open", limit=25)
thread = client.conversations.get(page["conversations"][0]["id"])
client.conversations.set_status(thread["conversation"]["id"], "done")
# Messages
sent = client.messages.send(to="+15125550123", body="Hi!", idempotency_key="welcome-ada")
hits = client.messages.search("refund", limit=10)
# Calls
calls = client.calls.list(limit=20)
client.calls.place(to="+15125550123")
# Numbers, 10DLC, agent
available = client.numbers.search_available(starts_with="512")
client.numbers.buy(phone_number=available[0]["phone_number"], idempotency_key="main-line")
status = client.messaging.status()
client.agent.update(greeting="Thanks for calling!")
client.agent.publish()Responses are plain dicts with the API's camelCase keys (conversationId, nextCursor). Python argument names are snake_case (first_name, idempotency_key, from_ for the reserved word from).
#Errors
import time
from ody import OdyError
try:
client.messages.send(to="+15125550123", body="Hi")
except OdyError as err:
if err.code == "resource_exhausted":
time.sleep(err.retry_after or 1)
elif err.code == "failed_precondition":
print("Not allowed yet:", err.message) # e.g. 10DLC pending
print(err.status, err.code, err.request_id)The codes are listed in Errors.
#Webhook verification
from ody import verify_webhook_signature
ok = verify_webhook_signature(secret, request.headers, request.get_data()) # raw bytesHeader lookup is case-insensitive, the comparison is constant-time, and timestamps more than 5 minutes off are rejected. Flask and standardwebhooks examples are in Verifying signatures.
#Webhooks
parse_webhook_event verifies and parses in one step and returns the envelope dict (id, apiVersion, createdAt, type, data). It raises WebhookVerificationError on a bad or stale signature:
from flask import Flask, request
from ody import parse_webhook_event, WebhookVerificationError, is_known_webhook_event_type
@app.post("/ody-webhook")
def ody_webhook():
try:
event = parse_webhook_event(os.environ["ODY_WEBHOOK_SECRET"], request.headers, request.get_data())
except WebhookVerificationError:
return "", 401
if event["type"] == "call.ringing":
screen_pop(event["data"]["from"], event["data"]["callerName"])
elif event["type"] == "call.answered" and event["data"]["answeredBy"] == "member":
assign(event["data"]["memberUid"])
elif event["type"] in ("contact.updated", "contact.deleted"):
sync_contact(event["data"]["contactId"])
elif not is_known_webhook_event_type(event["type"]):
log.info("new Ody event type %s", event["type"]) # a "*" subscription may see types newer than this SDK
return "", 200WEBHOOK_EVENT_TYPES is the catalog as a tuple. Each event's data fields are described in Event types.