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
Metadata
Release files for openphn 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| openphn-0.3.0.tar.gz | 14.6 kB | With provenance | Details for openphn-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| openphn-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for openphn-0.3.0-py3-none-any.whl |
Total release size: 28.8 kB
Release files / openphn-0.3.0.tar.gz
| Download URL | openphn-0.3.0.tar.gz |
|---|---|
| Size | 14.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e4ce49d17681c81b5d61781f5b83ba4bc3863332eeb571910a896645ffd45216
|
|
BLAKE2b-256 checksum How to use checksums |
ebad2e6fe681ff2d3466d7a0fc83f145da2a9cf3c8ac29b815c5b1edff48c078
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 23, 2026.
Transparency logRelease files / openphn-0.3.0-py3-none-any.whl
| Download URL | openphn-0.3.0-py3-none-any.whl |
|---|---|
| Size | 14.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
70ae406fb30e34d55234275cb48d87233b16b816d18b59bf337f8f28b64057c1
|
|
BLAKE2b-256 checksum How to use checksums |
0cd0426ee021419ef0027c5aca2366c057ff3b688749a9db76de9eed57bde9ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 23, 2026.
Transparency logSecurity for openphn 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for openphn-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.