- setup_telnyx.py: automates fax app creation, number purchase, and config generation via Telnyx API - Gitignore PROJECT.md and CLAUDE.md to keep project files local Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 lines
8.3 KiB
Python
Executable file
259 lines
8.3 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Telnyx setup wizard - automates account provisioning for AutoFax.
|
|
|
|
Run this once after signing up at portal.telnyx.com and getting an API key.
|
|
It will:
|
|
1. Create a Fax Application
|
|
2. Search for and purchase a fax-capable phone number
|
|
3. Assign the number to the Fax Application
|
|
4. Write the .env file (or autofax_config.json for the GUI)
|
|
|
|
Usage:
|
|
python setup_telnyx.py # interactive
|
|
python setup_telnyx.py --api-key KEY_xxx --to +18019382102 # non-interactive
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
API = "https://api.telnyx.com/v2"
|
|
BASE_DIR = Path(__file__).parent
|
|
|
|
|
|
def api(method, path, token, **kwargs):
|
|
"""Make a Telnyx API call. Raises on HTTP error with details."""
|
|
resp = requests.request(
|
|
method,
|
|
f"{API}{path}",
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
**kwargs,
|
|
)
|
|
if not resp.ok:
|
|
detail = resp.text[:500]
|
|
print(f" API error ({resp.status_code}): {detail}", file=sys.stderr)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
def create_fax_application(token: str, name: str = "AutoFax") -> dict:
|
|
"""Create a Fax Application. Returns the app data including connection ID."""
|
|
print(f"Creating Fax Application '{name}'...")
|
|
data = api("POST", "/fax_applications", token, json={
|
|
"application_name": name,
|
|
"webhook_event_url": "https://example.com/fax-status",
|
|
"active": True,
|
|
})
|
|
app = data["data"]
|
|
print(f" Created: {app['application_name']} (ID: {app['id']})")
|
|
return app
|
|
|
|
|
|
def search_fax_numbers(token: str, area_code: str = None, limit: int = 5) -> list:
|
|
"""Search for available fax-capable phone numbers."""
|
|
print("Searching for fax-capable numbers...")
|
|
params = {
|
|
"filter[country_code]": "US",
|
|
"filter[features]": "fax",
|
|
"filter[phone_number_type]": "local",
|
|
"filter[limit]": limit,
|
|
}
|
|
if area_code:
|
|
params["filter[national_destination_code]"] = area_code
|
|
|
|
data = api("GET", "/available_phone_numbers", token, params=params)
|
|
numbers = data.get("data", [])
|
|
if not numbers:
|
|
print(" No numbers found. Try a different area code.")
|
|
return numbers
|
|
|
|
|
|
def order_number(token: str, phone_number: str, connection_id: str) -> dict:
|
|
"""Purchase a phone number and assign it to the fax application."""
|
|
print(f"Ordering {phone_number}...")
|
|
data = api("POST", "/number_orders", token, json={
|
|
"phone_numbers": [{"phone_number": phone_number}],
|
|
"connection_id": connection_id,
|
|
})
|
|
order = data["data"]
|
|
print(f" Order ID: {order['id']} | Status: {order.get('status', 'pending')}")
|
|
|
|
# Wait for order to complete
|
|
order_id = order["id"]
|
|
for attempt in range(12):
|
|
time.sleep(5)
|
|
check = api("GET", f"/number_orders/{order_id}", token)
|
|
status = check["data"].get("status", "pending")
|
|
if status == "success":
|
|
print(f" Number purchased successfully!")
|
|
return check["data"]
|
|
elif status in ("failed",):
|
|
print(f" Order failed: {check['data']}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print(f" Waiting... ({status})")
|
|
|
|
print(" Order is still processing. Check the Telnyx portal.")
|
|
return order
|
|
|
|
|
|
def assign_number_to_app(token: str, phone_number_id: str, connection_id: str):
|
|
"""Assign a purchased number to the fax application."""
|
|
print(f"Assigning number to fax application...")
|
|
api("PATCH", f"/phone_numbers/{phone_number_id}", token, json={
|
|
"connection_id": connection_id,
|
|
})
|
|
print(" Done.")
|
|
|
|
|
|
def write_env(api_key: str, connection_id: str, from_number: str, to_number: str):
|
|
"""Write the .env file for the CLI workflow."""
|
|
env_path = BASE_DIR / ".env"
|
|
content = f"""# Generated by setup_telnyx.py
|
|
TELNYX_API_KEY={api_key}
|
|
TELNYX_CONNECTION_ID={connection_id}
|
|
TELNYX_FROM_NUMBER={from_number}
|
|
FAX_TO_NUMBER={to_number}
|
|
|
|
# ntfy notification URL (optional)
|
|
# NTFY_URL=https://nt.nevo.engineer/autofax
|
|
# NTFY_TOKEN=Bearer tk_your_token_here
|
|
"""
|
|
env_path.write_text(content)
|
|
print(f"\n Wrote {env_path}")
|
|
|
|
|
|
def write_gui_config(api_key: str, connection_id: str, from_number: str, to_number: str):
|
|
"""Write the GUI config file."""
|
|
config_path = BASE_DIR / "autofax_config.json"
|
|
config = {
|
|
"api_key": api_key,
|
|
"connection_id": connection_id,
|
|
"from_number": from_number,
|
|
"to_number": to_number,
|
|
"pdf_path": "",
|
|
"ntfy_url": "",
|
|
"ntfy_token": "",
|
|
}
|
|
config_path.write_text(json.dumps(config, indent=2))
|
|
print(f" Wrote {config_path}")
|
|
|
|
|
|
def pick_number(numbers: list) -> str:
|
|
"""Let the user pick from available numbers."""
|
|
print("\nAvailable numbers:")
|
|
for i, n in enumerate(numbers, 1):
|
|
pn = n.get("phone_number", "?")
|
|
region = n.get("region_information", [{}])
|
|
loc = ""
|
|
if region:
|
|
loc = f" ({region[0].get('region_name', '')}, {region[0].get('region_type', '')})"
|
|
cost = n.get("cost_information", {}).get("monthly_cost", "?")
|
|
print(f" [{i}] {pn}{loc} - ${cost}/mo")
|
|
|
|
while True:
|
|
try:
|
|
choice = input(f"\nSelect a number [1-{len(numbers)}]: ").strip()
|
|
idx = int(choice) - 1
|
|
if 0 <= idx < len(numbers):
|
|
return numbers[idx]["phone_number"]
|
|
except (ValueError, EOFError):
|
|
pass
|
|
print("Invalid choice, try again.")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Set up Telnyx for AutoFax")
|
|
parser.add_argument("--api-key", help="Telnyx API key (starts with KEY_)")
|
|
parser.add_argument("--to", help="Destination fax number in E.164 format")
|
|
parser.add_argument("--area-code", help="Preferred area code for your fax number")
|
|
parser.add_argument("--app-name", default="AutoFax", help="Fax application name")
|
|
args = parser.parse_args()
|
|
|
|
print("=" * 50)
|
|
print(" AutoFax - Telnyx Setup Wizard")
|
|
print("=" * 50)
|
|
print()
|
|
|
|
# Get API key
|
|
api_key = args.api_key
|
|
if not api_key:
|
|
print("You need a Telnyx API key. Get one at:")
|
|
print(" https://portal.telnyx.com/ -> API Keys")
|
|
print()
|
|
api_key = input("Paste your API key: ").strip()
|
|
|
|
if not api_key.startswith("KEY"):
|
|
print("Warning: API key usually starts with 'KEY'. Continuing anyway...")
|
|
|
|
# Get destination number
|
|
to_number = args.to
|
|
if not to_number:
|
|
to_number = input("Destination fax number (E.164, e.g. +18019382102): ").strip()
|
|
|
|
# Verify API key works
|
|
print("\nVerifying API key...")
|
|
try:
|
|
api("GET", "/balance", api_key)
|
|
print(" API key is valid.")
|
|
except Exception:
|
|
print("ERROR: Invalid API key or account issue.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Step 1: Create fax application
|
|
print()
|
|
app = create_fax_application(api_key, args.app_name)
|
|
connection_id = app["id"]
|
|
|
|
# Step 2: Search for a number
|
|
print()
|
|
numbers = search_fax_numbers(api_key, area_code=args.area_code)
|
|
if not numbers:
|
|
# Try without area code filter
|
|
numbers = search_fax_numbers(api_key)
|
|
if not numbers:
|
|
print("ERROR: No fax numbers available. Try again later.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Step 3: Pick and order a number
|
|
if args.area_code and len(numbers) == 1:
|
|
chosen = numbers[0]["phone_number"]
|
|
print(f"\nAuto-selected: {chosen}")
|
|
else:
|
|
chosen = pick_number(numbers)
|
|
|
|
print()
|
|
order_number(api_key, chosen, connection_id)
|
|
|
|
# Step 4: Write config files
|
|
print()
|
|
print("Writing configuration...")
|
|
write_env(api_key, connection_id, chosen, to_number)
|
|
write_gui_config(api_key, connection_id, chosen, to_number)
|
|
|
|
print()
|
|
print("=" * 50)
|
|
print(" Setup complete!")
|
|
print("=" * 50)
|
|
print()
|
|
print(f" Fax Application: {app['application_name']}")
|
|
print(f" Connection ID: {connection_id}")
|
|
print(f" From Number: {chosen}")
|
|
print(f" To Number: {to_number}")
|
|
print()
|
|
print("Next steps:")
|
|
print(" 1. Place your claim PDF in the claims/ directory")
|
|
print(" 2. Linux: ./install.sh")
|
|
print(" 3. Windows: Double-click AutoFax.exe")
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|