Skip to main content

Python SDK for OpenPhn — agentic voice calls with structured outcome JSON

Project description

OpenPhn — Python SDK

Agentic voice calls with structured JSON outcomes.

pip install openphn

Quickstart

from openphn import OpenPhn

with OpenPhn(api_key="sk_live_...") as client:
    result = client.call(
        to="+14155551234",
        objective="Confirm order A-14421 ships today",
        outcome_schema={
            "will_ship_today": {"type": "boolean"},
            "tracking":        {"type": "string",   "optional": True},
            "eta":             {"type": "datetime", "optional": True},
        },
        consent_type="existing_business_relationship",
    )
    print(result["outcome"])
    # {'will_ship_today': True, 'tracking': '1Z...', 'eta': '2026-04-23T18:00:00-07:00'}

client.call() blocks until the call finishes by default. Pass wait=False to return as soon as the server accepts the request, then pair with a webhook (see Webhooks).

Why OpenPhn

  • Structured JSON per call — you define outcome_schema; we return typed fields with per-field confidence scores and ambiguity flags.
  • Compliance at the API boundaryconsent_type enum required; call-hour rules (8am–9pm in destination's timezone) enforced server-side; DNC scrubbing native.
  • Hosted MCP server — any MCP-aware client (Claude Desktop, Cursor) can place calls via this platform as a tool.
  • Published-and-frozen flows — edit your graph freely; publish freezes the artifact so runtime behavior doesn't silently drift.

Conceptual docs: docs.openphn.com. Interactive API reference: api.openphn.com/docs.

Async

import asyncio
from openphn import AsyncOpenPhn

async def main():
    async with AsyncOpenPhn(api_key="sk_live_...") as client:
        r = await client.call(
            to="+14155551234",
            objective="Follow up on the Thursday appointment",
            outcome_schema={"attended": {"type": "boolean"}},
            consent_type="existing_business_relationship",
        )
        print(r["outcome"])

asyncio.run(main())

Typed errors

Every non-2xx becomes a typed subclass of OpenPhnError.

from openphn import (
    OpenPhn,
    DNCBlockedError,
    RateLimitError,
    ValidationError,
    VerificationPendingError,
)

try:
    client.call(to="+14155551234", ...)
except DNCBlockedError:
    # Destination on suppression list. Do not retry.
    pass
except VerificationPendingError:
    # Account still `pending_review`. Wait for admin approval.
    pass
except RateLimitError as e:
    time.sleep(e.retry_after_seconds or 30)
    client.call(...)
except ValidationError as e:
    print(e.error_code, e.message)
Exception Status When
AuthenticationError 401 Missing / malformed / revoked key
PermissionError 403 Valid key, insufficient scope or number_id pinning
DNCBlockedError 403 (dnc_blocked) Destination on suppression list
VerificationPendingError 403 (verification_pending) Account not yet approved
NotFoundError 404 Resource doesn't exist for your tenant
ConflictError 409 idempotency_key reused with different body
ValidationError 422 Semantically-invalid request
ConsentError 422 (consent_*) Consent type missing / invalid
RateLimitError 429 Over per-key quota. Has .retry_after_seconds.
ServerError 5xx Transient. Safe to retry idempotent requests.

Retries

Auto-retry on 429 + 5xx for idempotent requests (GET, DELETE, and any POST with idempotency_key set). Exponential backoff with jitter; honors Retry-After. Override with max_retries=N (default 3, 0 disables).

Idempotency

call() and create_batch() accept idempotency_key. Re-sending with the same key within 24h returns the original response instead of creating a duplicate.

import uuid
key = f"order-{order_id}-{uuid.uuid4()}"
r1 = client.call(to="+14155551234", ..., idempotency_key=key)
# network blip → safe retry
r2 = client.call(to="+14155551234", ..., idempotency_key=key)
assert r1["call_id"] == r2["call_id"]

Webhooks

wh = client.create_webhook(
    url="https://example.com/webhooks/openphn",
    description="Prod call-completion handler",
)
print(wh["secret"])  # SHOWN ONCE — save it for HMAC verification

Verify HMAC-SHA256 on delivery:

import hmac, hashlib, time

def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> None:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        f"{timestamp}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, signature):
        raise ValueError("bad signature")
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("timestamp too old")

Inspect delivery history (retries + response bodies) and manually replay:

for d in client.list_webhook_deliveries("wh_01HV..."):
    print(d["attempt"], d["status_code"], d["latency_ms"])

client.retry_webhook_delivery("wh_01HV...", "dlv_01HV...")

DNC (Do Not Call)

Upload up to 10,000 phone numbers per CSV request. E.164 normalized, NANP-only, deduped.

result = client.upload_dnc("suppressions.csv")
print(result["added_count"])

Subsequent call() to any listed number raises DNCBlockedError. Globally- suppressed numbers (federal DNC imports, platform bans) enforce the same way.

Iterate calls (auto-paged)

for call in client.iter_calls(status="delivered"):
    print(call["id"], call["outcome"])

# async
async for call in client.iter_calls(status="delivered"):
    ...

BYO Twilio (outbound)

verified = client.verify_twilio(
    account_sid="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    auth_token="your-twilio-auth-token",
)
print([n["phone_number"] for n in verified["numbers"]])

client.select_number(e164="+14155551234")

Configure an inbound number

client.update_number(
    number_id="num_01HV...",
    greeting_text="Hi, this is the virtual receptionist for Northside Dental...",
    transfer_destinations=[
        {"label": "Front desk",      "phone": "+14155551111", "enabled": True, "order": 0},
        {"label": "On-call manager", "phone": "+14155552222", "enabled": True, "order": 1},
    ],
    recording_enabled=True,
)

Analytics summary

s = client.analytics_summary(range="30d")
print(f"{s['total_calls']} calls · {s['success_rate']:.1%} success · "
      f"greeting p50 {s['greeting_latency_p50_ms']}ms")

Whoami

me = client.me()
print(me["email"], me["verification_status"], me.get("scopes"))

Staging / self-hosted base URL

OpenPhn(api_key="sk_live_...", base_url="https://api.staging.openphn.com")

Compatibility

  • Python 3.10+
  • httpx 0.27+
  • typing_extensions.NotRequired auto-used on 3.10; native typing.NotRequired on 3.11+

License

MIT.

Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

openphn-0.3.0.tar.gz (14.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

openphn-0.3.0-py3-none-any.whl (14.2 kB view details)

Uploaded Python 3

File details

Details for the file openphn-0.3.0.tar.gz.

File metadata

  • Download URL: openphn-0.3.0.tar.gz
  • Upload date:
  • Size: 14.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for openphn-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e4ce49d17681c81b5d61781f5b83ba4bc3863332eeb571910a896645ffd45216
MD5 52660eb39003c1af29260bf58d7be2d5
BLAKE2b-256 ebad2e6fe681ff2d3466d7a0fc83f145da2a9cf3c8ac29b815c5b1edff48c078

See more details on using hashes here.

Provenance

The following attestation bundles were made for openphn-0.3.0.tar.gz:

Publisher: publish-python-sdk.yml on knsgill/openphn

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file openphn-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: openphn-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 14.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for openphn-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 70ae406fb30e34d55234275cb48d87233b16b816d18b59bf337f8f28b64057c1
MD5 288458603fb93cf70d959f3c76f6ceec
BLAKE2b-256 0cd0426ee021419ef0027c5aca2366c057ff3b688749a9db76de9eed57bde9ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for openphn-0.3.0-py3-none-any.whl:

Publisher: publish-python-sdk.yml on knsgill/openphn

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page