Metadata-Version: 2.5
Name: helpvox
Version: 0.2.0
Summary: Official Python SDK for the HelpVox app platform
Project-URL: Homepage, https://www.helpvox.ai/docs/apps
Author: HelpVox
License: MIT
License-File: LICENSE
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# helpvox — Python SDK for the HelpVox app platform

Build apps that plug into HelpVox: install via OAuth, receive events, give the AI agent tools,
embed a dashboard page and call the REST API. Python 3.9+, no dependencies.

```bash
pip install helpvox
```

The wire contract this SDK follows is in [`docs/apps/platform-contract.md`](../../docs/apps/platform-contract.md).
You need your app's `client_id` (`hvapp_…`) and `client_secret` (`hvs_…`). The secret is the HMAC key
for everything HelpVox sends you — keep it in one environment variable.

## Install (OAuth)

Send the merchant to the consent screen, then exchange the code on your callback:

```python
import helpvox

url = helpvox.install_url("loyalty-points", redirect_url="https://app.example/oauth/callback", state=csrf)

# GET /oauth/callback?code=hvic_…&installation_id=…&state=…
token = helpvox.exchange_code(CLIENT_ID, CLIENT_SECRET, code)
# {"access_token": "hva_…", "token_type": "bearer", "scope": "…", "installation_id": "…", "vendor_id": "…"}
store(token["installation_id"], token["access_token"])

# Your agent tools stay off until you say you are ready:
helpvox.Client(token["access_token"]).ready()
```

Codes live 10 minutes and work once — exchanging the same code twice revokes the installation.
Failures raise `helpvox.ApiError` (`e.code == "invalid_grant"`).

## Events

Always verify against the **raw** request body.

```python
from helpvox import parse_event, SignatureError

try:
    event = parse_event(CLIENT_SECRET, raw_body, request_headers)
except SignatureError:
    return 401

if already_processed(event.id):   # the same id is sent on every retry
    return 200
if event.type == "order.status_changed":
    ...
elif event.type == "app.uninstalled":
    delete_data(event.installation_id)
```

`Event` has `id, type, created_at, vendor_id, installation_id, data, is_test`. Header lookup is
case-insensitive and accepts any dict-like headers object. Answer 2xx within 10 seconds.
Send yourself a sample with `client.send_test_event("order.created")` (`event.is_test` is `True`).

## Agent tools

Your handler gets `(input, context)` and returns plain text for the agent (cut to 2000 characters),
or raises `ToolError` with a short reason the agent can tell the customer.

```python
from helpvox import ToolContext, ToolError, handle_tool_call

def get_balance(input: dict, ctx: ToolContext) -> str:
    points = lookup(ctx.installation_id, input["phone"])
    if points is None:
        raise ToolError("There is no loyalty account for this phone number.")
    return f"The customer has {points} points."
```

`handle_tool_call` returns `(status, json_body)`: `401` for a bad signature, `400` for a malformed
body, otherwise `200` with `{"result": …}` or `{"error": …}`. Other exceptions propagate; HelpVox
then continues without the tool. `ToolContext` has `tool, installation_id, vendor_id, channel,
conversation_id, language, country, caller_phone`.

Flask:

```python
from flask import Flask, jsonify, request

app = Flask(__name__)

@app.post("/tools/balance")
def balance():
    status, body = handle_tool_call(CLIENT_SECRET, request.get_data(), request.headers, get_balance)
    return jsonify(body), status
```

FastAPI:

```python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post("/tools/balance")
async def balance(request: Request):
    status, body = handle_tool_call(CLIENT_SECRET, await request.body(), request.headers, get_balance)
    return JSONResponse(body, status_code=status)
```

Phone tools have 1500 ms by default, so keep the handler fast (if it blocks, run it in a thread pool
in async frameworks). Events work the same way: `parse_event(CLIENT_SECRET, request.get_data(), request.headers)`.

## Dashboard page

If your manifest sets `ui.settings_page`, HelpVox embeds it with `?installation_id=…&session=<JWT>`.
The token lives 60 seconds — verify it, then start your own session:

```python
from helpvox import verify_session_token

claims = verify_session_token(CLIENT_SECRET, request.args["session"], CLIENT_ID)
if claims is None:
    abort(401)
login(installation_id=claims["installation_id"], user=claims["sub"], lang=claims["lang"])
```

`sign_session_token(secret, claims)` issues one the same way HelpVox does, for your tests.

## REST API

```python
from helpvox import ApiError, Client

client = Client(access_token)

client.app()                          # installation, vendor, scopes, ready, capabilities
orders = client.orders(status="new", limit=50)
client.order(order_id)
client.update_order(order_id, {"note": "Gift wrap"})          # orders:write
client.products(); client.calls(); client.call(call_id)
client.callbacks(); client.appointments()

d = client.create_discount(conversation_id, 12.00, "EUR", "Loyalty points", reference="ledger-42")
client.discount(d["id"]); client.cancel_discount(d["id"])

# anything else
client.request("GET", "/api/v1/categories")

try:
    client.calls()
except ApiError as e:
    print(e.status, e.code, e.message, e.scope)   # 403 insufficient_scope … conversations:read
```

Customer fields are removed from responses without `customers:read`; transcripts without
`transcripts:read`.

For tests, pass `transport=` — a callable `(method, url, headers, body_bytes) -> (status, body_bytes)` —
to `Client` or `exchange_code`. Signatures: `helpvox.sign(secret, body)` / `helpvox.verify(secret, body, header)`.

## Development

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