Metadata-Version: 2.4
Name: byourside
Version: 0.1.0
Summary: By Your Side SDK: give an AI a phone (outbound objective-driven calls).
Author: By Your Side
License: MIT
Project-URL: Homepage, https://byourside.ai/docs/agent-api
Project-URL: Repository, https://github.com/allexp1/voip-agent
Keywords: voice,ai,phone,outbound,agent,sdk,voip,calls
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Telephony
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# By Your Side Python SDK

Zero-dependency Python client for the By Your Side Agent Outbound-Call API.
Place objective-driven AI calls, poll for results, and verify webhooks using only the Python standard library.

Requires Python 3.8+. No third-party packages needed.

---

## Quick start

### Place a call and wait for the result

```python
from byourside import Client

client = Client(api_key="bys_ak_YOUR_KEY")

# Place an outbound call with an objective
call = client.place_call(
    to="+14155550123",
    objective="Confirm the appointment for tomorrow at 2pm and ask if they need to reschedule.",
    fields=[
        {"name": "confirmed", "type": "boolean"},
        {"name": "new_time", "type": "string"},
    ],
)
print("Placed:", call["callId"], "status:", call["status"])

# Poll until the call reaches a terminal status (default timeout: 180s)
result = client.wait_for_call(call["callId"], timeout=180, interval=5)
print("Final status:", result["status"])
print("Extracted fields:", result.get("extracted"))
```

### List recent calls

```python
calls = client.list_calls(limit=10)
for c in calls:
    print(c["callId"], c["status"])
```

### Get a specific call

```python
call = client.get_call("call_abc123")
print(call)
```

---

## Call statuses

| Status | Meaning |
|---|---|
| `queued` | Call accepted, waiting to be placed |
| `in_progress` | Call is active |
| `completed` | Call finished normally (terminal) |
| `no_answer` | Recipient did not pick up (terminal) |
| `voicemail` | Reached voicemail (terminal) |
| `declined` | Recipient declined the call (terminal) |
| `failed` | Call could not complete (terminal) |

`wait_for_call` returns once the status is one of the five terminal states.

---

## Webhook verification

By Your Side sends a `X-BYS-Signature` header with each webhook delivery.
Verify it before processing the payload.

### Flask example

```python
from flask import Flask, request, abort
from byourside import verify_webhook

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_YOUR_SECRET"

@app.route("/webhook/bys", methods=["POST"])
def bys_webhook():
    sig = request.headers.get("X-BYS-Signature", "")
    raw_body = request.get_data(as_text=True)
    if not verify_webhook(sig, raw_body, WEBHOOK_SECRET):
        abort(400, "Invalid signature")
    payload = request.get_json()
    print("Call update:", payload["callId"], payload["status"])
    return "", 200
```

The signature format is `t=<unix_seconds>,v1=<hex>` where
`hex = HMAC-SHA256(secret, f"{t}.{raw_body}")`.

`verify_webhook` never raises. It returns `False` for invalid or expired signatures
(default tolerance: 300 seconds).

---

## Error handling

All API errors raise `ByoursideError`:

```python
from byourside import Client, ByoursideError

client = Client(api_key="bys_ak_YOUR_KEY")
try:
    client.place_call(to="+19001234567", objective="Sell something")
except ByoursideError as e:
    print(e.code)    # e.g. "destination_blocked"
    print(e.status)  # HTTP status, or 0 for network/timeout errors
    print(str(e))    # Human-readable message
```

Common error codes:

| Code | Meaning |
|---|---|
| `destination_blocked` | Destination not allowed (premium, IRSF, or unsupported country) |
| `invalid_number` | Number must be E.164, e.g. +14155550123 |
| `to_required` | Missing destination number |
| `objective_required` | Missing call objective |
| `caller_id_not_owned` | Caller ID not on your account |
| `rate_limited` | Rate limit reached, retry shortly |
| `over_minute_cap` | Outbound usage limit reached |
| `unauthorized` | Invalid or missing API key |
| `not_found` | Call ID not found |
| `placement_failed` | Carrier or trunk issue, retry shortly |
| `store_error` | Temporary service error, retry shortly |
| `network_error` | Could not reach the API |
| `timeout` | `wait_for_call` deadline exceeded |

---

## Development

Clone the repo. No install step needed for local use: run tests from the `sdks/python/` directory so that `byourside/` is on the path automatically.

```bash
cd sdks/python
python3 -m unittest discover -s tests
```

Expected output: all tests pass (11 tests across errors, client, wait, and webhooks).

If your environment does not add CWD to the path automatically, use:

```bash
cd sdks/python
PYTHONPATH=. python3 -m unittest discover -s tests
```

Syntax check all source files:

```bash
python3 -m py_compile byourside/errors.py byourside/client.py byourside/webhooks.py byourside/__init__.py
```

Publishing to PyPI is deferred. Do not publish without explicit owner approval.
