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 boundary —
consent_typeenum 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+
httpx0.27+typing_extensions.NotRequiredauto-used on 3.10; nativetyping.NotRequiredon 3.11+
License
MIT.
Links
- Docs: docs.openphn.com
- REST reference: api.openphn.com/docs
- Source: github.com/knsgill/openphn
- Changelog: docs.openphn.com/docs/changelog
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4ce49d17681c81b5d61781f5b83ba4bc3863332eeb571910a896645ffd45216
|
|
| MD5 |
52660eb39003c1af29260bf58d7be2d5
|
|
| BLAKE2b-256 |
ebad2e6fe681ff2d3466d7a0fc83f145da2a9cf3c8ac29b815c5b1edff48c078
|
Provenance
The following attestation bundles were made for openphn-0.3.0.tar.gz:
Publisher:
publish-python-sdk.yml on knsgill/openphn
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openphn-0.3.0.tar.gz -
Subject digest:
e4ce49d17681c81b5d61781f5b83ba4bc3863332eeb571910a896645ffd45216 - Sigstore transparency entry: 1361045315
- Sigstore integration time:
-
Permalink:
knsgill/openphn@64b9df4210c87f1e81aa93649270a3d6ab6444c3 -
Branch / Tag:
refs/tags/sdk-python-v0.3.0 - Owner: https://github.com/knsgill
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-sdk.yml@64b9df4210c87f1e81aa93649270a3d6ab6444c3 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
70ae406fb30e34d55234275cb48d87233b16b816d18b59bf337f8f28b64057c1
|
|
| MD5 |
288458603fb93cf70d959f3c76f6ceec
|
|
| BLAKE2b-256 |
0cd0426ee021419ef0027c5aca2366c057ff3b688749a9db76de9eed57bde9ab
|
Provenance
The following attestation bundles were made for openphn-0.3.0-py3-none-any.whl:
Publisher:
publish-python-sdk.yml on knsgill/openphn
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openphn-0.3.0-py3-none-any.whl -
Subject digest:
70ae406fb30e34d55234275cb48d87233b16b816d18b59bf337f8f28b64057c1 - Sigstore transparency entry: 1361045349
- Sigstore integration time:
-
Permalink:
knsgill/openphn@64b9df4210c87f1e81aa93649270a3d6ab6444c3 -
Branch / Tag:
refs/tags/sdk-python-v0.3.0 - Owner: https://github.com/knsgill
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-sdk.yml@64b9df4210c87f1e81aa93649270a3d6ab6444c3 -
Trigger Event:
push
-
Statement type: