Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Tests

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
- run: python -m pip install --upgrade pip
- run: python -m pip install -e ".[dev]"
- run: ruff check .
- run: mypy src/nextgenswitch
- run: pytest
- run: python -m build
- run: python -m twine check dist/*
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__pycache__/
*.py[cod]
*.egg-info/
.coverage
.mypy_cache/
.pytest_cache/
.ruff_cache/
.venv/
build/
dist/
.env
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Infosoftbd Solutions

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
207 changes: 206 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,208 @@
# NextGenSwitch Python SDK

SDK development is tracked in pull requests.
[![Tests](https://github.com/nextgenswitch/nextgenswitch-python/actions/workflows/tests.yml/badge.svg)](https://github.com/nextgenswitch/nextgenswitch-python/actions/workflows/tests.yml)
[![Python](https://img.shields.io/badge/Python-3.9%2B-3776AB.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

The official Python SDK for the [NextGenSwitch Programmable Voice API](https://nextgenswitch.com/docs/programmable-voice-api/). Create and modify calls with synchronous or asynchronous clients, build escaped Voice XML, stream audio to AI services, and parse Gather and Dial callbacks.

## Requirements

- Python 3.9 or newer
- A NextGenSwitch deployment and API credentials

## Installation

```bash
pip install nextgenswitch
```

Until the first PyPI release, install from GitHub:

```bash
pip install "nextgenswitch @ git+https://github.com/nextgenswitch/nextgenswitch-python.git"
```

## Configure the Client

```python
import os
from nextgenswitch import Client

client = Client(
os.environ["NEXTGENSWITCH_BASE_URL"],
os.environ["NEXTGENSWITCH_AUTHORIZATION"],
os.environ["NEXTGENSWITCH_AUTHORIZATION_SECRET"],
)
```

The SDK sends the documented `X-Authorization` and `X-Authorization-Secret` headers. Keep these values in environment variables or a secret manager and use HTTPS for remote deployments.

## Create a Call

```python
from nextgenswitch import VoiceResponse

flow = VoiceResponse().say("Welcome to NextGenSwitch.").gather(
action="https://example.com/gather",
method="POST",
numDigits=1,
timeout=10,
children=lambda gather: gather.say("Press one for sales."),
)

result = client.create_call(
"2001",
"1001",
response_xml=flow,
status_callback="https://example.com/call-status",
)
print(result.data)
```

Use `response_url="https://example.com/call-flow.xml"` instead of `response_xml` to provide a hosted XML document. Exactly one response source is required.

## Modify an Active Call

```python
updated = (
VoiceResponse()
.pause(2)
.say("Your call flow has been updated.")
.dial("1000", answerOnBridge=True)
)

client.modify_call("CALL-123", updated)
```

## Async Client

```python
import asyncio
import os

from nextgenswitch import AsyncClient, VoiceResponse

async def main() -> None:
async with AsyncClient(
os.environ["NEXTGENSWITCH_BASE_URL"],
os.environ["NEXTGENSWITCH_AUTHORIZATION"],
os.environ["NEXTGENSWITCH_AUTHORIZATION_SECRET"],
) as client:
result = await client.create_call(
"2001",
"1001",
response_xml=VoiceResponse().say("Hello from async Python."),
)
print(result.data)

asyncio.run(main())
```

## Voice XML

| XML verb | Python method |
| --- | --- |
| `<Say>` | `say(text, **attributes)` |
| `<Play>` | `play(url, **attributes)` |
| `<Gather>` | `gather(children=..., **attributes)` |
| `<Dial>` | `dial(to, children=..., **attributes)` |
| `<Record>` | `record(**attributes)` |
| `<Connect><Stream>` | `stream(url, parameters=..., **attributes)` |
| `<Hangup>` | `hangup()` |
| `<Pause>` | `pause(seconds)` |
| `<Redirect>` | `redirect(url, method=...)` |
| `<Bridge>` | `bridge(call_id, bridge_after_establish=...)` |
| `<Leave>` | `leave()` |

Python's XML library escapes text and attributes instead of concatenating untrusted XML strings.

## Record a Call

```python
flow = (
VoiceResponse()
.say("Please leave your message after the beep.")
.record(
action="https://example.com/recording",
method="POST",
timeout=5,
finishOnKey="#",
transcribe=True,
trim=True,
beep=True,
)
.hangup()
)
client.create_call("2001", "1001", response_xml=flow)
```

## Stream to an AI Voice Service

```python
flow = VoiceResponse().stream(
"wss://voice.example.com/session",
parameters={"session_id": "session-123", "tenant": "example"},
name="assistant-stream",
)
```

Resolve AI-provider secrets on the WebSocket service. Never put provider API keys in Voice XML.

## Parse Webhooks

```python
from nextgenswitch import GatherResult

gather = GatherResult.from_mapping(request.form)
if gather.digits == "1":
response = VoiceResponse().say("Connecting sales.").dial("1001")
else:
response = VoiceResponse().say("No valid selection received.").hangup()
```

Use `DialResult.from_mapping(payload)` for documented Dial action fields. Callback signature verification is not documented by the current API; restrict endpoints, require TLS, validate expected fields, and add deployment-appropriate authentication.

## Errors

- `ValidationError`: invalid SDK input
- `ApiError`: non-2xx response, with `status_code` and `response_body`
- `TransportError`: network or HTTP transport failure
- `NextGenSwitchError`: base SDK exception

## Examples

Configure `NEXTGENSWITCH_BASE_URL`, `NEXTGENSWITCH_AUTHORIZATION`, and `NEXTGENSWITCH_AUTHORIZATION_SECRET`, then adapt:

- [Create call](examples/create_call.py)
- [Modify active call](examples/modify_call.py)
- [Record caller audio](examples/record_call.py)
- [Stream to AI](examples/stream_to_ai.py)

All `example.com` URLs are placeholders for TLS endpoints you control.

## Development

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
ruff check .
mypy src/nextgenswitch
pytest
python -m build
python -m twine check dist/*
```

GitHub Actions validates Python 3.9–3.13.

## Documentation

- [Programmable Voice API](https://nextgenswitch.com/docs/programmable-voice-api/)
- [NextGenSwitch documentation](https://nextgenswitch.com/docs/)
- [NextGenSwitch website](https://nextgenswitch.com/)
- [Contact NextGenSwitch](https://nextgenswitch.com/contact/)

## License

[MIT](LICENSE)
26 changes: 26 additions & 0 deletions examples/create_call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os

from nextgenswitch import Client, VoiceResponse

client = Client(
os.environ["NEXTGENSWITCH_BASE_URL"],
os.environ["NEXTGENSWITCH_AUTHORIZATION"],
os.environ["NEXTGENSWITCH_AUTHORIZATION_SECRET"],
)

flow = VoiceResponse().say("Welcome to NextGenSwitch.").gather(
action="https://example.com/gather",
method="POST",
numDigits=1,
timeout=10,
children=lambda gather: gather.say("Press one for sales or two for support."),
)

result = client.create_call(
"2001",
"1001",
response_xml=flow,
status_callback="https://example.com/call-status",
)
print(result.data)
client.close()
19 changes: 19 additions & 0 deletions examples/modify_call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import os
import sys

from nextgenswitch import Client, VoiceResponse

if len(sys.argv) != 2:
raise SystemExit("Usage: python examples/modify_call.py CALL_ID")

client = Client(
os.environ["NEXTGENSWITCH_BASE_URL"],
os.environ["NEXTGENSWITCH_AUTHORIZATION"],
os.environ["NEXTGENSWITCH_AUTHORIZATION_SECRET"],
)
flow = VoiceResponse().pause(1).say("Your call flow has been updated.").dial(
"1000", answerOnBridge=True, timeLimit=300
)
result = client.modify_call(sys.argv[1], flow)
print(result.data)
client.close()
26 changes: 26 additions & 0 deletions examples/record_call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os

from nextgenswitch import Client, VoiceResponse

client = Client(
os.environ["NEXTGENSWITCH_BASE_URL"],
os.environ["NEXTGENSWITCH_AUTHORIZATION"],
os.environ["NEXTGENSWITCH_AUTHORIZATION_SECRET"],
)
flow = (
VoiceResponse()
.say("Please leave your message after the beep.")
.record(
action="https://example.com/recording",
method="POST",
timeout=5,
finishOnKey="#",
transcribe=True,
trim=True,
beep=True,
)
.say("Thank you. Goodbye.")
.hangup()
)
print(client.create_call("2001", "1001", response_xml=flow).data)
client.close()
19 changes: 19 additions & 0 deletions examples/stream_to_ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import os
import secrets

from nextgenswitch import Client, VoiceResponse

client = Client(
os.environ["NEXTGENSWITCH_BASE_URL"],
os.environ["NEXTGENSWITCH_AUTHORIZATION"],
os.environ["NEXTGENSWITCH_AUTHORIZATION_SECRET"],
)
flow = VoiceResponse().say("Connecting the virtual assistant.").stream(
"wss://voice.example.com/session",
parameters={"session_id": secrets.token_hex(16), "tenant": "example"},
name="ai-assistant",
)
print(client.create_call("2001", "1001", response_xml=flow).data)
client.close()

# Resolve AI-provider credentials on the WebSocket service, not in Voice XML.
Loading
Loading