Metadata-Version: 2.5
Name: calle-ai
Version: 1.0.1
Summary: Python server SDK for the CALL-E Developer API.
Project-URL: Homepage, https://github.com/CALLE-AI/server-sdk-python
Project-URL: Repository, https://github.com/CALLE-AI/server-sdk-python
Project-URL: Bug Tracker, https://github.com/CALLE-AI/server-sdk-python/issues
License-Expression: MIT
License-File: LICENSE
Keywords: call-e,calle,server-sdk,voice-ai,webhooks
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<1.0.0,>=0.27.0
Description-Content-Type: text/markdown

# calle-ai

[![PyPI version](https://img.shields.io/pypi/v/calle-ai)](https://pypi.org/project/calle-ai/)
[![Python versions](https://img.shields.io/pypi/pyversions/calle-ai)](https://pypi.org/project/calle-ai/)
[![CI](https://github.com/CALLE-AI/server-sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/CALLE-AI/server-sdk-python/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

Python server SDK for the CALL-E Developer API.

Use this SDK from backend services, workers, and other trusted server
environments. Do not expose CALL-E API keys in browser code.

SDK 1.0 uses the single-target Calls API and the same result model as Goal Runs. A required
closed scalar-object result schema defines the business fields. SDK wait helpers
continue while `result_status` is `pending`, even after execution reaches `completed`.
Empty `{}` is a ready result. Webhook data matches the persisted GET snapshot.

## Documentation

- Developer docs: <https://docs.heycall-e.com/>
- SDK guide: <https://docs.heycall-e.com/#/sdks>
- API Reference: <https://docs.heycall-e.com/#/api-reference>
- Webhooks: <https://docs.heycall-e.com/#/webhooks>
- Product changelog: <https://docs.heycall-e.com/#/changelog>
- TypeScript SDK: <https://github.com/CALLE-AI/server-sdk-typescript>

## SDK surface

- `client.calls` creates, reads, and polls call tasks and lists call events.
- `client.goals` lists and reads published Goals and runs them with structured
  results.
- `examples/webhook_server.py` shows how to receive current terminal webhook
  events.

## Install

Install the stable package from PyPI:

```bash
pip install calle-ai==1.0.1
```

For reproducible deployments, pin the package version selected by your
dependency-management workflow.

Use a local checkout for development and package smoke tests:

```bash
bash scripts/validate.sh
```

Python 3.11 or newer is required.

## Configuration

Create one `CalleClient` and close it when your process no longer needs it:

| Option | Required | Description |
| --- | --- | --- |
| `api_key` | Yes | CALL-E API key. Load it from a server-side secret store or environment variable. |
| `base_url` | No | API base URL. Defaults to `https://api.heycall-e.com`. |
| `timeout` | No | HTTP request timeout in seconds. Defaults to `30.0`. |
| `http_client` | No | Configured `httpx.Client` used as-is. It must define the required base URL, authentication, transport, and timeout. |

With the default HTTP client, use `CalleClient` as a context manager so the SDK
closes its connection pool:

```python
import os
from calle import CalleClient

with CalleClient(api_key=os.environ["CALLE_API_KEY"]) as client:
    call = client.calls.get("call_123")
```

## API keys and diagnostic output

Use the complete API key issued by the [CALL-E dashboard](https://dashboard.heycall-e.com/account/api-keys).
`<YOUR_CALLE_API_KEY>` and the fallback keys in example scripts are non-working
placeholders. Replace them with your own key; do not derive key validation or
redaction patterns from a sample prefix.

Before logging or sharing diagnostics:

- Prefer a small set of fields such as SDK version, HTTP status, and error
  code over dumping a full request, response, or error object.
- Remove the entire `Authorization` header and configured secret values.
  Matching one key prefix is not sufficient.
- Review phone fields and free text, including `task`, transcripts, summaries,
  evidence, custom results, metadata, and error details. The SDK preserves the
  returned task text, which may contain a phone number or other private data.
  Dashboard masking does not redact SDK output or raw API responses.

For example, a manually redacted response excerpt for sharing can omit all
other fields and replace both the task and recipient phone:

```json
{
  "status": "completed",
  "task": "[REDACTED]",
  "recipients": [{"phones": ["[REDACTED]"]}]
}
```

This is a diagnostic excerpt, not a create request. Inspect the final text
before publishing it; these replacements are not a general-purpose PII filter.

## Examples

Set the API key before running call examples:

```bash
export CALLE_API_KEY="<YOUR_CALLE_API_KEY>"
export CALLE_BASE_URL="https://api.heycall-e.com"
export CALLE_EXAMPLE_PHONE="+14155550100"
```

Run the create-and-wait example from a local checkout:

```bash
uv run python examples/create_and_wait.py
```

Run a published Goal with an explicit Goal, phone, variables, and durable
idempotency key:

```bash
export CALLE_GOAL_ID="<PUBLISHED_GOAL_ID>"
export CALLE_GOAL_PHONE="<AUTHORIZED_E164_PHONE>"
export CALLE_GOAL_VARIABLES='{"name":"Alex"}'
export CALLE_IDEMPOTENCY_KEY="<DURABLE_WORKFLOW_KEY>"
uv run python examples/run_goal_and_wait.py
```

To use an approved non-production environment, explicitly set:

```bash
export CALLE_BASE_URL="<APPROVED_TEST_API_BASE_URL>"
```

Run the webhook receiver example:

```bash
uv run python examples/webhook_server.py
```

The webhook receiver listens on `POST /calle/webhook` and processes terminal
event JSON. CALL-E sends terminal events only after the post-call outcome and
requested structured results are finalized.

CALL-E webhook delivery does not use a webhook secret, `CALL-E-Timestamp`, or
`CALL-E-Signature`. Use the required `CALL-E-Event-Id` header to deduplicate
at-least-once deliveries before performing side effects. The receiver example
parses JSON directly and checks that this header matches the body event id.
It defaults to a 10 MiB request-body limit and returns `413` for larger
payloads. Set `CALLE_WEBHOOK_MAX_BODY_BYTES` to match your ingress limits.

The bounded in-memory deduplication cache is only for local example use and
retains the latest 10,000 event ids. Production deployments must use a
production server or ingress with read deadlines and durable deduplication
storage with an explicit retention policy.

The `client.webhooks.verify` and `client.webhooks.unwrap` methods implement the
legacy signed-payload contract from SDK `0.2`. They remain available for source
compatibility but are deprecated and are not compatible with current unsigned
CALL-E deliveries.

## Quickstart

Run a reusable published Goal. The Goal owns its input and result schemas;
each Run supplies only a phone number, per-Run variables, and a durable
idempotency key:

```python
import os
from calle import CalleClient

client = CalleClient(api_key=os.environ["CALLE_API_KEY"])

goal = client.goals.get("goal_delivery_confirmation")
print(goal["title"], goal["published_run_spec"]["input_schema"])

run = client.goals.run_and_wait(
    goal_id=goal["id"],
    phone="+14155550100",
    variables={
        "customer_name": "Taylor",
        "order_reference": "ORD-8472",
        "delivery_window": "July 24, 2:00-4:00 PM",
    },
    idempotency_key="delivery:ORD-8472:confirm-window:v1",
)

if run["result"] is not None:
    print(run["call_id"])
    print(run["result"])
else:
    print(run["error"])
```

Persist the idempotency key before the first request and reuse it for network
retries. `wait_for_result` returns when `result_status` is no longer `pending`;
an execution `status` of `completed` can still be waiting for result
materialization.

## Migration to SDK 1.0

The `calls` wrapper now submits one explicit phone to `/v2/calls` and requires
an idempotency key. Keep the key for retries. The backend continues serving
legacy integrations until their planned retirement at the end of 2026; keep
SDK 0.7.x for historical legacy call IDs. Upgrade Goal Run integrations to 1.0
as well: old wait helpers can time out when both `result` and `error` are null.

```python
call = client.calls.create_and_wait(
    task="Ask whether Friday lunch is confirmed.",
    phone="<AUTHORIZED_E164_PHONE>", region="US", locale="en-US",
    result_schema={
        "type": "object", "additionalProperties": False, "required": ["answer"],
        "properties": {"answer": {"type": "string", "enum": ["yes", "no", "unknown"]}},
    },
    idempotency_key="lunch:friday:confirmation:v1",
)
if call["error"] is None:
    print(call["result"])
else:
    print(call["error"])
# Cancel a queued call before provider submission:
# client.calls.cancel(call_id)
```

Replace `recipient` / `recipients` with `phone` and optional `region` and `locale`.
Define the required `result_schema` (`resultSchema` in TypeScript) using Goal's
`calle.result.scalar-object.v1` profile: at most 32 scalar properties and
`additionalProperties: false`. Flatten old nested fields; arrays and null values
are unsupported. Old `structured_result`, `result_error`, summary,
confidence and provider attempt fields are removed. Request business summaries or
completion flags explicitly as scalar fields in the schema when needed.
The Calls API does not support batch, scheduled or recurring calls.

Calls and Goal Runs always expose `transcript`, an array of recorded turns with
`speaker` (`bot`, `user`, `unknown`), nullable `offset_seconds`, and `text`.
Before execution ends or without available transcript, the array is empty.
A pending, unavailable or failed business result does not remove an available
terminal transcript. Webhook data uses the same shape.

The returned object is `call`. Consume `result` and `error` exactly as for Goal Runs.
Poll while result_status is pending (resultStatus in TypeScript). No-answer, busy
and declined are ordinary call_outcome values (callOutcome in TypeScript).
Unavailable business evidence uses result_status=unavailable and error=null.
Explicit schema-valid task fallbacks remain results. Technical failures use error.
Cancellation uses result_status=not_applicable and no error. GET reads committed state and
terminal webhooks contain the same ready snapshot. Deduplicate by event id.

Cancellation returns `409 call_cannot_cancel` after provider submission begins.
Calls accept immediate execution only. If authorization expires after submission,
`error.detail_code=authorization_expired` means the provider may still complete the call; do not
create an automatic replacement call.

## Error handling

The SDK exports errors for API responses, authentication, rate limits,
timeouts, and connection failures:

```python
import os
from calle import CalleAPIError, CalleClient

with CalleClient(api_key=os.environ["CALLE_API_KEY"]) as client:
    try:
        client.calls.get("call_123")
    except CalleAPIError as error:
        print(error.status_code, error.code, error.details)
        raise
```

## Release

This repository publishes the Python distribution `calle-ai`. Application code
imports it as `calle`.

Merging to `main` runs CI and does not publish the package. A stable publish is
triggered only by publishing a GitHub Release with a matching `vX.Y.Z` tag;
manually running the workflow performs a dry run. See [RELEASE.md](./RELEASE.md)
for release gates and registry checks.

## Support and security

Use [GitHub Issues](https://github.com/CALLE-AI/server-sdk-python/issues) for
reproducible SDK bugs and feature requests. Do not report vulnerabilities in a
public issue. Follow [SECURITY.md](./SECURITY.md) for private reporting.

## License

This project is licensed under the [MIT License](./LICENSE). The same license
applies to the published PyPI distributions `calle-ai==0.6.0` and
`calle-ai==0.7.0`.

## Project Documents

- [CONTRIBUTING.md](./CONTRIBUTING.md)
- [CHANGELOG.md](./CHANGELOG.md)
- [SECURITY.md](./SECURITY.md)
- [RELEASE.md](./RELEASE.md)
