OdyDocs

Python SDKpreview

Use the ody Python package: setup, pagination, errors, idempotency and webhook verification. Standard library only.

Preview

ody-sdk isn't on PyPI yet; see availability. The API it wraps is stable and live.

#Setup

Shell
pip install ody-sdk   # once published
Python
import 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

Python
# 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

Python
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

Python
from ody import verify_webhook_signature

ok = verify_webhook_signature(secret, request.headers, request.get_data())  # raw bytes

Header 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:

Python
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 "", 200

WEBHOOK_EVENT_TYPES is the catalog as a tuple. Each event's data fields are described in Event types.

Last updated Wed Oct 14 2026 00:00:00 GMT+0000 (Coordinated Universal Time)Something wrong on this page? Tell us