From 0748178355bedda6c3832989670ad6afabab11d3 Mon Sep 17 00:00:00 2001 From: Sochen Date: Fri, 6 Mar 2026 21:35:18 +0000 Subject: [PATCH] Add Telnyx setup wizard, remove PROJECT.md from repo - 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 --- .gitignore | 4 + PROJECT.md | 5 - README.md | 30 +++++- setup_telnyx.py | 259 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 288 insertions(+), 10 deletions(-) delete mode 100644 PROJECT.md create mode 100755 setup_telnyx.py diff --git a/.gitignore b/.gitignore index 0b4b9c6..e33f485 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Project/session files +PROJECT.md +CLAUDE.md + # Credentials and config .env diff --git a/PROJECT.md b/PROJECT.md deleted file mode 100644 index 78657aa..0000000 --- a/PROJECT.md +++ /dev/null @@ -1,5 +0,0 @@ -Basically, my health insurance claims to cover doulas, however requires a tedious process where one must either fax them or snail mail them the claim. I did both. The fax returned as failure and I haven't seen the claim added even after mailing it a month ago. My coworker has the same issue. - -Build a script (can integrate with cron) to sent an electronic fax to them every hour on the hour, and safely collect the returned failed responses in a directory. I will show this to them or to my companies HR. Also find an electronic fax API to use with a service that is the cheapest for a single month. Assume the doula claim paperwork is about 5 black and white pages. - -You've got a figure out directive, you can ask some clarifying questions in the beginning, but then I want you to go and build the thing. Preferred language is python. diff --git a/README.md b/README.md index 76da386..a87f72c 100644 --- a/README.md +++ b/README.md @@ -24,24 +24,44 @@ build_windows.bat This produces `dist/AutoFax.exe` -- a single self-contained file you can copy to a flash drive. -## Linux/Server Setup (CLI + cron) +## Telnyx Account Setup -### 1. Telnyx Account +### Automated (recommended) + +The setup wizard handles everything -- creates a fax application, buys a number, and writes your config: + +```bash +# Interactive +python3 setup_telnyx.py + +# Or non-interactive +python3 setup_telnyx.py --api-key KEY_xxx --to +18019382102 --area-code 801 +``` + +You just need a Telnyx API key: +1. Sign up at [portal.telnyx.com](https://portal.telnyx.com/) +2. Add a payment method +3. Go to **API Keys** and copy your key + +The wizard creates both `.env` (for CLI) and `autofax_config.json` (for GUI). + +### Manual 1. Sign up at [portal.telnyx.com](https://portal.telnyx.com/) 2. Add a payment method -3. Go to **Messaging > Fax** and create a Fax Application +3. Go to **Messaging > Fax** and create a Fax Application (note the Connection ID) 4. Go to **Numbers** and purchase a fax-enabled number (~$1/month) 5. Assign the number to your Fax Application 6. Go to **API Keys** and copy your API key - -### 2. Configure +7. Configure: ```bash cp .env.example .env # Edit .env with your Telnyx credentials and the destination fax number ``` +## Linux/Server Setup (CLI + cron) + ### 3. Add Your Claim Place your claim PDF in the `claims/` directory: diff --git a/setup_telnyx.py b/setup_telnyx.py new file mode 100755 index 0000000..9a0412b --- /dev/null +++ b/setup_telnyx.py @@ -0,0 +1,259 @@ +#!/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()