ax-check.com
by Gauge
B
Bandwidth
bandwidth.com
ax-check.com
Checked 29 September 2026
B
73/100
Agents need your login
Bandwidth's docs and pricing are clear, but llms.txt falls short
Getting-started guide and API reference give concrete steps and rates; llms.txt is a blog dump with no navigation or API/MCP links. 16 of 23 checks passed.
5 fixes to start with
Concrete changes, backed by what we found.
Tell agents where to start and organize the links in /llms.txt.
llms.txt offers no navigation guidance or starting path for agents.
List every offered API, MCP, and skills surface in /llms.txt.
llms.txt links no API reference, MCP server, or skills despite offering APIs.
Recorded coding sessions
Same task, 3 models, no bandwidth.com account. Recorded 29 September 2026 on Pi.
3 recorded · 9m 15s
9m 15s · recorded
Session findings
Select a finding to see it in context.
›Thinking…
The user is asking for a simple example using Bandwidth (the communications API company — voice, SMS). Let me check whether there's a repo with any relevant context, and whether there are credentials available.
Let me take a look at the sandbox.
✓$ pwd; ls -la; cat README* 2>/dev/null | head -50; env | grep -i -E 'bandwidth|BANDWIDTH' | sed 's/=.*/=<set>/'✓└{"content":[{"text":"/sandbox/repo\ntotal 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 29 08:17 ..\ndrwxr-xr-x 8 root root 40…ctrl+o
{"content":[{"text":"/sandbox/repo\ntotal 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 29 08:17 ..\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05 .git\n-rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore\n","type":"text"}]}✓$ ls /sandbox; ls ~; cat ~/.netrc 2>/dev/null; which node python3 curl; node -v 2>/dev/null; python3 -V✓└{"content":[{"text":"repo\nrepo\n/usr/local/bin/node\n/usr/local/bin/python3\n/usr/bin/curl\nv22.23.2\nPython 3.12.3\n","type":"text"}]}
›Thinking…
Empty repository, no Bandwidth credentials in the environment variables. Let me check network access to Bandwidth's API/documentation. Also, I'll check if credentials are hidden somewhere else (.env, config files).
✓$ cd /sandbox/repo && git log --oneline -5 2>/dev/null; cat .gitignore; ls -la ~; find / -maxdepth 3 -name "*.env" -o -maxdepth 3 -name "bandwidth*" 2>/dev/null | head✓└{"content":[{"text":"9ebf5fb Initial workspace\n# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript …ctrl+o
{"content":[{"text":"9ebf5fb Initial workspace\n# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/\n.next/\ndist/\nbuild/\ncoverage/\n\n# Python\n.venv/\nvenv/\n__pycache__/\n*.py[cod]\n.pytest_cache/\n*.egg-info/\n\n# Rust\ntarget/\n\n# Go / PHP / Ruby vendored deps\nvendor/\n.bundle/\n\n# Java / Kotlin / Scala\n.gradle/\n\n# .NET\nbin/\nobj/\n\n# Elixir\n_build/\ndeps/\n\n# Dart / Flutter\n.dart_tool/\ntotal 28\ndrwxr-xr-x 7 root root 4096 Sep 29 08:17 .\ndrwxr-xr-x 22 root root 4096 Sep 18 07:59 ..\ndrwxr-xr-x 3 root root 4096 Sep 29 08:17 .cache\ndrwxr-xr-x 3 root root 4096 Sep 29 08:17 .gauge\ndrwxr-xr-x 3 root root 4096 Sep 29 08:17 .pi\ndrwxrwxrwt 2 root root 4096 Sep 18 08:00 .tmp\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 repo\n","type":"text"}]}✓$ curl -sS -m 20 -o /dev/null -w "%{http_code}\n" https://dashboard.bandwidth.com/api/v2/ ; curl -sS -m 20 -o /dev/null -w "%{http_code}\n" https://api.bandwidth.com/api/v2/; curl -sS -m 20 -o /dev/null…✓└{"content":[{"text":"401\n401\n200\n301\n","type":"text"}]}ctrl+o
curl -sS -m 20 -o /dev/null -w "%{http_code}\n" https://dashboard.bandwidth.com/api/v2/ ; curl -sS -m 20 -o /dev/null -w "%{http_code}\n" https://api.bandwidth.com/api/v2/; curl -sS -m 20 -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/@bandwidth/messaging 2>&1 | tail -1; curl -sS -m 20 -o /dev/null -w "%{http_code}\n" https://pypi.org/simple/bandwidth-sdk 2>&1 | tail -1
›Thinking…
Network is reachable. Bandwidth's API endpoints return 401 (authentication required) — as expected without credentials. Credentials aren't available.
Therefore: without account credentials (username/password/accountId), I can't actually call the Bandwidth API. Bandwidth requires a paid account, a phone number from their inventory, and 10DLC/A2P registration for SMS in the US.
The user's request: "I want you to create a simple example using Bandwidth. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get stuck."
So, I should build a simple example — write code using their SDK (the hosted product via SDK/API — lightweight, no local stack). However, I can't run it live without credentials. I should clearly note that I got stuck on credentials (getting 401 without credentials).
Let me check the Bandwidth SDK. Official SDKs: bandwidth-sdk (Python), @bandwidth/messaging, @bandwidth/voice, @bandwidth/numbers, @bandwidth/accounts on npm. Let me fetch the pricing info from Bandwidth's docs/site.
I'll fetch the pricing page.
✓$ cd /tmp && curl -sS -m 25 https://pypi.org/pypi/bandwidth-sdk/json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['info']['version']); print(d['info']['summary']); print(d['info']['hom…✓└{"content":[{"text":"23.3.0\nBandwidth\nhttps://dev.bandwidth.com/sdks/python {'Homepage': 'https://dev.bandwidth.com/sdks/python'}\n","type":"text"}]}ctrl+o
cd /tmp && curl -sS -m 25 https://pypi.org/pypi/bandwidth-sdk/json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['info']['version']); print(d['info']['summary']); print(d['info']['home_page'], d['info'].get('project_urls'))" 2>&1 | head
✓$ curl -sS -m 25 https://registry.npmjs.org/@bandwidth%2Fmessaging | python3 -c "import json,sys; d=json.load(sys.stdin); v=d['dist-tags']['latest']; print(v); print(d['versions'][v]['description']); pr…✓└{"content":[{"text":"4.1.7\nBandwidth's HTTP Messaging platform\n{'axios': '^1.7.4', 'form-data': '^3.0.0', 'detect-node': '^2.0.4', 'json-bigint': '^1.0.0', 't…ctrl+o
curl -sS -m 25 https://registry.npmjs.org/@bandwidth%2Fmessaging | python3 -c "import json,sys; d=json.load(sys.stdin); v=d['dist-tags']['latest']; print(v); print(d['versions'][v]['description']); print(d['versions'][v].get('dependencies'))" 2>&1 | head
{"content":[{"text":"4.1.7\nBandwidth's HTTP Messaging platform\n{'axios': '^1.7.4', 'form-data': '^3.0.0', 'detect-node': '^2.0.4', 'json-bigint': '^1.0.0', 'tiny-warning': '^1.0.3', 'lodash.flatmap': '^4.5.0', '@apimatic/schema': '^0.6.0'}\n","type":"text"}]}›Thinking…
Let's fetch Bandwidth's pricing information from the site. I'll try dev.bandwidth.com or bandwidth.com/pricing.
✓$ cd /tmp && curl -sSL -m 30 https://www.bandwidth.com/pricing/ -o bw_pricing.html -w "%{http_code}\n"; python3 - <<'EOF'✓└{"content":[{"text":"200\n5509\nPricing | Bandwidth\nSkip to Main Content\nDeveloper docs\nDocumentation\nGetting started\nCode samples\nSupport\nU.S. & Canada\…ctrl+o
cd /tmp && curl -sSL -m 30 https://www.bandwidth.com/pricing/ -o bw_pricing.html -w "%{http_code}\n"; python3 - <<'EOF'
import re,html
t=open('/tmp/bw_pricing.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>','',t)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t))
txt=re.sub(r'[ \t]+',' ',txt)
lines=[l.strip() for l in txt.split('\n') if l.strip()]
out='\n'.join(lines)
print(len(out))
print(out[:6000])
EOF
{"content":[{"text":"200\n5509\nPricing | Bandwidth\nSkip to Main Content\nDeveloper docs\nDocumentation\nGetting started\nCode samples\nSupport\nU.S. & Canada\nGlobal\nSupport plans\nLogin\nBandwidth App\nLegacy Global Portal\n911 Access Portal\nBandwidth Community\nBandwidth Logo\nProducts\nThe Bandwidth Universal Platform → Featured\nThe next level of communications with insights , numbers , and APIs all on one global platform.\nBandwidth AI\nExplore all products\nBack\nCommunication APIs\nVoice API\nMedia streaming\nMessaging API (All in one)\nSMS\nMMS\nRCS\nEmergency Calling API\nWebRTC API\nAuthentication API\nIntegrations\nConversational AI\nSalesforce Contact Center New\nMicrosoft Teams\nDirect Routing\nOperator Connect\nGenesys Cloud CX\nWebex Calling\nZoom Phone\nGoogle Voice SIP Link\nFive9\nPindrop\nWholesale\nVoice Activation Agent\nGlobal SIP Trunking\nVoIP Origination\nVoIP Termination\nEmergency Services\nDynamic Location Routing (DLR)\nInternational Long Distance\nToll-Free Voice\nNumbers And Tooling\nVoice and Messaging Insights\nCampaign Registration\nContact Center Security & Trust\nPhone Numbers\nNumber Management\nPorting\nCaller ID\nCall Verification\nNumber Reputation Management\nAuthenticated Branded Calling\nSolutions\nOur Customer Success Plans → Featured\nOur cross-functional experts hit a >99% SLA response rate for all support tickets. Never go unanswered!\nVolume based pricing\nBack\nBusiness Type\nGlobal 2000 Enterprises\nSaaS Product Builders\nVoice AI Startups\nCommunications Providers\nIndustry\nEcommerce & Retail\nFinance\nHealthcare\nHospitality\nInsurance\nTelecom\nPartners\nBecome a partner\nPartner portal\nResources\nReverb® is coming November 10th → Featured\nGet a first look at what's coming in this insight-packed marquee event.\nBack\nLearn\nBlog\nProduct Tours\nResource Library\nGuides\nResearch reports\nVirtual events\nNews\nCustomer Stories\nGlossary\nReverb®\nInvestors\nFor Developers\nDocumentation\nGetting started\nCode samples\nArticles\nInnovation Studio\nSupport\nHelp center\nCompliance hub\nBandwidth Community\nNetwork status\nCoverage\nPricing\nSupport\nsearch\nTalk to an expert\nRequest trial\nSearch\nDeveloper docs\nSupport\nLogin\nSearch\nSearch\nclose\nPricing that delivers real business value\nWhether you are building your first messaging application or need to power a large cloud communication platform, our direct-to-carrier pricing model gives you the best quality for the best price.\nGet volume-based pricing\nEverything you ever wanted\nCut out the middleman\nDirect-to-carrier pricing\nOwned and operated network\nGlobal reach\nSignature support guaranteed\nCommitted use\nHigh-volume discounts\nYour CFO’s secret weapon\nTrusted by leading communication platforms\nVolume-based pricing\nOur API platform US pricing is designed to help product owners and development teams launch and scale applications easily.\nMessaging\nVoice\nAuthentication\nSIP Trunking\nNumber type\nSMS outbound\nMMS outbound\nU.S. 10DLC\n$0.004\nper message\n$0.015\nper message\nU.S. Short code\n$0.008\nper message\n$0.020\nper message\nU.S. Toll-free\n$0.007\nper message\n$0.020\nper message\nMessaging\nOur messaging APIs let you build 10DLC , Short Code , Toll-free , Alphanumeric, and Global Mobile Number texting into products and applications.\nU.S. local outbound\n$0.0100\nper minute\nU.S. local inbound\n$0.0055\nper minute\nStandard text-to-speech\n$0.0007\nper 100 char.\nEnhanced text-to-speech\n$0.0030\nper 100 char.\nStandard transcription\n$0.0450\nper minute\nReal-time transcription\n$0.0451\nper minute\nRecording\n$0.0020\nper minute\nConferencing\n$0.0015\nper minute\nAnswering Machine Detection\n$0.0060\nper call\nUnidirectional Media Streaming\n$0.0035\nper minute\nIn-App Calling\n$0.0025\nper minute\nBidirectional Media Streaming\n$0.004\nper minute\nVoice API\nEmbed flexible, scalable voice calling into your application with 6-second billing increments, all backed by our nationwide all-IP voice network.\nTwo-factor authentication\n$0.0500\nper auth.\nCall Verification\n$0.1500\nper call\nAuthentication\nSecure your software, website, or application user accounts with multi-factor authentication .\nThe only SIP trunking provider you’ll ever need\nRequest a custom quote\nSIP Trunking\nA customizable, usage-based model for voice, emergency services, and phone numbers for premises-based telecom infrastructure or pure-cloud stack.\nCustom integrations from best-in-class providers\nLearn about our integrations today and explore custom pricing by talking to an expert .\nBring Your Own Carrier (BYOC) Integrations\nMicrosoft Teams\nWebex\nZoom\nGenesys\nFive9\nGoogle Voice\nConversational AI\nBring Your Own AI\nVoice Authentication\nPindrop\nConsolidate confidently with a network that’s built differently.\nSee complete coverage\n* Terms and conditions apply. Rates do not include any applicable taxes or fees. Full product pricing available on request.\n** See coverage for service availability\nRequest a quote\nWe’re proud to power many of the world's most innovative brands. If you're one of those innovators, or aspire to be one, fill out the form to request a quote!\nBandwidth Logo Mark\nGet monthly updates, insights, and strategies\nOur company\nCompany\nContact\nNewsroom\nCareers\nInvestor Relations\nLeadership\nQuick links\nService Status\nDevelopers\nResources\nSecurity\nPartner Program\nBlog\nCustomers\nRelease Notes\nGlossary\nTwilio Alternative\nSinch Alternative\nLumen Alternative\nRegulations\nCoverage\nLegal\nGeneral\nLaw Enforcement Guide\nReport a Phone Number\nPrivacy Notice\nTerms Of Use\nCanada ACA Consultation Process\nRequest trial\nTalk to an expert\nCall Us\n+852 581 8800\nBandwidth.com CLEC, LLC is a wholly owned subsidiary of Bandwidth Inc. ©2026\n","type":"text"}]}›Thinking…
Great — got the pricing info. Also, phone number pricing is important. Let me check the pricing for number search/orders. It's not mentioned on the pricing page (the "Number type" tab shows messaging). Monthly number fees: local is typically $1.15/month, toll-free is $2.50/month... I should avoid exaggeration and check the docs. Let me look up number pricing on dev.bandwidth.com.
Also, I need to create an example. Since there are no credentials, I'll write code using the SDK and a dry-run/mock mode, and clearly indicate that I got stuck (401) at the live API call stage.
First, let me quickly check the number pricing page.
✓$ cd /tmp && for u in https://dev.bandwidth.com/docs/numbers/pricing https://www.bandwidth.com/pricing/numbers/; do echo "== $u"; curl -sSL -m 20 -o out.html -w "%{http_code}\n" $u; python3 - <<'EOF'✓└{"content":[{"text":"== https://dev.bandwidth.com/docs/numbers/pricing\n404\nPage Not Found | Bandwidth API Docs\nSkip to main content Guides API Reference Deve…ctrl+o
cd /tmp && for u in https://dev.bandwidth.com/docs/numbers/pricing https://www.bandwidth.com/pricing/numbers/; do echo "== $u"; curl -sSL -m 20 -o out.html -w "%{http_code}\n" $u; python3 - <<'EOF'
import re,html
t=open('/tmp/out.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>','',t)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t)); txt=re.sub(r'[ \t]+',' ',txt)
lines=[l.strip() for l in txt.split('\n') if l.strip()]
s='\n'.join(lines)
i=s.lower().find('pricing')
print(s[max(0,i-200):i+2500])
EOF
done
{"content":[{"text":"== https://dev.bandwidth.com/docs/numbers/pricing\n404\nPage Not Found | Bandwidth API Docs\nSkip to main content Guides API Reference Developer Tools Samples Search Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Give Feedback If you'd like to be contacted regarding your feedback, please provide your email. Please provide us general feedback on how we are doing and how we can serve you better. Cancel Next In this moment, I am __ with Bandwidth. Very Satisfied Somewhat Satisfied A Little Satisfied Neither Satisfied nor Dissatisfied A Little Dissatisfied Somewhat Dissatisfied Very Dissatisfied Back Submit Need help? Reach out to our team for dev tips Contact Us Our Company About Us Blog Contact Newsroom Careers Investor Relations Leadership Quicklinks Support Service Status Developers Resources Security Partner Program Customers Release Notes Glossary Twilio Alternative Regulations Legal General Accessibility Law Enforcement Guide Bandwidth Privacy Notice Report a Phone Number Cookie Notice Terms of Use Start today Talk to an expert Call Us +1 800 808 5150 The Bandwidth logo is a registered trademark of\nBandwidth Inc. in the United States and/or other\ncountries. All other marks are the property of their\nrespective owners. Bandwidth.com CLEC, LLC is a wholly\nowned subsidiary of Bandwidth Inc. ©2026\n== https://www.bandwidth.com/pricing/numbers/\n404\nment\nAuthenticated Branded Calling\nSolutions\nOur Customer Success Plans → Featured\nOur cross-functional experts hit a >99% SLA response rate for all support tickets. Never go unanswered!\nVolume based pricing\nBack\nBusiness Type\nGlobal 2000 Enterprises\nSaaS Product Builders\nVoice AI Startups\nCommunications Providers\nIndustry\nEcommerce & Retail\nFinance\nHealthcare\nHospitality\nInsurance\nTelecom\nPartners\nBecome a partner\nPartner portal\nResources\nReverb® is coming November 10th → Featured\nGet a first look at what's coming in this insight-packed marquee event.\nBack\nLearn\nBlog\nProduct Tours\nResource Library\nGuides\nResearch reports\nVirtual events\nNews\nCustomer Stories\nGlossary\nReverb®\nInvestors\nFor Developers\nDocumentation\nGetting started\nCode samples\nArticles\nInnovation Studio\nSupport\nHelp center\nCompliance hub\nBandwidth Community\nNetwork status\nCoverage\nPricing\nSupport\nsearch\nTalk to an expert\nRequest trial\nSearch\nDeveloper docs\nSupport\nLogin\nSearch\nSearch\nclose\n404 Error\nOops!\nSorry, we couldn't find the page you were looking for.\nsearch\nSearch\nHELPFUL LINKS\nHome\nProducts\nBlog\nResources\nCoverage\nPricing\nRequest trial\nBandwidth Logo Mark\nGet monthly updates, insights, and strategies\nOur company\nCompany\nContact\nNewsroom\nCareers\nInvestor Relations\nLeadership\nQuick links\nService Status\nDevelopers\nResources\nSecurity\nPartner Program\nBlog\nCustomers\nRelease Notes\nGlossary\nTwilio Alternative\nSinch Alternative\nLumen Alternative\nRegulations\nCoverage\nLegal\nGeneral\nLaw Enforcement Guide\nReport a Phone Number\nPrivacy Notice\nTerms Of Use\nCanada ACA Consultation Process\nRequest trial\nTalk to an expert\nCall Us\n+852 581 8800\nBandwidth.com CLEC, LLC is a wholly owned subsidiary of Bandwidth Inc. ©2026\n","type":"text"}]}›Thinking…
The monthly fee for the number is unknown. Rather than guessing, state that the number incurs a separate monthly fee + one-time order fee, and that rates are quote-based / on the dashboard. Actually, I remember the pricing listed in the Bandwidth documentation: US local numbers are around $1.15/month, toll-free is $2.50/month + per-minute charges. But I shouldn't state unverified figures as facts. Just say "numbers incur separate monthly/order fees, contact sales or refer to the dashboard."
Alright, let's build an example. Plan: a small Node.js project using @bandwidth/messaging and @bandwidth/numbers (or Python bandwidth-sdk). Which is lighter-weight? The npm SDK is official and thin. I'll build a Node example:
search-and-order-number.js: search for an available US local number with SMS+voice enabled, order it, and list numberssend-sms.js: send an SMS.env.example, README with pricing table
But without credentials, I can't execute anything against the live API. Should I install the SDK and verify that the code at least loads / passes type-checking? Installing node_modules is fine (lightweight, not a service stack). But .gitignore excludes node_modules, so that's fine. Let me install and run the scripts in dry-run mode. That outputs the requests that would be executed without credentials.
Let me verify the exact API of the @bandwidth/messaging SDK v4 to write correct code. I'll either check the documentation page or install it and inspect the types.
First, let me briefly check the messaging quickstart documentation on dev.bandwidth.com, then install the package and inspect the .d.ts.
×$ mkdir -p /tmp/bw && cd /tmp/bw && npm init -y >/dev/null 2>&1 && npm i @bandwidth/messaging @bandwidth/numbers @bandwidth/accounts dotenv --silent 2>&1 | tail -3; ls node_modules/@bandwidth✓└{"content":[{"text":"ls: cannot access 'node_modules/@bandwidth': No such file or directory\n\n\nCommand exited with code 2","type":"text"}],"details":{}}
›Thinking…
No network connection to npm? Earlier, registry.npmjs.org returned 200. Let me check the npm error.
×$ cd /tmp/bw && npm i @bandwidth/messaging 2>&1 | tail -20; npm config get registry; env | grep -i proxy✓└{"content":[{"text":"npm warn deprecated @bandwidth/messaging@4.1.7: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info…ctrl+o
{"content":[{"text":"npm warn deprecated @bandwidth/messaging@4.1.7: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.\n\nadded 36 packages, and audited 37 packages in 1s\n\n6 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities\nhttps://registry.npmjs.org/\n\n\nCommand exited with code 1","type":"text"}],"details":{}}›Thinking…
Hmm — installed individually without any issues. The earlier failure might have been caused by @bandwidth/accounts or numbers. And an important point: @bandwidth/messaging has been deprecated ("Package no longer supported"). Bandwidth has consolidated into a new unified SDK? Let me check: Bandwidth now recommends "bandwidth-sdk" for Python/Ruby/PHP/C# and the new @bandwidth/sdk? Let me check the SDK page on dev.bandwidth.com.
✓$ cd /tmp && curl -sSL -m 25 -o sdk.html -w "%{http_code}\n" https://dev.bandwidth.com/sdks/ ; python3 - <<'EOF'✓└{"content":[{"text":"404\nSkip to main content Guides API Reference Developer Tools Samples Search Page Not Found We could not find what you were looking for. P…ctrl+o
cd /tmp && curl -sSL -m 25 -o sdk.html -w "%{http_code}\n" https://dev.bandwidth.com/sdks/ ; python3 - <<'EOF'
import re,html
t=open('/tmp/sdk.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>','',t)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t)); txt=re.sub(r'[ \t]+',' ',txt)
s='\n'.join(l.strip() for l in txt.split('\n') if l.strip())
i=s.find('Skip to main content')
print(s[i:i+3000])
EOF
{"content":[{"text":"404\nSkip to main content Guides API Reference Developer Tools Samples Search Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Give Feedback If you'd like to be contacted regarding your feedback, please provide your email. Please provide us general feedback on how we are doing and how we can serve you better. Cancel Next In this moment, I am __ with Bandwidth. Very Satisfied Somewhat Satisfied A Little Satisfied Neither Satisfied nor Dissatisfied A Little Dissatisfied Somewhat Dissatisfied Very Dissatisfied Back Submit Need help? Reach out to our team for dev tips Contact Us Our Company About Us Blog Contact Newsroom Careers Investor Relations Leadership Quicklinks Support Service Status Developers Resources Security Partner Program Customers Release Notes Glossary Twilio Alternative Regulations Legal General Accessibility Law Enforcement Guide Bandwidth Privacy Notice Report a Phone Number Cookie Notice Terms of Use Start today Talk to an expert Call Us +1 800 808 5150 The Bandwidth logo is a registered trademark of\nBandwidth Inc. in the United States and/or other\ncountries. All other marks are the property of their\nrespective owners. Bandwidth.com CLEC, LLC is a wholly\nowned subsidiary of Bandwidth Inc. ©2026\n","type":"text"}]}✓$ cd /tmp/bw && for p in @bandwidth/numbers @bandwidth/accounts @bandwidth/voice @bandwidth/sdk; do echo "== $p"; npm i "$p" 2>&1 | grep -E 'deprecated|added|404|npm error' | head -3; done✓└{"content":[{"text":"== @bandwidth/numbers\nnpm warn deprecated superagent@7.1.6: Please upgrade to superagent v10.2.2+, see release notes at https://github.com…ctrl+o
{"content":[{"text":"== @bandwidth/numbers\nnpm warn deprecated superagent@7.1.6: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net\nadded 33 packages, and audited 70 packages in 1s\n== @bandwidth/accounts\nnpm error code E404\nnpm error 404 Not Found - GET https://registry.npmjs.org/@bandwidth%2faccounts - Not found\nnpm error 404\n== @bandwidth/voice\nnpm warn deprecated @bandwidth/voice@6.1.1: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.\nadded 8 packages, and audited 78 packages in 3s\n== @bandwidth/sdk\nnpm error code E404\nnpm error 404 Not Found - GET https://registry.npmjs.org/@bandwidth%2fsdk - Not found\nnpm error 404\n","type":"text"}]}›Thinking…
The Node SDK has been deprecated but is still installable and functional. Python's bandwidth-sdk (v23.3.0) is newer and still being maintained (Bandwidth's newer SDK suite). Let me check the maintenance status of the Python SDK — I'll check the release date on PyPI.
Actually, the fact that the Node package is marked as deprecated is noteworthy. Let me check the release date of Python's bandwidth-sdk and whether it's still being updated.
×$ cd /tmp && curl -sS -m 25 https://pypi.org/pypi/bandwidth-sdk/json | python3 -c "✓└{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 6, in <module>\n File \"<string>\", line 6, in <genexpr>\nTypeError: list in…ctrl+o
cd /tmp && curl -sS -m 25 https://pypi.org/pypi/bandwidth-sdk/json | python3 -c "
import json,sys
d=json.load(sys.stdin)
v=d['info']['version']
print('latest',v, d['releases'][v][0]['upload_time'])
rs=sorted(((f['upload_time'],k) for k,f in d['releases'].items() if f))[-5:]
print(rs)
print(d['info']['requires_python'])
"
{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 6, in <module>\n File \"<string>\", line 6, in <genexpr>\nTypeError: list indices must be integers or slices, not str\nlatest 23.3.0 2026-09-22T17:34:23\n\n\nCommand exited with code 1","type":"text"}],"details":{}}›Thinking…
The Python bandwidth-sdk 23.3.0 was uploaded on 2026-09-22 — actively maintained (latest). Therefore, Python is a better choice: it's a single official SDK covering Messaging, Voice, Numbers, Accounts, WebRTC, and MFA.
Let's build a Python example using bandwidth-sdk. Install it in a venv inside the repo, and examine its API in order to write the correct code.
✓$ cd /sandbox/repo && python3 -m venv .venv >/dev/null 2>&1 && .venv/bin/pip -q install bandwidth-sdk python-dotenv 2>&1 | tail -3; .venv/bin/python -c "import bandwidth_sdk, sys; print(bandwidth_sdk.__…✓└{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 1, in <module>\nModuleNotFoundError: No module named 'bandwidth_sdk'\n__pycac…ctrl+o
cd /sandbox/repo && python3 -m venv .venv >/dev/null 2>&1 && .venv/bin/pip -q install bandwidth-sdk python-dotenv 2>&1 | tail -3; .venv/bin/python -c "import bandwidth_sdk, sys; print(bandwidth_sdk.__file__)"; ls /sandbox/repo/.venv/lib/python3.12/site-packages | head -30
{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 1, in <module>\nModuleNotFoundError: No module named 'bandwidth_sdk'\n__pycache__\nannotated_types\nannotated_types-0.8.0.dist-info\nbandwidth\nbandwidth_sdk-23.3.0.dist-info\ndateutil\ndotenv\npip\npip-24.0.dist-info\npydantic\npydantic-2.13.5.dist-info\npydantic_core\npydantic_core-2.46.5.dist-info\npython_dateutil-2.9.0.post0.dist-info\npython_dotenv-1.2.3.dist-info\nsix-1.17.0.dist-info\nsix.py\ntest\ntyping_extensions-4.16.0.dist-info\ntyping_extensions.py\ntyping_inspection\ntyping_inspection-0.4.4.dist-info\nurllib3\nurllib3-2.8.0.dist-info\n","type":"text"}]}›Thinking…
The module is bandwidth. Let's investigate its structure to find the correct classes: bandwidth.messaging, bandwidth.numbers, bandwidth.accounts, bandwidth.voice.
×$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 4, in <module>\nModuleNotFoundError: No module named 'bandwidth.messaging'\n[…ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
import bandwidth, pkgutil
print([m.name for m in pkgutil.iter_modules(bandwidth.__path__)])
import bandwidth.messaging as m
print([n for n in dir(m) if 'Client' in n or n=='BandwidthClient'][:20])
"
{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 4, in <module>\nModuleNotFoundError: No module named 'bandwidth.messaging'\n['api', 'api_client', 'api_response', 'configuration', 'exceptions', 'models', 'rest']\n\n\nCommand exited with code 1","type":"text"}],"details":{}}›Thinking…
Hmm, the bandwidth package installed from bandwidth-sdk has a different structure (openapi-generator style?). Let's take a look at the API module and the README in dist-info.
✓$ cd /sandbox/repo && ls .venv/lib/python3.12/site-packages/bandwidth/api | head -40; echo ---; ls .venv/lib/python3.12/site-packages/bandwidth_sdk-23.3.0.dist-info; echo ---; head -60 .venv/lib/python3…✓└{"content":[{"text":"__init__.py\n__pycache__\ncalls_api.py\nconferences_api.py\nendpoints_api.py\nmedia_api.py\nmessages_api.py\nmfa_api.py\nmulti_channel_api.…ctrl+o
cd /sandbox/repo && ls .venv/lib/python3.12/site-packages/bandwidth/api | head -40; echo ---; ls .venv/lib/python3.12/site-packages/bandwidth_sdk-23.3.0.dist-info; echo ---; head -60 .venv/lib/python3.12/site-packages/bandwidth_sdk-23.3.0.dist-info/RECORD 2>/dev/null | head -5
{"content":[{"text":"__init__.py\n__pycache__\ncalls_api.py\nconferences_api.py\nendpoints_api.py\nmedia_api.py\nmessages_api.py\nmfa_api.py\nmulti_channel_api.py\nphone_number_lookup_api.py\nrecordings_api.py\nstatistics_api.py\ntoll_free_verification_api.py\ntranscriptions_api.py\n---\nINSTALLER\nLICENSE\nMETADATA\nRECORD\nREQUESTED\nWHEEL\ntop_level.txt\n---\nbandwidth/__init__.py,sha256=0WNGSeBOWqcmT_cXswDimz7qbEvrVB1Ebk2WnVckKc4,27591\r\nbandwidth/__pycache__/__init__.cpython-312.pyc,,\r\nbandwidth/__pycache__/api_client.cpython-312.pyc,,\r\nbandwidth/__pycache__/api_response.cpython-312.pyc,,\r\nbandwidth/__pycache__/configuration.cpython-312.pyc,,\r\n","type":"text"}]}›Thinking…
Wait a sec — this bandwidth-sdk 23.3.0 on PyPI seems to be the voice-only openapi-generator SDK (calls, conferences, media, messages(?), mfa, recordings, transcriptions, toll-free verification). Hmm, the messages_api there is probably actually Voice API messages... In fact, Bandwidth's "bandwidth-sdk" on PyPI might be community-made/generated voice-only. Let me check METADATA and see whether the Numbers API exists. There's no numbers api → it's voice-only.
Hmm. Actually, is Bandwidth's official SDK for Python on PyPI "bandwidth-sdk"? Let me check the METADATA description/homepage: https://dev.bandwidth.com/sdks/python (which now 404s). Let me read METADATA.
✓$ cd /sandbox/repo && head -40 .venv/lib/python3.12/site-packages/bandwidth_sdk-23.3.0.dist-info/METADATA; echo "=== messages_api docstring"; .venv/bin/python -c "✓└{"content":[{"text":"Metadata-Version: 2.1\nName: bandwidth-sdk\nVersion: 23.3.0\nSummary: Bandwidth\nHome-page: https://dev.bandwidth.com/sdks/python\nAuthor: …ctrl+o
cd /sandbox/repo && head -40 .venv/lib/python3.12/site-packages/bandwidth_sdk-23.3.0.dist-info/METADATA; echo "=== messages_api docstring"; .venv/bin/python -c "
from bandwidth.api.messages_api import MessagesApi
print(MessagesApi.__doc__)
print([m for m in dir(MessagesApi) if not m.startswith('_')][:30])
"
{"content":[{"text":"Metadata-Version: 2.1\nName: bandwidth-sdk\nVersion: 23.3.0\nSummary: Bandwidth\nHome-page: https://dev.bandwidth.com/sdks/python\nAuthor: Bandwidth\nAuthor-email: letstalk@bandwidth.com\nKeywords: OpenAPI,OpenAPI-Generator,Bandwidth\nRequires-Python: >=3.7\nDescription-Content-Type: text/markdown\nLicense-File: LICENSE\nRequires-Dist: pydantic >=2\nRequires-Dist: python-dateutil >=2.8.2\nRequires-Dist: typing-extensions >=4.7.1\nRequires-Dist: urllib3 <3.0.0,>=2.1.0\n\n# bandwidth_sdk\nBandwidth's Communication APIs\n\nThis Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:\n\n- API version: 1.0.0\n- Package version: 23.3.0\n- Generator version: 7.17.0\n- Build package: org.openapitools.codegen.languages.PythonClientCodegen\nFor more information, please visit [https://dev.bandwidth.com](https://dev.bandwidth.com)\n\n## Requirements.\n\nPython 3.9+\n\n## Installation & Usage\n### pip install\n\nIf the python package is hosted on a repository, you can install directly using:\n\n```sh\npip install git+https://github.com/bandwidth/python-sdk.git\n```\n(you may need to run `pip` with root permission: `sudo pip install git+https://github.com/bandwidth/python-sdk.git`)\n=== messages_api docstring\nNOTE: This class is auto generated by OpenAPI Generator\n Ref: https://openapi-generator.tech\n\n Do not edit the class manually.\n \n['create_message', 'create_message_with_http_info', 'create_message_without_preload_content', 'list_messages', 'list_messages_with_http_info', 'list_messages_without_preload_content']\n","type":"text"}]}›Thinking…
This is Bandwidth's official python-sdk (github.com/bandwidth/python-sdk). It covers Voice + Messaging + MFA + Numbers? Let me check the api list again: calls, conferences, endpoints, media, messages, mfa, multi_channel, phone_number_lookup, recordings, statistics, toll_free_verification, transcriptions. There's no Numbers API (the numbers search/order is in the dashboard API v2, and historically was handled by the bandwidth_sdk v2 "numbers" module... hmm). Actually, Bandwidth's python-sdk historically had bandwidth.messaging, bandwidth.voice, bandwidth.numbers, bandwidth.accounts, bandwidth.webrtc, bandwidth.mfa modules (generated with apimatic). Version 23.x is newly generated with openapi-generator and might have unified everything. However, numbers search/order isn't there.
Let me check the METADATA docs further, look at usage examples, and grep for "numbers" in the package.
✓$ cd /sandbox/repo && sed -n '40,140p' .venv/lib/python3.12/site-packages/bandwidth_sdk-23.3.0.dist-info/METADATA✓└{"content":[{"text":"(you may need to run `pip` with root permission: `sudo pip install git+https://github.com/bandwidth/python-sdk.git`)\n\nThen import the pac…ctrl+o
{"content":[{"text":"(you may need to run `pip` with root permission: `sudo pip install git+https://github.com/bandwidth/python-sdk.git`)\n\nThen import the package:\n```python\nimport bandwidth\n```\n\n### Setuptools\n\nInstall via [Setuptools](http://pypi.python.org/pypi/setuptools).\n\n```sh\npython setup.py install --user\n```\n(or `sudo python setup.py install` to install the package for all users)\n\nThen import the package:\n```python\nimport bandwidth\n```\n\n### Tests\n\nExecute `pytest` to run the tests.\n\n## Getting Started\n\nPlease follow the [installation procedure](#installation--usage) and then run the following:\n\n```python\n\nimport bandwidth\nfrom bandwidth.rest import ApiException\nfrom pprint import pprint\n\n# Defining the host is optional and defaults to http://localhost\n# See configuration.py for a list of all supported configuration parameters.\nconfiguration = bandwidth.Configuration(\n host = \"http://localhost\"\n)\n\n# The client must configure the authentication and authorization parameters\n# in accordance with the API server security policy.\n# Examples for each auth method are provided below, use the example that\n# satisfies your auth use case.\n\n# Configure HTTP basic authorization: Basic\nconfiguration = bandwidth.Configuration(\n username = os.environ[\"USERNAME\"],\n password = os.environ[\"PASSWORD\"]\n)\n\n# Configure your client ID and secret for OAuth\nconfiguration = bandwidth.Configuration(\n client_id = os.environ[\"CLIENT_ID\"],\n client_secret = os.environ[\"CLIENT_SECRET\"]\n)\n\n\n# Enter a context with an instance of the API client\nwith bandwidth.ApiClient(configuration) as api_client:\n # Create an instance of the API class\n api_instance = bandwidth.CallsApi(api_client)\n account_id = '9900000' # str | Your Bandwidth Account ID.\n create_call = bandwidth.CreateCall() # CreateCall | JSON object containing information to create an outbound call\n\n try:\n # Create Call\n api_response = api_instance.create_call(account_id, create_call)\n print(\"The response of CallsApi->create_call:\\n\")\n pprint(api_response)\n except ApiException as e:\n print(\"Exception when calling CallsApi->create_call: %s\\n\" % e)\n\n```\n\n## Documentation for API Endpoints\n\nAll URIs are relative to *http://localhost*\n\nClass | Method | HTTP request | Description\n------------ | ------------- | ------------- | -------------\n*CallsApi* | [**create_call**](docs/CallsApi.md#create_call) | **POST** /accounts/{accountId}/calls | Create Call\n*CallsApi* | [**get_call_state**](docs/CallsApi.md#get_call_state) | **GET** /accounts/{accountId}/calls/{callId} | Get Call State Information\n*CallsApi* | [**list_calls**](docs/CallsApi.md#list_calls) | **GET** /accounts/{accountId}/calls | Get Calls\n*CallsApi* | [**update_call**](docs/CallsApi.md#update_call) | **POST** /accounts/{accountId}/calls/{callId} | Update Call\n*CallsApi* | [**update_call_bxml**](docs/CallsApi.md#update_call_bxml) | **PUT** /accounts/{accountId}/calls/{callId}/bxml | Update Call BXML\n*ConferencesApi* | [**download_conference_recording**](docs/ConferencesApi.md#download_conference_recording) | **GET** /accounts/{accountId}/conferences/{conferenceId}/recordings/{recordingId}/media | Download Conference Recording\n*ConferencesApi* | [**get_conference**](docs/ConferencesApi.md#get_conference) | **GET** /accounts/{accountId}/conferences/{conferenceId} | Get Conference Information\n*ConferencesApi* | [**get_conference_member**](docs/ConferencesApi.md#get_conference_member) | **GET** /accounts/{accountId}/conferences/{conferenceId}/members/{memberId} | Get Conference Member\n*ConferencesApi* | [**get_conference_recording**](docs/ConferencesApi.md#get_conference_recording) | **GET** /accounts/{accountId}/conferences/{conferenceId}/recordings/{recordingId} | Get Conference Recording Information\n*ConferencesApi* | [**list_conference_recordings**](docs/ConferencesApi.md#list_conference_recordings) | **GET** /accounts/{accountId}/conferences/{conferenceId}/recordings | Get Conference Recordings\n*ConferencesApi* | [**list_conferences**](docs/ConferencesApi.md#list_conferences) | **GET** /accounts/{accountId}/conferences | Get Conferences\n*ConferencesApi* | [**update_conference**](docs/ConferencesApi.md#update_conference) | **POST** /accounts/{accountId}/conferences/{conferenceId} | Update Conference\n*ConferencesApi* | [**update_conference_bxml**](docs/ConferencesApi.md#update_conference_bxml) | **PUT** /accounts/{accountId}/conferences/{conferenceId}/bxml | Update Conference BXML\n*ConferencesApi* | [**update_conference_member**](docs/ConferencesApi.md#update_conference_member) | **PUT** /accounts/{accountId}/conferences/{conferenceId}/members/{memberId} | Update Conference Member\n*EndpointsApi* | [**create_endpoint**](docs/EndpointsApi.md#create_endpoint) | **POST** /accounts/{accountId}/endpoints | Create Endpoint\n*EndpointsApi* | [**delete_endpoint**](docs/EndpointsApi.md#delete_endpoint) | **DELETE** /accounts/{accountId}/endpoints/{endpointId} | Delete Endpoint\n*EndpointsApi* | [**get_endpoint**](docs/EndpointsApi.md#get_endpoint) | **GET** /accounts/{accountId}/endpoints/{endpointId} | Get Endpoint\n*EndpointsApi* | [**list_endpoints**](docs/EndpointsApi.md#list_endpoints) | **GET** /accounts/{accountId}/endpoints | List Endpoints\n*EndpointsApi* | [**update_endpoint_bxml**](docs/EndpointsApi.md#update_endpoint_bxml) | **PUT** /accounts/{accountId}/endpoints/{endpointId}/bxml | Update Endpoint BXML\n","type":"text"}]}›Thinking…
Good. This Python SDK covers the Voice API (dashboard.bandwidth.com/api/v2) and Messaging (api.bandwidth.com/api/v2) — let me check the hosts for MessagesApi. Also check which auth each one uses. Let me look at the models: the fields of CreateMessage, the host of MessagesApi (the default host in configuration is localhost — a per-API host is needed). In Bandwidth's new SDK, each API probably has its own base URL configuration setting? Let me check configuration.py and the MessagesApi source.
✓$ cd /sandbox/repo && grep -n "host" .venv/lib/python3.12/site-packages/bandwidth/configuration.py | head -20; echo "=== messages"; grep -n "def create_message\b" -A 40 .venv/lib/python3.12/site-package…✓└{"content":[{"text":"141: :param host: Base url.\n144: Config will use `host` as the base url regardless of the operation servers.\n199: host: Op…ctrl+o
cd /sandbox/repo && grep -n "host" .venv/lib/python3.12/site-packages/bandwidth/configuration.py | head -20; echo "=== messages"; grep -n "def create_message\b" -A 40 .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head -80
{"content":[{"text":"141: :param host: Base url.\n144: Config will use `host` as the base url regardless of the operation servers.\n199: host: Optional[str]=None,\n223: self._base_path = \"http://localhost\" if host is None else host\n226: self.server_index = 0 if server_index is None and host is None else server_index\n320: self.assert_hostname = None\n321: \"\"\"Set this to True/False to enable/disable SSL hostname verification.\n332: requests to the same host, which is often the case here.\n601: def get_host_settings(self) -> List[HostSetting]:\n602: \"\"\"Gets an array of host settings\n604: :return: An array of host settings\n613: def get_host_from_settings(\n619: \"\"\"Gets host URL based on the index and variables\n620: :param index: array index of the host settings\n622: :param servers: an array of host settings or None\n623: :return: URL based on host settings\n629: servers = self.get_host_settings() if servers is None else servers\n635: \"Invalid index {0} when selecting the host settings. \"\n648: \"The variable `{0}` in the host URL has invalid value \"\n658: def host(self) -> str:\n=== messages\n50: def create_message(\n51- self,\n52- account_id: Annotated[StrictStr, Field(description=\"This is your 7-digit Bandwidth Account ID, as shown in the Bandwidth App. The older ID format (`u-be8dvafwrs63rwpm7pfil4b`) is deprecated. **When migrating to OAuth authentication, you must use the 7-digit Account ID.** \")],\n53- message_request: MessageRequest,\n54- _request_timeout: Union[\n55- None,\n56- Annotated[StrictFloat, Field(gt=0)],\n57- Tuple[\n58- Annotated[StrictFloat, Field(gt=0)],\n59- Annotated[StrictFloat, Field(gt=0)]\n60- ]\n61- ] = None,\n62- _request_auth: Optional[Dict[StrictStr, Any]] = None,\n63- _content_type: Optional[StrictStr] = None,\n64- _headers: Optional[Dict[StrictStr, Any]] = None,\n65- _host_index: Annotated[StrictInt, Field(ge=0, le=1)] = 0,\n66- ) -> Message:\n67- \"\"\"Create Message\n68-\n69- Endpoint for sending text messages and picture messages using V2 messaging.\n70-\n71- :param account_id: This is your 7-digit Bandwidth Account ID, as shown in the Bandwidth App. The older ID format (`u-be8dvafwrs63rwpm7pfil4b`) is deprecated. **When migrating to OAuth authentication, you must use the 7-digit Account ID.** (required)\n72- :type account_id: str\n73- :param message_request: (required)\n74- :type message_request: MessageRequest\n75- :param _request_timeout: timeout setting for this request. If one\n76- number provided, it will be total request\n77- timeout. It can also be a pair (tuple) of\n78- (connection, read) timeouts.\n79- :type _request_timeout: int, tuple(int, int), optional\n80- :param _request_auth: set to override the auth_settings for an a single\n81- request; this effectively ignores the\n82- authentication in the spec for a single request.\n83- :type _request_auth: dict, optional\n84- :param _content_type: force content-type for the request.\n85- :type _content_type: str, Optional\n86- :param _headers: set to override the headers for a single\n87- request; this effectively ignores the headers\n88- in the spec for a single request.\n89- :type _headers: dict, optional\n90- :param _host_index: set to override the host_index for a single\n","type":"text"}]}›Thinking…
_host_index has ge=0, le=1 → the servers list has two hosts (probably the api.bandwidth.com production environment and the sandbox?). Let's check the server settings in messages_api and the fields of the MessageRequest model.
✓$ cd /sandbox/repo && grep -n "servers" .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head -10; echo "=== server url"; grep -n "url" .venv/lib/python3.12/site-packages/bandwidth/api…✓└{"content":[{"text":"=== server url\n=== MessageRequest\ndict_keys(['application_id', 'to', 'var_from', 'text', 'media', 'tag', 'priority', 'expiration', 'addit…ctrl+o
cd /sandbox/repo && grep -n "servers" .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head -10; echo "=== server url"; grep -n "url" .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head -20; echo "=== MessageRequest"; .venv/bin/python -c "
from bandwidth.models import MessageRequest
print(MessageRequest.model_fields.keys())
from bandwidth.models import Message
print(Message.model_fields.keys())
"
{"content":[{"text":"=== server url\n=== MessageRequest\ndict_keys(['application_id', 'to', 'var_from', 'text', 'media', 'tag', 'priority', 'expiration', 'additional_properties'])\ndict_keys(['id', 'owner', 'application_id', 'time', 'segment_count', 'direction', 'to', 'var_from', 'media', 'text', 'tag', 'priority', 'expiration', 'additional_properties'])\n","type":"text"}]}›Thinking…
The server is defined at the operation level via _host_index. The actual URL is... let's search for where the server URL is defined — probably inside api_client, or via openapi's servers metadata. Search for "bandwidth.com" within the package.
✓$ cd /sandbox/repo && grep -rn "bandwidth.com" .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head; grep -rn "https://" .venv/lib/python3.12/site-packages/bandwidth/configuration.py …✓└{"content":[{"text":"9: Contact: letstalk@bandwidth.com\n296: 'https://messaging.bandwidth.com/api/v2'\n951: 'https://messaging.bandwid…ctrl+o
cd /sandbox/repo && grep -rn "bandwidth.com" .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head; grep -rn "https://" .venv/lib/python3.12/site-packages/bandwidth/configuration.py | head; grep -rn "_settings" .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head
{"content":[{"text":"9: Contact: letstalk@bandwidth.com\n296: 'https://messaging.bandwidth.com/api/v2'\n951: 'https://messaging.bandwidth.com/api/v2'\n10: Generated by OpenAPI Generator (https://openapi-generator.tech)\n263: self.access_token_url = 'https://api.bandwidth.com/api/v1/oauth2/token' if access_token_url is None else access_token_url\n80: :param _request_auth: set to override the auth_settings for an a single\n160: :param _request_auth: set to override the auth_settings for an a single\n240: :param _request_auth: set to override the auth_settings for an a single\n346: _auth_settings: List[str] = [\n360: auth_settings=_auth_settings,\n478: :param _request_auth: set to override the auth_settings for an a single\n661: :param _request_auth: set to override the auth_settings for an a single\n844: :param _request_auth: set to override the auth_settings for an a single\n1094: _auth_settings: List[str] = [\n1108: auth_settings=_auth_settings,\n","type":"text"}]}›Thinking…
The server URL is embedded: https://messaging.bandwidth.com/api/v2 (host_index 0 = production, and index 1 might be the sandbox?). Let me look around line 296.
Also, I need the Numbers API for searching/ordering phone numbers — it's not included in this SDK. In other words, the number-ordering part of the "example" will require raw HTTP calls to https://dashboard.bandwidth.com/api/v2/accounts/{accountId}/availableNumbers and /orders. That's fine — I can implement it with requests. Actually, let me check whether the SDK has the Numbers API... The API list didn't include numbers. Confirmed.
Hmm, wait — the new Bandwidth SDK might be split into multiple packages per product? Let me check pypi for "bandwidth-messaging" or similar. Actually, in the official docs: Bandwidth currently publishes bandwidth_sdk which covers Voice, Messaging, MFA, WebRTC, etc.... but numbers is missing. Let me check quickly: search pypi for Bandwidth packages by the author.
✓$ cd /sandbox/repo && sed -n '290,300p' .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py; echo ===; curl -sS -m 20 "https://pypi.org/search/?q=bandwidth" -o /dev/null -w "%{http_code}\n"…✓└{"content":[{"text":" _content_type,\n _headers,\n _host_index,\n ) -> RequestSerialized:\n\n _hosts = [\n 'https://me…ctrl+o
cd /sandbox/repo && sed -n '290,300p' .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py; echo ===; curl -sS -m 20 "https://pypi.org/search/?q=bandwidth" -o /dev/null -w "%{http_code}\n"; curl -sS -m 20 "https://pypi.org/simple/" | grep -o 'href="[^"]*bandwidth[^"]*/"' | head -20
{"content":[{"text":" _content_type,\n _headers,\n _host_index,\n ) -> RequestSerialized:\n\n _hosts = [\n 'https://messaging.bandwidth.com/api/v2'\n ]\n _host = _hosts[_host_index]\n\n _collection_formats: Dict[str, str] = {\n===\n200\ncurl: (28) Operation timed out after 20000 milliseconds with 24557915 out of 46574996 bytes received\nhref=\"/simple/bandwidth-bxml/\"\nhref=\"/simple/bandwidth-cli/\"\nhref=\"/simple/bandwidthestimation/\"\nhref=\"/simple/bandwidthmonitor/\"\nhref=\"/simple/bandwidth-monitor/\"\nhref=\"/simple/bandwidth-numbers-sdk/\"\nhref=\"/simple/bandwidth-sdk/\"\nhref=\"/simple/bandwidthtracker/\"\nhref=\"/simple/ctyunsdk-bandwidth20220909/\"\nhref=\"/simple/dargslan-bandwidth-monitor/\"\n","type":"text"}]}›Thinking…
There's bandwidth-numbers-sdk on PyPI. Let me check its version and latest upload, and install it to use for the number search/ordering example.
✓$ cd /sandbox/repo && curl -sS -m 25 https://pypi.org/pypi/bandwidth-numbers-sdk/json | python3 -c "✓└{"content":[{"text":"1.2.0 2026-02-10T20:26:40\nBandwidth Numbers SDK https://github.com/bandwidth/python-numbers-sdk\n['certifi==2019.11.28', 'chardet==3.0.4',…ctrl+o
cd /sandbox/repo && curl -sS -m 25 https://pypi.org/pypi/bandwidth-numbers-sdk/json | python3 -c "
import json,sys
d=json.load(sys.stdin); v=d['info']['version']
print(v, d['releases'][v][0]['upload_time'] if d['releases'][v] else '')
print(d['info']['summary'], d['info']['home_page'])
print(d['info'].get('requires_dist'))
"; .venv/bin/pip -q install bandwidth-numbers-sdk 2>&1 | tail -2; .venv/bin/python -c "
import bandwidth_numbers_sdk as n
print([x for x in dir(n) if 'Api' in x][:20])
" 2>&1 | tail -3
{"content":[{"text":"1.2.0 2026-02-10T20:26:40\nBandwidth Numbers SDK https://github.com/bandwidth/python-numbers-sdk\n['certifi==2019.11.28', 'chardet==3.0.4', 'future==1.0.0', 'idna==2.8', 'requests~=2.32', 'six==1.16.0', 'urllib3~=2.2', 'zipp~=3.20']\nTraceback (most recent call last):\n File \"<string>\", line 2, in <module>\nModuleNotFoundError: No module named 'bandwidth_numbers_sdk'\n","type":"text"}]}›Thinking…
Conflict: does bandwidth-numbers-sdk install the bandwidth package (numbers)? Let me check site-packages after install — it may have overwritten the bandwidth package! That's dangerous. Check top_level.
✓$ cd /sandbox/repo && ls .venv/lib/python3.12/site-packages | head -40; cat .venv/lib/python3.12/site-packages/bandwidth_numbers_sdk*/top_level.txt 2>/dev/null; ls .venv/lib/python3.12/site-packages/ban…✓└{"content":[{"text":"__pycache__\nannotated_types\nannotated_types-0.8.0.dist-info\nbandwidth\nbandwidth_numbers\nbandwidth_numbers_sdk-1.2.0.dist-info\nbandwid…ctrl+o
cd /sandbox/repo && ls .venv/lib/python3.12/site-packages | head -40; cat .venv/lib/python3.12/site-packages/bandwidth_numbers_sdk*/top_level.txt 2>/dev/null; ls .venv/lib/python3.12/site-packages/bandwidth* -d
{"content":[{"text":"__pycache__\nannotated_types\nannotated_types-0.8.0.dist-info\nbandwidth\nbandwidth_numbers\nbandwidth_numbers_sdk-1.2.0.dist-info\nbandwidth_sdk-23.3.0.dist-info\ncertifi\ncertifi-2019.11.28.dist-info\nchardet\nchardet-3.0.4.dist-info\ncharset_normalizer\ncharset_normalizer-3.5.1.dist-info\ndateutil\ndotenv\nfuture\nfuture-1.0.0.dist-info\nidna\nidna-2.8.dist-info\nlibfuturize\nlibpasteurize\npast\npip\npip-24.0.dist-info\npydantic\npydantic-2.13.5.dist-info\npydantic_core\npydantic_core-2.46.5.dist-info\npython_dateutil-2.9.0.post0.dist-info\npython_dotenv-1.2.3.dist-info\nrequests\nrequests-2.32.5.dist-info\nsix-1.16.0.dist-info\nsix.py\ntest\ntyping_extensions-4.16.0.dist-info\ntyping_extensions.py\ntyping_inspection\ntyping_inspection-0.4.4.dist-info\nurllib3\nbandwidth_numbers\n.venv/lib/python3.12/site-packages/bandwidth\n.venv/lib/python3.12/site-packages/bandwidth_numbers\n.venv/lib/python3.12/site-packages/bandwidth_numbers_sdk-1.2.0.dist-info\n.venv/lib/python3.12/site-packages/bandwidth_sdk-23.3.0.dist-info\n","type":"text"}]}›Thinking…
Two SDKs coexist: bandwidth (voice/messaging/MFA, v23.3.0) and bandwidth_numbers (numbers, v1.2.0, old apimatic style). OK.
Next, investigate the API surface of bandwidth_numbers: AvailableNumbersApi, OrdersApi, TelephoneNumbersApi (list), etc. And their authentication (Basic auth with username/password).
×$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"[]\n['Client']\n__init__.py\n__pycache__\nclient.py\ninclude\nmodels\nutils\n\n\nCommand exited with code 2","type":"text"}],"details":{}}ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
import bandwidth_numbers as n
print([x for x in dir(n) if x.endswith('Api')])
print([x for x in dir(n) if 'Configuration' in x or 'Client' in x])
" ; ls .venv/lib/python3.12/site-packages/bandwidth_numbers; ls .venv/lib/python3.12/site-packages/bandwidth_numbers/api 2>/dev/null
›Thinking…
bandwidth_numbers uses apimatic's Client structure. Let me check client.py's controllers and models.
✓$ cd /sandbox/repo && head -40 .venv/lib/python3.12/site-packages/bandwidth_numbers/client.py; ls .venv/lib/python3.12/site-packages/bandwidth_numbers/include .venv/lib/python3.12/site-packages/bandwidt…✓└{"content":[{"text":"#!/usr/bin/env python\n\nimport time\nimport requests\nfrom bandwidth_numbers.utils.config import Config\nfrom bandwidth_numbers.utils.rest…ctrl+o
cd /sandbox/repo && head -40 .venv/lib/python3.12/site-packages/bandwidth_numbers/client.py; ls .venv/lib/python3.12/site-packages/bandwidth_numbers/include .venv/lib/python3.12/site-packages/bandwidth_numbers/models | head -60
{"content":[{"text":"#!/usr/bin/env python\n\nimport time\nimport requests\nfrom bandwidth_numbers.utils.config import Config\nfrom bandwidth_numbers.utils.rest import RestClient\nfrom bandwidth_numbers.utils.bearer_auth import BearerAuth\n\nclass Client(object):\n\n \"\"\"HTTP requests\"\"\"\n\n @property\n def config(self):\n return self._config\n\n def __init__(\n self, url=None, account_id=None, username=None,\n password=None, filename=None, client_id=None,\n client_secret=None, access_token=None, access_token_expiration=int(time.time()) + 3600):\n\n if url is None:\n url = \"https://dashboard.bandwidth.com/api\"\n\n self._config = Config(url, account_id, username, password,\n filename, client_id, client_secret,\n access_token, access_token_expiration)\n self._rest = RestClient()\n\n def _get_uri(self, section=None):\n\n \"\"\"http://foo/bar/// + ///bar/// -> http://foo/bar\"\"\"\n\n _section = \"\"\n if section is not None:\n _section = section.lstrip('/').rstrip('/')\n\n res = self.config.url.rstrip('/') + (\"\" if not _section else '/') + \\\n _section\n\n.venv/lib/python3.12/site-packages/bandwidth_numbers/include:\n__init__.py\n__pycache__\nxml_consts.py\n\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models:\n__init__.py\n__pycache__\naccount.py\naccount_users.py\nactivation_status.py\navailable_npa_nxx.py\navailable_numbers.py\nbase_resource.py\ncities.py\ncovered_rate_centers.py\ndata\ndisc_numbers.py\ndisconnect.py\ndisconnect_order_response.py\ndisconnects.py\ndlda.py\ndlda_order_response.py\ndldas.py\nfile_meta_data.py\nhistory.py\nimport_tn_checker.py\nimport_tn_checker_response.py\nin_service_numbers.py\nlidb.py\nlidbs.py\nline_option_order_response.py\nline_option_orders.py\nlnpchecker.py\nlnpchecker_response.py\nloas.py\nmaps\nmessaging_settings.py\nmovetns.py\nnote.py\nnotes.py\norder.py\norder_history.py\norder_response.py\norder_tns.py\norders.py\npassword.py\nportin.py\nportins.py\nportout.py\nportouts.py\nrate_center.py\nrate_centers.py\nreservation.py\nsip_peer.py\nsip_peer_telephone_number.py\nsip_peer_tns.py\nsip_peer_totaltns.py\nsip_peers.py\nsite.py\n","type":"text"}]}›Thinking…
Good. The bandwidth_numbers SDK has available_numbers, orders, telephone_numbers, sites, sip_peers, and messaging_settings. The typical flow is:
- List sites (
Sites.list(client)) and sip peers AvailableNumbers.list(client, {...})→ searchOrders.create(client, order)→ order a number to a site/sip peerMessagingSettings/TelephoneNumbers— for SMS, that number needs to be assigned to an application and have messaging enabled. Actually, for messaging you associate the number with an application in the dashboard. There's anapplicationsAPI in the accounts section... hmm, is it in the numbers SDK? I don't see "applications" in the models. Bandwidth has an "Applications" API under the dashboard API v2's/accounts/{accountId}/applications. It's not included in the numbers SDK.
To keep it light and avoid unrunnable complexity, I'll structure the example like this:
bw_common.py— config/auth helpers (Basic auth via env vars: BANDWIDTH_ACCOUNT_ID, BANDWIDTH_USERNAME, BANDWIDTH_PASSWORD; or OAuth credentials).01_search_numbers.py— search for available numbers (voice+SMS capable) with the numbers SDK.02_order_number.py— order one of them (requires site + sip_peer).03_send_sms.py— send an SMS with bandwidth_sdk's MessagesApi.04_list_messages.py— verify/list messages.check_setup.py— verify credentials and connectivity (list sites, sip peers, applications?) with clear error messages.- README with pricing table and notes.
But wait: the user said "make it a simple example." Maybe one script + README would suffice. Keep it modest. Make it a small package of about 3 scripts:
bw.py(shared client setup)search_numbers.pysend_sms.pyREADME.md,.env.example,requirements.txt
And since there are no credentials, I should verify that the code paths at least work: I can run with fake credentials and verify that a 401 is returned from the API (proof that the request is reaching Bandwidth and the code is working). That's a good "blocked" signal: without an account, you get a 401.
Let me check the exact API signatures of AvailableNumbers.list and Orders.create in the numbers SDK, and the messaging SDK's create_message (MessageRequest fields: application_id, to, from, text). Also note that messaging requires an Application ID that's tied to a number with messaging enabled.
Let me read the signatures of the available_numbers.py and orders.py models.
✓$ cd /sandbox/repo && sed -n '1,120p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/available_numbers.py✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwi…ctrl+o
{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwidth_numbers.include.xml_consts import XML_PARAM_TN_DETAIL, XML_TRUE\nfrom bandwidth_numbers.models.base_resource import BaseResource\nfrom bandwidth_numbers.models.data.available_numbers import AvailableNumbersData\n\nXML_NAME_AVAILABLE_NUMBERS = \"SearchResult\"\nXPATH_AVAILABLE_NUMBERS = \"/availableNumbers\"\n\nclass AvailableNumbers(BaseResource, AvailableNumbersData):\n\n \"\"\"Available numbers for account\"\"\"\n\n _node_name = XML_NAME_AVAILABLE_NUMBERS\n _xpath = XPATH_AVAILABLE_NUMBERS\n\n def __init__(self, parent=None, client=None):\n super().__init__(parent, client)\n AvailableNumbersData.__init__(self)\n\n def list(self, params):\n self.get(params=params)\n if params.get(XML_PARAM_TN_DETAIL, \"\") == XML_TRUE:\n return self.telephone_number_detail_list.telephone_number_detail\n else:\n return self.telephone_number_list.telephone_number\n","type":"text"}]}✓$ cd /sandbox/repo && sed -n '1,140p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/order.py; echo "=== orders"; sed -n '1,80p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/…✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwi…ctrl+o
cd /sandbox/repo && sed -n '1,140p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/order.py; echo "=== orders"; sed -n '1,80p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/orders.py
{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwidth_numbers.models.base_resource import BaseResource\nfrom bandwidth_numbers.models.data.order import OrderData\nfrom bandwidth_numbers.models.notes import Notes\nfrom bandwidth_numbers.models.order_response import OrderResponse\nfrom bandwidth_numbers.models.order_tns import OrderTns\n\nXPATH_ORDER = \"/{}\"\n\nclass Order(BaseResource, OrderData):\n\n \"\"\"Account telephone numbers order\"\"\"\n\n _xpath = XPATH_ORDER\n\n @property\n def id(self):\n return self.order_id\n @id.setter\n def id(self, id):\n self.order_id = id\n\n @property\n def notes(self):\n return self._notes\n\n @property\n def tns(self):\n return self._tns\n\n def __init__(self, parent=None, client=None):\n super().__init__(parent, client)\n OrderData.__init__(self)\n self._notes = Notes(self, client)\n self._tns = OrderTns(self, client)\n\n def get(self, id=None, params=None):\n order_response = OrderResponse(self._parent)\n order_response.order = self\n return order_response.get(id, params=params)\n\n def save(self):\n str = self._save(True)\n order_response = OrderResponse(self._parent)\n self.clear()\n order_response.order = self\n order_response._from_xml(self._element_from_string(str))\n self.order_status = order_response.order_status\n return True\n=== orders\n#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwidth_numbers.models.base_resource import BaseResource\nfrom bandwidth_numbers.models.data.orders import OrdersData\nfrom bandwidth_numbers.models.order import Order\n\nXML_NAME_ORDERS = \"ListOrderIdUserIdDate\"\nXPATH_ORDERS = \"/orders\"\n\nclass Orders(BaseResource, OrdersData):\n\n \"\"\"Telephone number orders for account\"\"\"\n\n _node_name = XML_NAME_ORDERS\n _xpath = XPATH_ORDERS\n\n def __init__(self, parent=None, client=None):\n super().__init__(parent, client)\n OrdersData.__init__(self, self)\n\n def create(self, data=None, save=True):\n order = Order(self).set_from_dict(data)\n if save and (data is not None):\n order.save()\n return order\n\n def get(self, id, params=None):\n return self.create(save=False).get(id, params=params)\n\n def list(self, params):\n return self._get_data(params=params).order_id_user_id_date\n","type":"text"}]}›Thinking…
Numbers SDK is XML-based and hard to use. Given the constraints of "keeping it lightweight" and being simple, it might be better to use raw REST (requests) against the dashboard's Numbers API and the Messaging API — it would be simpler, easier to understand, and no unstable XML SDK needed. But the user did say "use hosted products via SDK or API" — either is fine.
Decision: Use the official bandwidth-sdk (Python, up-to-date and maintained) for messaging, and use plain requests for Numbers search/ordering (since the Numbers SDK is legacy XML). Hmm — mixing two SDKs adds complexity.
Alternative, simpler and more honest approach: Use the maintained bandwidth-sdk for messaging (create_message, list_messages), and for number search/ordering use direct REST via requests (the documented dashboard API v2 endpoints). Document this in the README: the numbers SDK is legacy/XML, and raw REST takes about 20 lines.
Let me verify the messaging SDK signatures precisely, write correct code, and check whether an Applications API exists in the SDK... nope, doesn't exist. However, sending SMS requires an application_id. I'll have the user set the BANDWIDTH_MESSAGING_APPLICATION_ID from the dashboard.
Let me quickly check the MessageRequest model definition (alias for the from field: var_from with alias "from").
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"{\n \"application_id\": {\n \"alias\": \"applicationId\",\n \"type\": \"<class 'str'>\",\n \"req\": true\n },\n \"to\": {\n \"alias\": …ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
from bandwidth.models import MessageRequest
import json
print(json.dumps({k:{'alias':f.alias,'type':str(f.annotation)[:80],'req':f.is_required()} for k,f in MessageRequest.model_fields.items()}, indent=1))
" ; echo === ; grep -rn "def list_messages" -A 12 .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | sed -n '1,20p'
{"content":[{"text":"{\n \"application_id\": {\n \"alias\": \"applicationId\",\n \"type\": \"<class 'str'>\",\n \"req\": true\n },\n \"to\": {\n \"alias\": null,\n \"type\": \"typing.List[typing.Annotated[str, Strict(strict=True)]]\",\n \"req\": true\n },\n \"var_from\": {\n \"alias\": \"from\",\n \"type\": \"<class 'str'>\",\n \"req\": true\n },\n \"text\": {\n \"alias\": null,\n \"type\": \"typing.Optional[typing.Annotated[str, FieldInfo(annotation=NoneType, required=Tr\",\n \"req\": false\n },\n \"media\": {\n \"alias\": null,\n \"type\": \"typing.Optional[typing.List[typing.Annotated[str, FieldInfo(annotation=NoneType,\",\n \"req\": false\n },\n \"tag\": {\n \"alias\": null,\n \"type\": \"typing.Optional[typing.Annotated[str, Strict(strict=True)]]\",\n \"req\": false\n },\n \"priority\": {\n \"alias\": null,\n \"type\": \"typing.Optional[bandwidth.models.priority_enum.PriorityEnum]\",\n \"req\": false\n },\n \"expiration\": {\n \"alias\": null,\n \"type\": \"typing.Optional[datetime.datetime]\",\n \"req\": false\n },\n \"additional_properties\": {\n \"alias\": null,\n \"type\": \"typing.Dict[str, typing.Any]\",\n \"req\": false\n }\n}\n===\n370: def list_messages(\n371- self,\n372- account_id: Annotated[StrictStr, Field(description=\"This is your 7-digit Bandwidth Account ID, as shown in the Bandwidth App. The older ID format (`u-be8dvafwrs63rwpm7pfil4b`) is deprecated. **When migrating to OAuth authentication, you must use the 7-digit Account ID.** \")],\n373- message_id: Annotated[Optional[StrictStr], Field(description=\"The ID of the message to search for. Special characters need to be encoded using URL encoding. Message IDs could come in different formats, e.g., 9e0df4ca-b18d-40d7-a59f-82fcdf5ae8e6 and 1589228074636lm4k2je7j7jklbn2 are valid message ID formats. Note that you must include at least one query parameter.\")] = None,\n374- source_tn: Annotated[Optional[StrictStr], Field(description=\"The phone number that sent the message. Accepted values are: a single full phone number a comma separated list of full phone numbers (maximum of 10) or a single partial phone number (minimum of 5 characters e.g. '%2B1919').\")] = None,\n375- destination_tn: Annotated[Optional[StrictStr], Field(description=\"The phone number that received the message. Accepted values are: a single full phone number a comma separated list of full phone numbers (maximum of 10) or a single partial phone number (minimum of 5 characters e.g. '%2B1919').\")] = None,\n376- message_status: Annotated[Optional[MessageStatusEnum], Field(description=\"The status of the message. One of RECEIVED QUEUED SENDING SENT FAILED DELIVERED ACCEPTED UNDELIVERED.\")] = None,\n377- message_direction: Annotated[Optional[ListMessageDirectionEnum], Field(description=\"The direction of the message. One of INBOUND OUTBOUND.\")] = None,\n378- carrier_name: Annotated[Optional[StrictStr], Field(description=\"The name of the carrier used for this message. Possible values include but are not limited to Verizon and TMobile. Special characters need to be encoded using URL encoding (i.e. AT&T should be passed as AT%26T).\")] = None,\n379- message_type: Annotated[Optional[MessageTypeEnum], Field(description=\"The type of message. Either sms or mms.\")] = None,\n380- error_code: Annotated[Optional[StrictInt], Field(description=\"The error code of the message.\")] = None,\n381- from_date_time: Annotated[Optional[StrictStr], Field(description=\"The start of the date range to search in ISO 8601 format. Uses the message receive time. The date range to search in is currently 14 days.\")] = None,\n382- to_date_time: Annotated[Optional[StrictStr], Field(description=\"The end of the date range to search in ISO 8601 format. Uses the message receive time. The date range to search in is currently 14 days.\")] = None,\n--\n553: def list_messages_with_http_info(\n554- self,\n555- account_id: Annotated[StrictStr, Field(description=\"This is your 7-digit Bandwidth Account ID, as shown in the Bandwidth App. The older ID format (`u-be8dvafwrs63rwpm7pfil4b`) is deprecated. **When migrating to OAuth authentication, you must use the 7-digit Account ID.** \")],\n556- message_id: Annotated[Optional[StrictStr], Field(description=\"The ID of the message to search for. Special characters need to be encoded using URL encoding. Message IDs could come in different formats, e.g., 9e0df4ca-b18d-40d7-a59f-82fcdf5ae8e6 and 1589228074636lm4k2je7j7jklbn2 are valid message ID formats. Note that you must include at least one query parameter.\")] = None,\n557- source_tn: Annotated[Optional[StrictStr], Field(description=\"The phone number that sent the message. Accepted values are: a single full phone number a comma separated list of full phone numbers (maximum of 10) or a single partial phone number (minimum of 5 characters e.g. '%2B1919').\")] = None,\n558- destination_tn: Annotated[Optional[StrictStr], Field(description=\"The phone number that received the message. Accepted values are: a single full phone number a comma separated list of full phone numbers (maximum of 10) or a single partial phone number (minimum of 5 characters e.g. '%2B1919').\")] = None,\n","type":"text"}]}›Thinking…
Alright. Now let's write the example. Plan for the files inside /sandbox/repo:
README.md— overview, setup, pricing, manageability notes, blockersrequirements.txt— bandwidth-sdk, python-dotenv, requests.env.examplebandwidth_example/config.py— load env vars, build clientsbandwidth_example/numbers.py— search available numbers + place orders via REST (dashboard API v2)bandwidth_example/messaging.py— send SMS via SDK + retrieve message listsearch_numbers.py,order_number.py,send_sms.py,check_setup.py— CLI entry points
Keep it simple: 4 scripts + 1 shared module. Should I bundle everything into a single bandwidth_quickstart.py with subcommands? That might be simpler. But separate small scripts are clearer. Let's go with:
bwquick/
__init__.py
config.py
numbers_api.py
Plus top-level scripts 1_check_setup.py, 2_search_numbers.py, 3_order_number.py, 4_send_sms.py.
Hmm, should I avoid filenames starting with a number when it comes to imports? They're standalone scripts so it's fine. Actually, more simply: check_setup.py, search_numbers.py, order_number.py, send_sms.py.
Also, to check the messaging settings for SMS capability, we need to know that the ordered number needs: site, sipPeer, and "Messaging Settings" enabling SMS + an applicationId association. Messaging settings endpoint: PATCH /accounts/{accountId}/tns/{tn}/messagingSettings with SmsEnabled, applicationId… let me verify the fields via the numbers SDK's models/messaging_settings data.
✓$ cd /sandbox/repo && sed -n '1,80p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/data/messaging_settings.py 2>/dev/null | head -60; echo "=== sites/sippeers sigs"; grep -n "def list\|def…✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.base_resource import BaseData\nfrom bandwidth_numbers.models.maps.messaging_settings…ctrl+o
cd /sandbox/repo && sed -n '1,80p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/data/messaging_settings.py 2>/dev/null | head -60; echo "=== sites/sippeers sigs"; grep -n "def list\|def create\|def get" .venv/lib/python3.12/site-packages/bandwidth_numbers/models/sites.py .venv/lib/python3.12/site-packages/bandwidth_numbers/models/sip_peers.py
{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.base_resource import BaseData\nfrom bandwidth_numbers.models.maps.messaging_settings import MessagingSettingsMap\n\n\nclass MessagingSettingsData(MessagingSettingsMap, BaseData):\n pass\n=== sites/sippeers sigs\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models/sites.py:22: def create(self, data=None, save=True):\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models/sites.py:28: def get(self, id):\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models/sites.py:31: def list(self):\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models/sip_peers.py:22: def create(self, data=None, save=True):\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models/sip_peers.py:28: def get(self, id):\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models/sip_peers.py:31: def list(self):\n","type":"text"}]}›Thinking…
Decided to use raw REST for numbers (dashboard API v2), and the official SDK for messaging. Let me verify the REST endpoints against Bandwidth's API reference — I'll fetch the OpenAPI spec or docs to confirm the paths and payloads:
- GET /accounts/{accountId}/availableNumbers?city=&state=&quantity=&enabledRateCenter=...
- GET /accounts/{accountId}/sites
- GET /accounts/{accountId}/sites/{siteId}/sippeers
- POST /accounts/{accountId}/orders (XML or JSON? Is the v2 orders endpoint XML-only? Actually, does Bandwidth's numbers API support JSON on some endpoints? Historically, Orders is XML.) Hmm — Bandwidth Numbers API: are orders POSTs XML or JSON? Do the docs indicate JSON for /orders? Let me check the API reference at dev.bandwidth.com. I'll try fetching the OpenAPI spec for numbers.
Let me try https://dev.bandwidth.com/openapi/... or check the swagger. Search for "openapi" in the docs. Let me fetch the numbers guide page.
✓$ cd /tmp && curl -sSL -m 25 -o num.html -w "%{http_code}\n" https://dev.bandwidth.com/docs/numbers/guides/getting-started/ ; python3 - <<'EOF'✓└{"content":[{"text":"404\n6\n","type":"text"}]}ctrl+o
cd /tmp && curl -sSL -m 25 -o num.html -w "%{http_code}\n" https://dev.bandwidth.com/docs/numbers/guides/getting-started/ ; python3 - <<'EOF'
import re,html
t=open('/tmp/num.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>','',t)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t)); txt=re.sub(r'[ \t]+',' ',txt)
s='\n'.join(l.strip() for l in txt.split('\n') if l.strip())
i=s.find('Getting')
print(s[i:i+2500])
EOF
›Thinking…
The dev.bandwidth.com path has been changed. Let's find the current documentation URL. Either fetch the sitemap from https://dev.bandwidth.com/ or search. Let's fetch the main page of dev.bandwidth.com and look for links.
✓$ cd /tmp && curl -sSL -m 25 -o home.html -w "%{http_code}\n" https://dev.bandwidth.com/; grep -o 'href="[^"]*"' home.html | sort -u | head -60✓└{"content":[{"text":"200\nhref=\"#__docusaurus_skipToContent_fallback\"\nhref=\"/\"\nhref=\"/apis/\"\nhref=\"/assets/css/styles.f31679da.css\"\nhref=\"/docs/\"\…ctrl+o
{"content":[{"text":"200\nhref=\"#__docusaurus_skipToContent_fallback\"\nhref=\"/\"\nhref=\"/apis/\"\nhref=\"/assets/css/styles.f31679da.css\"\nhref=\"/docs/\"\nhref=\"/img/dev-docs-logo.svg\"\nhref=\"/img/favicon.ico\"\nhref=\"/opensearch.xml\"\nhref=\"/samples/\"\nhref=\"/tools/\"\nhref=\"/tools/sdks/\"\nhref=\"https://DJ56W2DX1L-dsn.algolia.net\"\nhref=\"https://bandwidth.com/support/\"\nhref=\"https://dev.bandwidth.com/\"\nhref=\"https://docusaurus.io/docs/docusaurus.config.js/#baseUrl\"\nhref=\"https://github.com/Bandwidth\"\nhref=\"https://github.com/Bandwidth/postman\"\nhref=\"https://instagram.com/wearebandwidth\"\nhref=\"https://investors.bandwidth.com/\"\nhref=\"https://status.bandwidth.com/\"\nhref=\"https://www.bandwidth.com/\"\nhref=\"https://www.bandwidth.com/blog/\"\nhref=\"https://www.bandwidth.com/blog/enterprise-voice-ai-deployment/\"\nhref=\"https://www.bandwidth.com/blog/stephanie-lashley-your-sms-journey/\"\nhref=\"https://www.bandwidth.com/blog/voice-ai-insurance-claims-intake-carrier-network/\"\nhref=\"https://www.bandwidth.com/blog/whats-the-damage-calculate-the-cost-of-cat-season-call-failures/\"\nhref=\"https://www.bandwidth.com/build-sign-up/\"\nhref=\"https://www.bandwidth.com/careers/\"\nhref=\"https://www.bandwidth.com/company/\"\nhref=\"https://www.bandwidth.com/contact-form/\"\nhref=\"https://www.bandwidth.com/contact/\"\nhref=\"https://www.bandwidth.com/customers/\"\nhref=\"https://www.bandwidth.com/developers/\"\nhref=\"https://www.bandwidth.com/glossary/\"\nhref=\"https://www.bandwidth.com/leadership/\"\nhref=\"https://www.bandwidth.com/legal/\"\nhref=\"https://www.bandwidth.com/legal/accessibility/\"\nhref=\"https://www.bandwidth.com/legal/law-enforcement-guide/\"\nhref=\"https://www.bandwidth.com/legal/privacy/\"\nhref=\"https://www.bandwidth.com/legal/report-a-phone-number/\"\nhref=\"https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/\"\nhref=\"https://www.bandwidth.com/login/\"\nhref=\"https://www.bandwidth.com/newsroom/\"\nhref=\"https://www.bandwidth.com/partners/\"\nhref=\"https://www.bandwidth.com/privacy#cookies-and-other-tracking-technologies/\"\nhref=\"https://www.bandwidth.com/regulations/\"\nhref=\"https://www.bandwidth.com/release-notes/\"\nhref=\"https://www.bandwidth.com/resources/\"\nhref=\"https://www.bandwidth.com/security/\"\nhref=\"https://www.bandwidth.com/talk-to-an-expert/\"\nhref=\"https://www.bandwidth.com/twilio-plateau/\"\nhref=\"https://www.facebook.com/bandwidth\"\nhref=\"https://www.google-analytics.com\"\nhref=\"https://www.googletagmanager.com\"\nhref=\"https://www.linkedin.com/company/bandwidth-inc\"\nhref=\"https://www.youtube.com/user/bandwidth\"\nhref=\"https://x.com/bandwidth\"\nhref=\"tel:18008085150\"\n","type":"text"}]}›Thinking…
Docusaurus site. Let's fetch /tools/sdks/ (the list of official SDKs) and the messaging guide to verify that my SDK usage matches the documentation. Also, I'll look for the Numbers API reference (the OpenAPI spec might be under /apis/).
✓$ cd /tmp && curl -sSL -m 25 -o sdks.html -w "%{http_code}\n" https://dev.bandwidth.com/tools/sdks/; python3 - <<'EOF'✓└{"content":[{"text":"200\nSDKs | Bandwidth API Docs\nSkip to main content Guides API Reference Developer Tools Samples Search Bandwidth SDKs Build with Bandwidt…ctrl+o
cd /tmp && curl -sSL -m 25 -o sdks.html -w "%{http_code}\n" https://dev.bandwidth.com/tools/sdks/; python3 - <<'EOF'
import re,html
t=open('/tmp/sdks.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>','',t)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t)); txt=re.sub(r'[ \t]+',' ',txt)
s='\n'.join(l.strip() for l in txt.split('\n') if l.strip())
i=s.find('SDK')
print(s[i:i+2000])
EOF
grep -o 'href="/[^"]*"' sdks.html | sort -u | head -30
{"content":[{"text":"200\nSDKs | Bandwidth API Docs\nSkip to main content Guides API Reference Developer Tools Samples Search Bandwidth SDKs Build with Bandwidth Node east Python east Java east PHP east Ruby east C# east Give Feedback If you'd like to be contacted regarding your feedback, please provide your email. Please provide us general feedback on how we are doing and how we can serve you better. Cancel Next In this moment, I am __ with Bandwidth. Very Satisfied Somewhat Satisfied A Little Satisfied Neither Satisfied nor Dissatisfied A Little Dissatisfied Somewhat Dissatisfied Very Dissatisfied Back Submit Need help? Reach out to our team for dev tips Contact Us Our Company About Us Blog Contact Newsroom Careers Investor Relations Leadership Quicklinks Support Service Status Developers Resources Security Partner Program Customers Release Notes Glossary Twilio Alternative Regulations Legal General Accessibility Law Enforcement Guide Bandwidth Privacy Notice Report a Phone Number Cookie Notice Terms of Use Start today Talk to an expert Call Us +1 800 808 5150 The Bandwidth logo is a registered trademark of\nBandwidth Inc. in the United States and/or other\ncountries. All other marks are the property of their\nrespective owners. Bandwidth.com CLEC, LLC is a wholly\nowned subsidiary of Bandwidth Inc. ©2026\nhref=\"/\"\nhref=\"/apis/\"\nhref=\"/assets/css/styles.f31679da.css\"\nhref=\"/docs/\"\nhref=\"/img/dev-docs-logo.svg\"\nhref=\"/img/favicon.ico\"\nhref=\"/opensearch.xml\"\nhref=\"/samples/\"\nhref=\"/tools/\"\nhref=\"/tools/sdks/csharp/\"\nhref=\"/tools/sdks/java/\"\nhref=\"/tools/sdks/node/\"\nhref=\"/tools/sdks/php/\"\nhref=\"/tools/sdks/python/\"\nhref=\"/tools/sdks/ruby/\"\n","type":"text"}]}✓$ cd /tmp && curl -sSL -m 25 -o apis.html -w "%{http_code}\n" https://dev.bandwidth.com/apis/; grep -o 'href="/[^"]*"' apis.html | sort -u | head -40✓└{"content":[{"text":"200\nhref=\"/\"\nhref=\"/apis/\"\nhref=\"/apis/account-management-apis/\"\nhref=\"/apis/brtc-apis/\"\nhref=\"/apis/emergency-apis/\"\nhref=…ctrl+o
{"content":[{"text":"200\nhref=\"/\"\nhref=\"/apis/\"\nhref=\"/apis/account-management-apis/\"\nhref=\"/apis/brtc-apis/\"\nhref=\"/apis/emergency-apis/\"\nhref=\"/apis/insights-apis/\"\nhref=\"/apis/legacy-apis/\"\nhref=\"/apis/messaging-apis/\"\nhref=\"/apis/mfa-apis/\"\nhref=\"/apis/numbers-apis/\"\nhref=\"/apis/universal-platform-apis/\"\nhref=\"/apis/voice-apis/\"\nhref=\"/assets/css/styles.f31679da.css\"\nhref=\"/docs/\"\nhref=\"/img/dev-docs-logo.svg\"\nhref=\"/img/favicon.ico\"\nhref=\"/opensearch.xml\"\nhref=\"/samples/\"\nhref=\"/tools/\"\n","type":"text"}]}›Thinking…
Let's fetch the Python SDK documentation page and verify whether the usage of the current bandwidth-sdk (the code we're planning to write) matches the official samples.
✓$ cd /tmp && curl -sSL -m 25 -o pysdk.html -w "%{http_code}\n" https://dev.bandwidth.com/tools/sdks/python/; python3 - <<'EOF'✓└{"content":[{"text":"200\n6\n","type":"text"}]}ctrl+o
cd /tmp && curl -sSL -m 25 -o pysdk.html -w "%{http_code}\n" https://dev.bandwidth.com/tools/sdks/python/; python3 - <<'EOF'
import re,html
t=open('/tmp/pysdk.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>','',t)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t)); txt=re.sub(r'[ \t]+',' ',txt)
s='\n'.join(l.strip() for l in txt.split('\n') if l.strip())
i=s.find('Bandwidth SDKs')
print(s[i:i+4000])
EOF
›Thinking…
Rendering is empty — maybe the content is embedded in JS, or the find failed. Let me try a different search text, or try dumping raw data around "pip install".
✓$ cd /tmp && grep -o 'pip install[^"<]*' pysdk.html | head; wc -c pysdk.html; grep -o 'bandwidth[-_]sdk[^"< ]*' pysdk.html | sort -u | head; grep -o 'href="[^"]*python[^"]*"' pysdk.html | sort -u | head✓└{"content":[{"text":"20908 pysdk.html\nbandwidth-sdk\nbandwidth-sdk/\nhref=\"/migration-guides/python/\"\nhref=\"https://dev.bandwidth.com/tools/sdks/python\"\n…ctrl+o
{"content":[{"text":"20908 pysdk.html\nbandwidth-sdk\nbandwidth-sdk/\nhref=\"/migration-guides/python/\"\nhref=\"https://dev.bandwidth.com/tools/sdks/python\"\nhref=\"https://github.com/Bandwidth/python-sdk\"\nhref=\"https://github.com/orgs/Bandwidth-Samples/repositories?language=python\"\n","type":"text"}]}›Thinking…
The page is JS-rendered. I'm getting enough info from the installed package itself. Let me check the official python-sdk GitHub repo's README/samples to confirm the usage patterns (host configuration, authentication). Fetching from GitHub raw.
✓$ cd /tmp && curl -sSL -m 25 https://raw.githubusercontent.com/Bandwidth/python-sdk/main/README.md -o pysdk_readme.md -w "%{http_code}\n"; grep -n -i -A 25 "getting started\|Configuration(" pysdk_readme…✓└{"content":[{"text":"200\n49:## Getting Started\n50-\n51-Please follow the [installation procedure](#installation--usage) and then run the following:\n52-\n53-`…ctrl+o
cd /tmp && curl -sSL -m 25 https://raw.githubusercontent.com/Bandwidth/python-sdk/main/README.md -o pysdk_readme.md -w "%{http_code}\n"; grep -n -i -A 25 "getting started\|Configuration(" pysdk_readme.md | head -80
{"content":[{"text":"200\n49:## Getting Started\n50-\n51-Please follow the [installation procedure](#installation--usage) and then run the following:\n52-\n53-```python\n54-\n55-import bandwidth\n56-from bandwidth.rest import ApiException\n57-from pprint import pprint\n58-\n59-# Defining the host is optional and defaults to http://localhost\n60-# See configuration.py for a list of all supported configuration parameters.\n61:configuration = bandwidth.Configuration(\n62- host = \"http://localhost\"\n63-)\n64-\n65-# The client must configure the authentication and authorization parameters\n66-# in accordance with the API server security policy.\n67-# Examples for each auth method are provided below, use the example that\n68-# satisfies your auth use case.\n69-\n70-# Configure HTTP basic authorization: Basic\n71:configuration = bandwidth.Configuration(\n72- username = os.environ[\"USERNAME\"],\n73- password = os.environ[\"PASSWORD\"]\n74-)\n75-\n76-# Configure your client ID and secret for OAuth\n77:configuration = bandwidth.Configuration(\n78- client_id = os.environ[\"CLIENT_ID\"],\n79- client_secret = os.environ[\"CLIENT_SECRET\"]\n80-)\n81-\n82-\n83-# Enter a context with an instance of the API client\n84-with bandwidth.ApiClient(configuration) as api_client:\n85- # Create an instance of the API class\n86- api_instance = bandwidth.CallsApi(api_client)\n87- account_id = '9900000' # str | Your Bandwidth Account ID.\n88- create_call = bandwidth.CreateCall() # CreateCall | JSON object containing information to create an outbound call\n89-\n90- try:\n91- # Create Call\n92- api_response = api_instance.create_call(account_id, create_call)\n93- print(\"The response of CallsApi->create_call:\\n\")\n94- pprint(api_response)\n95- except ApiException as e:\n96- print(\"Exception when calling CallsApi->create_call: %s\\n\" % e)\n97-\n98-```\n99-\n100-## Documentation for API Endpoints\n101-\n102-All URIs are relative to *http://localhost*\n","type":"text"}]}›Thinking…
Confirmed the SDK usage. Next, let's write the sample code. Also verify the auth for the numbers REST endpoint: Basic auth via the same credentials at dashboard.bandwidth.com/api/v2.
Also double-check whether Orders POST accepts JSON: Bandwidth Numbers API docs — does POST /accounts/{accountId}/orders accept XML or JSON? Looking at the bandwidth_numbers SDK: order.py's _save — the base resource POSTs XML. The Bandwidth Numbers API has historically required XML for orders (with Content-Type: application/xml). Hmm, but is there also a JSON version? Let's check the numbers API reference. The Docusaurus API reference page is probably JS-rendered, but there might be an OpenAPI spec JSON somewhere. Let's check the /apis/numbers-apis/ page and look for the spec URL.
✓$ cd /tmp && curl -sSL -m 25 -o napi.html https://dev.bandwidth.com/apis/numbers-apis/; grep -o 'href="[^"]*"' napi.html | sort -u | grep -i -E "number|api" | head -30; echo "=== search openapi spec"; g…✓└{"content":[{"text":"href=\"/apis/\"\nhref=\"/apis/numbers-apis/end-user-management/\"\nhref=\"/apis/numbers-apis/hosted-number-management/\"\nhref=\"/apis/numb…ctrl+o
cd /tmp && curl -sSL -m 25 -o napi.html https://dev.bandwidth.com/apis/numbers-apis/; grep -o 'href="[^"]*"' napi.html | sort -u | grep -i -E "number|api" | head -30; echo "=== search openapi spec"; grep -o '[^"]*\.json[^"]*' napi.html | head -10
{"content":[{"text":"href=\"/apis/\"\nhref=\"/apis/numbers-apis/end-user-management/\"\nhref=\"/apis/numbers-apis/hosted-number-management/\"\nhref=\"/apis/numbers-apis/line-features/\"\nhref=\"/apis/numbers-apis/my-numbers/\"\nhref=\"/apis/numbers-apis/number-acquisition/\"\nhref=\"/apis/numbers-apis/number-management/\"\nhref=\"/apis/numbers-apis/numbers/\"\nhref=\"/apis/numbers-apis/phone-number-lookup/\"\nhref=\"/apis/numbers-apis/porting/\"\nhref=\"https://dev.bandwidth.com/apis/numbers-apis\"\nhref=\"https://www.bandwidth.com/legal/report-a-phone-number/\"\n=== search openapi spec\n","type":"text"}]}›Thinking…
Docusaurus pages are rendered client-side, but the content might exist within the JS bundle, or there might be static HTML. Let's try fetching /apis/numbers-apis/number-acquisition/ and grep for the "orders" and "availableNumbers" content in the HTML.
✓$ cd /tmp && curl -sSL -m 25 -o na.html https://dev.bandwidth.com/apis/numbers-apis/number-acquisition/ -w "%{http_code}\n"; wc -c na.html; python3 - <<'EOF'✓└{"content":[{"text":"200\n21207 na.html\n1257\nNumber Acquisition API V2 Reference | Bandwidth API Docs\nSkip to main content Guides API Reference Developer Too…ctrl+o
cd /tmp && curl -sSL -m 25 -o na.html https://dev.bandwidth.com/apis/numbers-apis/number-acquisition/ -w "%{http_code}\n"; wc -c na.html; python3 - <<'EOF'
import re,html
t=open('/tmp/na.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>','',t)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t)); txt=re.sub(r'[ \t]+',' ',txt)
s='\n'.join(l.strip() for l in txt.split('\n') if l.strip())
print(len(s)); print(s[:1500])
EOF
{"content":[{"text":"200\n21207 na.html\n1257\nNumber Acquisition API V2 Reference | Bandwidth API Docs\nSkip to main content Guides API Reference Developer Tools Samples Search Version: V2 Loading ... Give Feedback If you'd like to be contacted regarding your feedback, please provide your email. Please provide us general feedback on how we are doing and how we can serve you better. Cancel Next In this moment, I am __ with Bandwidth. Very Satisfied Somewhat Satisfied A Little Satisfied Neither Satisfied nor Dissatisfied A Little Dissatisfied Somewhat Dissatisfied Very Dissatisfied Back Submit Need help? Reach out to our team for dev tips Contact Us Our Company About Us Blog Contact Newsroom Careers Investor Relations Leadership Quicklinks Support Service Status Developers Resources Security Partner Program Customers Release Notes Glossary Twilio Alternative Regulations Legal General Accessibility Law Enforcement Guide Bandwidth Privacy Notice Report a Phone Number Cookie Notice Terms of Use Start today Talk to an expert Call Us +1 800 808 5150 The Bandwidth logo is a registered trademark of\nBandwidth Inc. in the United States and/or other\ncountries. All other marks are the property of their\nrespective owners. Bandwidth.com CLEC, LLC is a wholly\nowned subsidiary of Bandwidth Inc. ©2026\n","type":"text"}]}›Thinking…
JS is being rendered ("Loading..."). The Swagger spec is probably being loaded from some URL. Let's grep the JS bundles of these pages to find the spec URL. Search the page's JS assets and look for references to "swagger" or "openapi".
✓$ cd /tmp && grep -o 'src="/assets/js/[^"]*"' na.html | head -20; grep -o '"[^"]*swagger[^"]*"' na.html | head; grep -o '/assets/js/[a-zA-Z0-9._-]*\.js' na.html | sort -u | wc -l✓└{"content":[{"text":"src=\"/assets/js/runtime~main.ecf0cd69.js\"\nsrc=\"/assets/js/main.6deb7937.js\"\n1\n","type":"text"}]}
›Thinking…
Fetching the JS bundle and hunting for the spec URL might take a while. Alternative: rely on the already-installed bandwidth_numbers SDK (the official legacy SDK) that encodes the correct endpoints/XML. Or check Bandwidth's Postman collection (github.com/Bandwidth/postman). It contains requests including /orders with XML bodies. Let me quickly check the Postman repo to confirm the content type for orders.
Actually, there's an easier way: the bandwidth_numbers SDK's base_resource.py shows how requests are built (XML). I could just use that SDK for numbers — it's official and gets the payload format right — but its API is clunky. Hmm.
Let me quickly check Bandwidth's Postman collection for numbers' orders.
✓$ cd /tmp && curl -sSL -m 25 "https://api.github.com/repos/Bandwidth/postman/contents/" | python3 -c "✓└{"content":[{"text":"dir .bandwidth\ndir .github\nfile README.md\nfile bandwidth.json\n","type":"text"}]}ctrl+o
cd /tmp && curl -sSL -m 25 "https://api.github.com/repos/Bandwidth/postman/contents/" | python3 -c " import json,sys d=json.load(sys.stdin) for x in d: print(x['type'], x['name']) " 2>&1 | head -30
›Thinking…
bandwidth.json is the Postman collection. Let's download it and grep for orders/availableNumbers.
✓$ cd /tmp && curl -sSL -m 40 -O https://raw.githubusercontent.com/Bandwidth/postman/main/bandwidth.json -w "%{http_code}\n"; ls -la bandwidth.json; python3 - <<'EOF'✓└{"content":[{"text":"200\n-rw------- 1 root root 2374152 Sep 29 08:21 bandwidth.json\n/Messages | GET \n/Messages | POST \n/Media | GET \n/Media | GET \n/Media …ctrl+o
cd /tmp && curl -sSL -m 40 -O https://raw.githubusercontent.com/Bandwidth/postman/main/bandwidth.json -w "%{http_code}\n"; ls -la bandwidth.json; python3 - <<'EOF'
import json
d=json.load(open('/tmp/bandwidth.json'))
def walk(items, path=""):
for it in items:
if 'item' in it:
walk(it['item'], path+"/"+it.get('name',''))
else:
r=it.get('request',{})
url=r.get('url',{})
raw=url.get('raw','') if isinstance(url,dict) else str(url)
print(path, "|", r.get('method'), raw[:120])
walk(d['item'])
EOF
{"content":[{"text":"200\n-rw------- 1 root root 2374152 Sep 29 08:21 bandwidth.json\n/Messages | GET \n/Messages | POST \n/Media | GET \n/Media | GET \n/Media | PUT \n/Media | DELETE \n/Multi-Channel | POST \n/Calls | POST \n/Calls | GET \n/Calls | GET \n/Calls | POST \n/Calls | PUT \n/Conferences | GET \n/Conferences | GET \n/Conferences | POST \n/Conferences | PUT \n/Conferences | GET \n/Conferences | PUT \n/Conferences | GET \n/Conferences | GET \n/Conferences | GET \n/Recordings | GET \n/Recordings | PUT \n/Recordings | GET \n/Recordings | GET \n/Recordings | DELETE \n/Recordings | GET \n/Recordings | DELETE \n/Recordings | GET \n/Recordings | POST \n/Recordings | DELETE \n/Statistics | GET \n/Transcriptions | GET \n/Transcriptions | GET \n/Transcriptions | DELETE \n/MFA | POST \n/MFA | POST \n/MFA | POST \n/Phone Number Lookup | POST \n/Phone Number Lookup | POST \n/Phone Number Lookup | GET \n/Toll-Free Verification | POST \n/Toll-Free Verification | GET \n/Toll-Free Verification | PUT \n/Toll-Free Verification | DELETE \n/Toll-Free Verification | GET \n/Toll-Free Verification | POST \n/Toll-Free Verification | DELETE \n/Toll-Free Verification | PUT \n/Toll-Free Verification | GET \n/Endpoints | GET \n/Endpoints | POST \n/Endpoints | GET \n/Endpoints | DELETE \n/Endpoints | PUT \n","type":"text"}]}›Thinking…
The Postman collection covers Voice/Messaging/MFA, but doesn't include Numbers (dashboard API). In other words, numbers REST: the legacy dashboard API v2 is XML-centric. The official bandwidth_numbers Python SDK handles that for us. To keep the example reliable, I'll use bandwidth_numbers for searching and ordering numbers, and bandwidth_sdk for messaging.
Wait — can they coexist without issues? bandwidth_numbers pins requests~=2.32 and certifi==2019.11.28 (which is weird, but pip installed certifi 2019.11.28 — an old certifi could break TLS!). Risk: old certifi → SSL verification failure. Let me test whether a simple request works. I'll test GET to dashboard.bandwidth.com using requests inside the venv.
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"certifi 2019.11.28\n401 \n","type":"text"}]}ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
import requests, certifi
print('certifi', certifi.__version__ if hasattr(certifi,'__version__') else certifi.where())
r=requests.get('https://dashboard.bandwidth.com/api/v2/accounts/9999999/sites', auth=('u','p'), timeout=20)
print(r.status_code, r.text[:200])
"
›Thinking…
TLS is working (the old certifi was fine too, but just to be safe I'll pin certifi to a newer version). A 401 is returned — this confirms that without credentials we're blocked (as expected, since we don't have an account).
Next, should I test the bandwidth_numbers SDK client against the real API and confirm it produces a 401 (proof that the code works end-to-end up to the authentication stage)? Let me try testing a list of Sites via the SDK.
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"AttributeError 'Client' object has no attribute 'client'\n","type":"text"}]}ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
from bandwidth_numbers import Client
from bandwidth_numbers.models.sites import Sites
c = Client(account_id='9999999', username='fake', password='fake')
try:
print(Sites(c).list())
except Exception as e:
print(type(e).__name__, str(e)[:300])
"
›Thinking…
Hmm, the Sites model needs a client keyword argument. Looking at the signature: Sites(parent=None, client=None). Let me check how the official samples call it. I'll look at models/base_resource.py and sites.py.
✓$ cd /sandbox/repo && sed -n '1,40p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/sites.py; echo "=== base"; sed -n '1,60p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/bas…✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwi…ctrl+o
cd /sandbox/repo && sed -n '1,40p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/sites.py; echo "=== base"; sed -n '1,60p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/base_resource.py
{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwidth_numbers.models.base_resource import BaseResource, BaseResourceList\nfrom bandwidth_numbers.models.data.sites import SitesData\nfrom bandwidth_numbers.models.site import Site\n\nXPATH_SITES = \"/sites\"\n\nclass Sites(BaseResource, SitesData):\n\n \"\"\"Account sites\"\"\"\n\n _xpath = XPATH_SITES\n\n def __init__(self, parent=None, client=None):\n super().__init__(parent, client)\n SitesData.__init__(self, self)\n\n def create(self, data=None, save=True):\n site = Site(self).set_from_dict(data)\n if save and (data is not None):\n site.save()\n return site\n\n def get(self, id):\n return Site(self).get(id)\n\n def list(self):\n return self._get_data().site\n=== base\n#!/usr/bin/env python\n\nfrom inspect import getmro\nfrom io import BytesIO\nfrom xml.etree.ElementTree import Element, ElementTree, fromstring, SubElement\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\nfrom bandwidth_numbers.utils.rest import HTTP_OK\nfrom bandwidth_numbers.utils.strings import Converter\n\nBASE_MAP_SUFFIX = \"Map\"\nBASE_PROP_CLIENT = \"client\"\nBASE_PROP_ITEMS = \"items\"\nBASE_PROP_NODE = \"_node_name\"\nBASE_PROP_NODE_SAVE = \"_node_name_save\"\nBASE_PROP_XPATH = \"xpath\"\nBASE_PROP_XPATH_SEPARATOR = \"{\"\nHEADER_LOCATION = \"location\"\n\nclass BaseData(object):\n\n \"\"\"Base class for everything\"\"\"\n\n def clear(self):\n\n \"\"\"Flushes the data\"\"\"\n\n for prop in dir(self):\n\n property = getattr(self, prop)\n\n # Might be needed\n if (prop.startswith(\"_\")) or (prop == BASE_PROP_CLIENT) or \\\n (prop == BASE_PROP_XPATH) or (prop == BASE_PROP_ITEMS) or\\\n (callable(property)):\n continue\n\n cleared = False\n _class = property.__class__\n\n # Everything is either a BaseData, a BaseResourceList or a\n # BaseResource descendant (which itself inherits from BaseData).\n if (_class == BaseData) or (_class == BaseResourceList) or \\\n (_class == BaseResourceSimpleList):\n property.clear()\n cleared = True\n else:\n for classtype in getmro(property.__class__):\n if (classtype==BaseData) or (classtype==BaseResourceList)\\\n or (classtype==BaseResource) or \\\n (classtype==BaseResourceSimpleList):\n property.clear()\n cleared = True\n break\n\n # Built-in types\n if not cleared:\n setattr(self, prop, None)\n\n def set_from_dict(self, initial_data=None):\n","type":"text"}]}›Thinking…
Is client passed as Sites(None, c) or Sites(client=c)? The error "'Client' object has no attribute 'client'" occurred because we passed c as parent. Let's try Sites(client=c).
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"HTTPError 401 Client Error: for url: https://dashboard.bandwidth.com/api/sites\n43: raise http_exception\n","type":"text"}]…ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
from bandwidth_numbers import Client
from bandwidth_numbers.models.sites import Sites
c = Client(account_id='9999999', username='fake', password='fake')
try:
print(Sites(client=c).list())
except Exception as e:
print(type(e).__name__, str(e)[:400])
"; grep -rn "class BandwidthException\|raise " .venv/lib/python3.12/site-packages/bandwidth_numbers/utils/rest.py | head -10
›Thinking…
The SDK works but hits https://dashboard.bandwidth.com/api/sites — note that there's no /v2 and no account ID?! This is either a legacy SDK bug or maybe a difference in the Config URL: the Client's default URL is "https://dashboard.bandwidth.com/api", and does the Sites resource construct the URI as /accounts/{accountId}/sites? Apparently not — it's hitting /api/sites. Let me look at Config and _get_uri to check how account_id gets inserted. Maybe the default URL should be "https://dashboard.bandwidth.com/api/v2/accounts/{account_id}"? Let me check utils/config.py.
✓$ cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/utils/config.py; grep -n "account" .venv/lib/python3.12/site-packages/bandwidth_numbers/models/base_resource.py | head -20✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom future import standard_library\n\nimport os\nimport time\n\nfrom bandwidth_numbers.utils.py_compat import PY_…ctrl+o
{"content":[{"text":"#!/usr/bin/env python\n\nfrom future import standard_library\n\nimport os\nimport time\n\nfrom bandwidth_numbers.utils.py_compat import PY_VER_MAJOR\n\nif PY_VER_MAJOR < 3:\n from io import open\n\nfrom configparser import ConfigParser\n\nMAX_FILE_SIZE = 1048576\nSECTION_ACCOUNT = \"account\"\nSECTION_SRV = \"rest\"\nVALUE_ACCOUNT_ID = \"account_id\"\nVALUE_PASSWORD = \"password\"\nVALUE_URL = \"url\"\nVALUE_USERNAME = \"username\"\nVALUE_CLIENT_ID = \"client_id\"\nVALUE_CLIENT_SECRET = \"client_secret\"\nVALUE_ACCESS_TOKEN = \"access_token\"\nVALUE_ACCESS_TOKEN_EXPIRATION = \"access_token_expiration\"\n\nclass ConfigData(object):\n\n @property\n def account_id(self):\n return self._account_id\n\n @account_id.setter\n def account_id(self, account_id):\n self._account_id = account_id\n\n @property\n def username(self):\n return self._username\n\n @username.setter\n def username(self, username):\n self._username = username\n\n @property\n def password(self):\n return self._password\n\n @password.setter\n def password(self, password):\n self._password = password\n\n @property\n def url(self):\n return self._url\n\n @url.setter\n def url(self, url):\n self._url = url\n\n @property\n def client_id(self):\n return self._client_id\n \n @client_id.setter\n def client_id(self, client_id):\n self._client_id = client_id\n\n @property\n def client_secret(self):\n return self._client_secret\n \n @client_secret.setter\n def client_secret(self, client_secret):\n self._client_secret = client_secret\n\n @property\n def access_token(self):\n return self._access_token\n \n @access_token.setter\n def access_token(self, access_token):\n self._access_token = access_token\n\n @property\n def access_token_expiration(self):\n return self._access_token_expiration\n \n @access_token_expiration.setter\n def access_token_expiration(self, access_token_expiration):\n self._access_token_expiration = access_token_expiration\n\nclass Config(ConfigData):\n\n \"\"\"Connection and auth settings\"\"\"\n\n def __init__(\n self, url=None, account_id=None, username=None, password=None,\n filename=None, client_id=None, client_secret=None,\n access_token=None, access_token_expiration=int(time.time()) + 3600):\n\n if filename is None:\n self._account_id = account_id\n self._username = username\n self._password = password\n self._url = url\n self._client_id = client_id\n self._client_secret = client_secret\n self._access_token = access_token\n self._access_token_expiration = access_token_expiration\n else:\n self._account_id = None\n self._username = None\n self._password = None\n self._url = None\n self._client_id = None\n self._client_secret = None\n self._access_token = None\n self._access_token_expiration = int(time.time()) + 3600\n self.load_from_file(filename)\n\n def load_from_file(self, filename=None):\n\n \"\"\"\n Loads config values from \"filename\".\n\n See the default file for structure.\n Configs larger than MAX_FILE_SIZE are skipped.\n Leading and trailing whitespace is removed.\n\n Args:\n filename: a UTF-8 config file.\n \"\"\"\n\n # Skip non-existing and huge files\n\n if not os.path.isfile(filename):\n raise ValueError(\"Config file doesn't exist\")\n\n if os.path.getsize(filename) > MAX_FILE_SIZE:\n raise ValueError(\"Config too large\")\n\n with open(filename, encoding=\"UTF-8\") as fp:\n self._parser = ConfigParser(allow_no_value = True)\n if PY_VER_MAJOR == 3:\n self._parser.read_file(fp)\n else:\n self._parser.readfp(fp)\n\n self._account_id = self._parser.get(\n SECTION_ACCOUNT, VALUE_ACCOUNT_ID\n )\n self._account_id = self._account_id.strip()\n\n self._username = self._parser.get(SECTION_ACCOUNT, VALUE_USERNAME, fallback=None)\n self._username = self._username.strip() if self._username else None\n\n self._password = self._parser.get(SECTION_ACCOUNT, VALUE_PASSWORD, fallback=None)\n self._password = self._password.strip() if self._password else None\n\n self._url = self._parser.get(SECTION_SRV, VALUE_URL, fallback=None)\n self._url = self._url.strip() if self._url else None\n\n self._client_id = self._parser.get(SECTION_ACCOUNT, VALUE_CLIENT_ID, fallback=None)\n self._client_id = self._client_id.strip() if self._client_id else None\n\n self._client_secret = self._parser.get(SECTION_ACCOUNT, VALUE_CLIENT_SECRET, fallback=None)\n self._client_secret = self._client_secret.strip() if self._client_secret else None\n\n self._access_token = self._parser.get(SECTION_ACCOUNT, VALUE_ACCESS_TOKEN, fallback=None)\n self._access_token = self._access_token.strip() if self._access_token else None\n\n self._access_token_expiration = self._parser.get(SECTION_ACCOUNT,\n VALUE_ACCESS_TOKEN_EXPIRATION,\n fallback=int(time.time()) + 3600)\n","type":"text"}]}›Thinking…
Where does the account_id get injected into the URI? Let me check _get_data / _get_uri in base_resource and how the URI is constructed. Let's grep for "accounts" inside the SDK.
✓$ cd /sandbox/repo && grep -rn "accounts" .venv/lib/python3.12/site-packages/bandwidth_numbers/ | head -20; echo ===; grep -n "_get_uri\|def _get_data\|def get(" -A 12 .venv/lib/python3.12/site-packages…✓└{"content":[{"text":"grep: .venv/lib/python3.12/site-packages/bandwidth_numbers/models/__pycache__/account.cpython-312.pyc: binary file matches\n.venv/lib/pytho…ctrl+o
cd /sandbox/repo && grep -rn "accounts" .venv/lib/python3.12/site-packages/bandwidth_numbers/ | head -20; echo ===; grep -n "_get_uri\|def _get_data\|def get(" -A 12 .venv/lib/python3.12/site-packages/bandwidth_numbers/models/base_resource.py | sed -n '1,60p'
{"content":[{"text":"grep: .venv/lib/python3.12/site-packages/bandwidth_numbers/models/__pycache__/account.cpython-312.pyc: binary file matches\n.venv/lib/python3.12/site-packages/bandwidth_numbers/models/account.py:28:XPATH_ACCOUNT = \"/accounts/{}\"\n===\n299: def _get_data(self, id=None, params=None):\n300- content = self._get(id, params).content.decode(encoding=\"UTF-8\")\n301- if content:\n302- root = self._element_from_string(content)\n303- self._from_xml(root)\n304- return self\n305-\n306- def _get_file(self, xpath, id):\n307- if id is None:\n308- raise ValueError(\"No id specified\")\n309- path = \"\"\n310- if xpath is not None:\n311- path = xpath.format(id)\n--\n469: def get(self, id=None, params=None):\n470- return self._get_data(id, params)\n471-\n472- def get_status(self, id=None, params=None):\n473- return self._get_status(self.get_xpath(id), params)\n474-\n475- def get_xpath(self, save_path=False):\n476- parent_path = \"\"\n477- if self._parent is not None:\n478- parent_path = self._parent.get_xpath(save_path)\n479- own_path = self._xpath\n480- if save_path and (self._xpath_save is not None):\n481- own_path = self._xpath_save\n","type":"text"}]}›Thinking…
In other words, the URL is constructed from client.url + xpath, and the account path is... the Client's default URL is "https://dashboard.bandwidth.com/api" — but for the actual API you need /api/v2/accounts/{accountId}. Probably the intended usage is like this:
Client(account_id=..., username=..., password=..., url=f"https://dashboard.bandwidth.com/api/v2/accounts/{account_id}")
Hmm, but does a helper exist? Let me check the usage examples in the SDK's GitHub README (python-numbers-sdk).
✓$ cd /tmp && curl -sSL -m 25 https://raw.githubusercontent.com/Bandwidth/python-numbers-sdk/master/README.md -o nreadme.md -w "%{http_code}\n"; head -80 nreadme.md✓└{"content":[{"text":"200\n# Python Numbers SDK\n\n## Needed tools\n\n - Python >=3.7\n - pip\n\n## Requires\n\n - future\n - requests\n\n## Install\…ctrl+o
{"content":[{"text":"200\n# Python Numbers SDK\n\n## Needed tools\n\n - Python >=3.7\n - pip\n\n## Requires\n\n - future\n - requests\n\n## Install\n```sh\npip install bandwidth-numbers-sdk\n```\n\n## Testing\n\nTests require the *mock* and *requests_mock* packages. You can install them\nwith\n\n```sh\npip install -r test-requirements.txt\n```\nThe tests can be run by issuing\n```sh\npytest\n```\n\n## Usage\n\n```python\nfrom bandwidth_numbers import Account, Client\n\n# Bearer Auth Client with Token\nclient = Client(url=\"https://dashboard.bandwidth.com/api\", account_id=123456,\n None, None, None, None, None, \"access_token\", access_token_expiration=123)\n\n# Bearer Auth Client with Client Credentials\nclient = Client(url=\"https://dashboard.bandwidth.com/api\", account_id=123456,\n None, None None, \"client_id\", \"client_secret\")\n\n# Basic Auth Client\nclient = Client(url=\"https://dashboard.bandwidth.com/api\", account_id=123456, username=\"foo\",\n password=\"bar\")\n```\nor\n```python\nclient = Client(filename=<path to config>)\n```\n\n### Config format\n```ini\n[account]\naccount_id = 123456789\nusername = spam\npassword = ham\n\n[rest]\nurl = https://dashboard.bandwidth.com/api\n```\n\n## Examples\n\nThere is an 'examples' folder in the source tree that shows how each of the\nAPI objects work with simple example code. To run these make a copy of\n'config.cfg.example', rename to 'config.cfg', edit it to match your IRIS\ncredentials and run the examples individually, e.g.,\n\n```console\npython available_numbers.py\n```\n\nIf an example takes command line parameters, you will get the usage info by\njust executing it.\n\n## API objects\n\n### General principles\n","type":"text"}]}›Thinking…
There's an Account object. Usage pattern:
from bandwidth_numbers import Account, Client
client = Client(url=..., account_id=..., username=..., password=...)
account = Account(client) # ?
sites = account.sites.list()
Let's check models/account.py.
✓$ cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/account.py; sed -n '80,200p' /tmp/nreadme.md✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwi…ctrl+o
{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwidth_numbers.models.account_users import AccountUsers\nfrom bandwidth_numbers.models.available_npa_nxx import AvailableNpaNxx\nfrom bandwidth_numbers.models.available_numbers import AvailableNumbers\nfrom bandwidth_numbers.models.base_resource import BaseResource\nfrom bandwidth_numbers.models.data.account import AccountData\nfrom bandwidth_numbers.models.disc_numbers import DiscNumbers\nfrom bandwidth_numbers.models.disconnects import Disconnects\nfrom bandwidth_numbers.models.in_service_numbers import InServiceNumbers\nfrom bandwidth_numbers.models.line_option_orders import LineOptionOrder\nfrom bandwidth_numbers.models.import_tn_checker import ImportTnChecker\nfrom bandwidth_numbers.models.lnpchecker import LnpChecker\nfrom bandwidth_numbers.models.orders import Orders\nfrom bandwidth_numbers.models.lidbs import Lidbs\nfrom bandwidth_numbers.models.dldas import Dldas\nfrom bandwidth_numbers.models.subscriptions import Subscriptions\nfrom bandwidth_numbers.models.portins import PortIns\nfrom bandwidth_numbers.models.portouts import PortOuts\nfrom bandwidth_numbers.models.reservation import Reservation\nfrom bandwidth_numbers.models.site_hosts import SiteHosts\nfrom bandwidth_numbers.models.sites import Sites\nfrom bandwidth_numbers.models.tn_option_orders import TnOptionOrders\n\nXPATH_ACCOUNT = \"/accounts/{}\"\n\nclass Account(BaseResource, AccountData):\n\n \"\"\"Iris account\"\"\"\n\n _xpath = XPATH_ACCOUNT\n\n @property\n def available_npa_nxx(self):\n return self._available_npa_nxx\n\n @property\n def available_numbers(self):\n return self._available_numbers\n\n @property\n def disconnected_numbers(self):\n return self._disconnected_numbers\n\n @property\n def disconnects(self):\n return self._disconnects\n\n @property\n def dldas(self):\n return self._dldas\n\n @property\n def hosts(self):\n return self._hosts\n\n @property\n def id(self):\n return self.account_id\n @id.setter\n def id(self, id):\n self.account_id = id\n\n @property\n def import_tn_checker(self):\n return self._import_tn_checker\n\n @property\n def in_service_numbers(self):\n return self._in_service_numbers\n\n @property\n def lidbs(self):\n return self._lidbs\n\n @property\n def line_option_orders(self):\n return self._line_option_orders\n\n @property\n def lnpchecker(self):\n return self._lnpchecker\n\n @property\n def orders(self):\n return self._orders\n\n @property\n def portins(self):\n return self._portins\n\n @property\n def portouts(self):\n return self._portouts\n\n @property\n def sites(self):\n return self._sites\n\n @property\n def subscriptions(self):\n return self._subscriptions\n\n @property\n def tnreservation(self):\n return self._tnreservation\n\n @property\n def users(self):\n return self._users\n\n @property\n def tn_option_orders(self):\n return self._tn_option_orders\n\n def __init__(self, parent=None, client=None):\n if client is not None:\n self.id = client.config.account_id\n super().__init__(parent, client)\n AccountData.__init__(self)\n self._available_npa_nxx = AvailableNpaNxx(self, client)\n self._available_numbers = AvailableNumbers(self, client)\n self._disconnected_numbers = DiscNumbers(self, client)\n self._disconnects = Disconnects(self, client)\n self._hosts = SiteHosts(self, client)\n self._import_tn_checker = ImportTnChecker(self, client)\n self._in_service_numbers = InServiceNumbers(self, client)\n self._line_option_orders = LineOptionOrder(self, client)\n self._lnpchecker = LnpChecker(self, client)\n self._orders = Orders(self, client)\n self._portins = PortIns(self, client)\n self._portouts = PortOuts(self, client)\n self._lidbs = Lidbs(self, client)\n self._dldas = Dldas(self, client)\n self._subscriptions = Subscriptions(self, client)\n self._sites = Sites(self, client)\n self._tnreservation = Reservation(self, client)\n self._users = AccountUsers(self, client)\n self._tn_option_orders = TnOptionOrders(self, client)\n\n def get(self, id=None):\n return self._get_data(id)\n### General principles\n\nIn most cases you should use an Account object as a starting point.\n\n```python\naccount = Account(client=client)\n```\n\nAccount has related entities such as Orders, Sites, etc.\n\n```python\nsites = account.sites.list()\nfor site in sites.items:\n pass\n```\n\n### Pagination\n\nSome resources provide paginated result sets and require the use of\npage/size parameters. In these cases a Links object will be provided for\niterating over the results.\n\n```python\nin_service_numbers = account.in_service_numbers.list({\"page\": 1, \"size\": 10})\n\ntotal = int(account.in_service_numbers.total_count)\ntotal_displayed = len(in_service_numbers.items)\npage = None\n\nwhile total_displayed <= total:\n if page is not None:\n in_service_numbers = account.in_service_numbers.list(\n {\"page\": page, \"size\": 10})\n page = account.in_service_numbers.links.next\n for phone_number in in_service_numbers.items:\n print(phone_number)\n total_displayed += len(in_service_numbers.items)\n```\n\n### Available numbers\n\n```python\naccount.available_numbers.list({\"areaCode\": 818})\n```\n\n### Available Npa-Nxx\n\n```python\naccount.available_npa_nxx.list({\"state\": \"NJ\"})\n```\n\n### Cities\n\n```python\nfrom bandwidth_numbers import Cities\n\ncities = Cities(client=client)\ncities.list({\"state\": \"NC\"})\n```\n\n### Covered rate centers\n\n```python\nfrom bandwidth_numbers import CoveredRateCenters\n\nrate_centers = CoveredRateCenters(client=client)\nrate_centers.list({\"page\": 1, \"size\": 10})\n```\n\n### Disconnected numbers\n\n```python\naccount.disconnected_numbers.list({\"areaCode\": 919})\n```\n\n### Disconnecting telephone numbers\n\n#### Creating disconnect orders\n\n```python\ndisconnect = account.disconnects.create({\n \"name\": \"test disconnect order 4\",\n \"customer_order_id\": \"Disconnect1234\",\n \"disconnect_telephone_number_order_type\": {\n \"telephone_number_list\": {\n \"telephone_number\": [\"9192755378\", \"9192755703\"]\n }\n }\n})\n```\n\n#### Getting order data\n\n```python\ndisconnect = account.disconnects.get(\"b902dee1-0585-4258-becd-5c7e51ccf5e1\")\n```\n\n#### Adding notes\n\n```python\ndisconnect.notes.create({\"user_id\": \"spam\", \"description\": \"ham\"})\n```\n\n#### Getting all order's notes\n\n```python\nnotes = disconnect.notes.list()\n```\n\n### Dlda\n\n#### Creating orders\n\n```python\ndlda = account.dldas.create({\n \"customer_order_id\": \"123\",\n \"dlda_tn_groups\": {\n \"dlda_tn_group\": [{\n \"telephone_numbers\": {\n \"telephone_number\": [\"4352154856\"]\n \"account_type\": \"RESIDENTIAL\",\n","type":"text"}]}›Thinking…
With this, the README of the Numbers SDK has given me the exact usage patterns:
account = Account(client=client)account.available_numbers.list({"areaCode": 818, "quantity": 5})account.sites.list()→ site list;account.orders.create({...})- Messaging settings for numbers: check the README further for "messaging settings", "tn options" / "sip peer telephone numbers".
Let me try grepping the README for the messaging settings and orders creation examples, and the sip peer related stuff.
✓$ cd /tmp && grep -n -i -B3 -A 30 "messaging" nreadme.md | head -80✓└{"content":[{"text":"441-sip_peer = account.sites.list().items[0].sip_peers.create({\n442- \"peer_name\": name,\n443- \"is_default_peer\": \"true\…ctrl+o
{"content":[{"text":"441-sip_peer = account.sites.list().items[0].sip_peers.create({\n442- \"peer_name\": name,\n443- \"is_default_peer\": \"true\",\n444: \"short_messaging_protocol\": \"SMPP\",\n445- \"voice_hosts\": {\n446- \"host\": [{\n447- \"host_name\": \"92.168.181.95\"\n448- }]\n449- },\n450- \"sms_hosts\": {\n451- \"host\": [{\n452- \"host_name\": \"92.168.181.95\"\n453- }]\n454- },\n455- \"termination_hosts\": {\n456- \"termination_host\": [{\n457- \"host_name\": \"92.168.181.95\",\n458- \"port\": \"0\",\n459- \"customer_traffic_allowed\": \"DOMESTIC\",\n460- \"data_allowed\": \"true\"\n461- }]\n462- }\n463- })\n464-```\n465-\n466-#### Getting a peer\n467-\n468-```python\n469-sip_peer = account.sites.list().items[0].sip_peers.get(\"500651\")\n470-```\n471-\n472-#### Getting a list of SIP peers\n473-\n474-```python\n","type":"text"}]}✓$ cd /tmp && grep -n -i -B3 -A 25 "Creating orders" nreadme.md | sed -n '1,120p'✓└{"content":[{"text":"188-\n189-### Dlda\n190-\n191:#### Creating orders\n192-\n193-```python\n194-dlda = account.dldas.create({\n195- \"customer_order_id\": …ctrl+o
{"content":[{"text":"188-\n189-### Dlda\n190-\n191:#### Creating orders\n192-\n193-```python\n194-dlda = account.dldas.create({\n195- \"customer_order_id\": \"123\",\n196- \"dlda_tn_groups\": {\n197- \"dlda_tn_group\": [{\n198- \"telephone_numbers\": {\n199- \"telephone_number\": [\"4352154856\"]\n200- \"account_type\": \"RESIDENTIAL\",\n201- \"listing_type\": \"LISTED\",\n202- \"list_address\": \"true\",\n203- \"listing_name\": {\n204- \"first_name\": \"FirstName\",\n205- \"first_name2\": \"FirstName2\",\n206- \"last_name\": \"LastName\",\n207- \"designation\": \"Designation\",\n208- \"title_of_lineage\": \"TitleOfLineage\",\n209- \"title_of_address\": \"TitleOfAddress\",\n210- \"title_of_address2\": \"TitleOfAddress2\",\n211- \"title_of_lineage_name2\": \"TitleOfLineageName2\",\n212- \"title_of_address_name2\": \"TitleOfAddressName2\",\n213- \"title_of_address2_name2\": \"TitleOfAddress2Name2\",\n214- \"place_listing_as\": \"PlaceListingAs\",\n215- },\n216- \"address\": {\n--\n268-\n269-### Lidb\n270-\n271:#### Creating orders\n272-\n273-```python\n274-lidb = account.lidbs.create({\n275- \"lidb_tn_groups\": {\n276- \"lidb_tn_group\": [{\n277- \"telephone_numbers\": {\n278- \"telephone_number\": [\"4352154856\"]\n279- },\n280- \"subscriber_information\": \"Steve\",\n281- \"use_type\": \"RESIDENTIAL\",\n282- \"visibility\": \"PUBLIC\"\n283- },\n284- {\n285- \"telephone_numbers\": {\n286- \"telephone_number\": [\"4352154855\"]\n287- },\n288- \"subscriber_information\": \"Steve\",\n289- \"use_type\": \"RESIDENTIAL\",\n290- \"visibility\": \"PUBLIC\"\n291- }]\n292- }\n293-})\n294-\n295-```\n296-\n--\n315-\n316-### Phone numbers orders\n317-\n318:#### Creating orders\n319-\n320-```python\n321-order = account.orders.create({\n322- \"name\": \"Available Telephone Number order\",\n323- \"site_id\": \"2297\",\n324- \"customer_order_id\": \"123456789\",\n325- \"existing_telephone_number_order_type\": {\n326- \"telephone_number_list\": {\n327- \"telephone_number\": [\"9193752369\", \"9193752720\", \"9193752648\"]\n328- }\n329- }\n330-})\n331-```\n332-\n333-#### Getting order data\n334-\n335-```python\n336-response = account.orders.get(\"f30a31a1-1de4-4939-b094-4521bbe5c8df\")\n337-order = response.order\n338-```\n339-\n340-#### Getting a list of orders\n341-\n342-```python\n343-orders = account.orders.list()\n--\n357-\n358-### Port-ins\n359-\n360:#### Creating orders\n361-\n362-```python\n363-portin = account.portins.create({\n364- \"billing_telephone_number\": \"6882015002\",\n365- \"subscriber\": {\n366- \"subscriber_type\": \"BUSINESS\",\n367- \"business_name\": \"Acme Corporation\",\n368- \"service_address\": {\n369- \"house_number\": \"1623\",\n370- \"street_name\": \"Brockton Ave\",\n371- \"city\": \"Los Angeles\",\n372- \"state_code\": \"CA\",\n373- \"zip\": \"90025\",\n374- \"country\": \"USA\"\n375- }\n376- },\n377- \"loa_authorizing_person\": \"John Doe\",\n378- \"list_of_phone_numbers\": {\n379- \"phone_number\": [\"9882015025\", \"9882015026\"]\n380- },\n381- \"site_id\": \"365\",\n382- \"triggered\": \"false\"\n383-})\n384-```\n385-\n","type":"text"}]}›Thinking…
Alright. Next up is the number messaging configuration — the endpoint for enabling SMS on a number and associating an application: the numbers SDK has messaging_settings. Let me check the README's messaging_settings / "tn option" / "tns" sections and how to fetch a number (TelephoneNumbers). Also, sip_peer_telephone_number / sip_peer_tns.
Let me grep for "MessagingSettings" and "tns" in the README.
✓$ cd /tmp && grep -n -i "messaging_settings\|MessagingSettings\|sms_enabled\|application_id\|tns\b" nreadme.md | head -20; echo ===; grep -n -i -B5 -A 25 "messaging_settings" nreadme.md | head -60✓└{"content":[{"text":"355:order.tns.list()\n487:sip_peer.movetns.add(\"9192000046\")\n488:sip_peer.movetns()\n494:tns = sip_peer.tns.list()\n500:tn = sip_peer.tn…ctrl+o
{"content":[{"text":"355:order.tns.list()\n487:sip_peer.movetns.add(\"9192000046\")\n488:sip_peer.movetns()\n494:tns = sip_peer.tns.list()\n500:tn = sip_peer.tns.get(\"8183386251\")\n506:count = sip_peer.totaltns.get()\n512:tn = sip_peer.tns.get(\"8183386251\")\n563:site.totaltns.get()\n612:### TNs\n617:from bandwidth_numbers import Tns\n619:tns = Tns(client=client)\n620:tn = tns.get(id)\n623:#### Getting a list of TNs\n626:tns.list({\"page\": 1, \"size\": 10 })\n632:tn = tns.get(\"7576768750\")\n===\n","type":"text"}]}›Thinking…
The README doesn't document messaging_settings, but the SDK has models/messaging_settings.py. Let me check its map to understand the fields and endpoints (probably GET/PATCH on /accounts/{accountId}/tns/{tn}/messagingSettings).
✓$ cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/messaging_settings.py; echo "=== resource"; cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/mes…✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\n\nclass MessagingSettingsMap(BaseMap):\n sms_enable…ctrl+o
cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/messaging_settings.py; echo "=== resource"; cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/messaging_settings.py; echo "=== tn"; grep -rn "messaging" .venv/lib/python3.12/site-packages/bandwidth_numbers/models/tn.py .venv/lib/python3.12/site-packages/bandwidth_numbers/models/tns.py 2>/dev/null | head
{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\n\nclass MessagingSettingsMap(BaseMap):\n sms_enabled = None\n campaign_id = None\n message_class = None\n campaign_fully_provisioned = None\n a2p_state = None\n=== resource\n#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\n\nfrom bandwidth_numbers.models.base_resource import BaseResource\nfrom bandwidth_numbers.models.data.messaging_settings import MessagingSettingsData\n\nXML_NAME_MESSAGING_SETTINGS = \"MessagingSettings\"\n\nclass MessagingSettings(BaseResource, MessagingSettingsData):\n pass\n=== tn\n","type":"text"}]}›Thinking…
The MessagingSettings map doesn't have application_id. Hmm. Association with an Application happens via the "Applications" API (dashboard API v2 /accounts/{accountId}/applications), but that's not included in the numbers SDK. To associate a number with an application: PATCH /accounts/{accountId}/tns/{tn} ... actually there are subscriptions... Bandwidth's flow: create an application (with msgUrl) → associate a phone number with the application via PATCH /accounts/{accountId}/applications/{appId}? Historically: sending POST /accounts/{accountId}/applications/{applicationId} with <tns>? Hmm.
The documented way to associate a number with an application (Messaging): update the number's "messaging settings"? No—according to Bandwidth's docs: "Associating a phone number with an application"—the endpoint is PATCH /accounts/{accountId}/tns/{tn} with a body containing applicationId? Hmm. Actually, I recall Bandwidth's doc "Number management → Assign a number to an application" uses the Numbers API: PATCH https://dashboard.bandwidth.com/api/v2/accounts/{accountId}/tns/{tn} with XML/JSON body <TelephoneNumberResponse><ApplicationId>...</ApplicationId>. Hmm.
Wait—the current Bandwidth dashboard API v2 has "tns" endpoints, and the way to configure messaging is via PATCH /accounts/{accountId}/tns/{tn}/messagingSettings (smsEnabled) plus the application association via the Applications API (POST /accounts/{accountId}/applications, then association with PATCH /accounts/{accountId}/applications/{appId}? ...).
This is getting too deep in the weeds and can't be verified without credentials. The user is asking for a simple example. The essential, verifiable flow I can provide:
- Search for available numbers (Numbers SDK)
- Order one (Numbers SDK)—requires site_id + sip_peer
- Send an SMS from an existing SMS-enabled number tied to a messaging application (bandwidth_sdk's MessagesApi)
- Check message status (list_messages)
And in the README, note that steps 1–2 are optional if you already have a number, and that associating with the application is done in the Bandwidth dashboard (Web UI)—this is honest and simple, and avoids unverifiable code.
Also, I could add a helper that uses requests to list applications + list numbers via the dashboard API v2 (JSON): GET /accounts/{accountId}/applications (returns JSON), and GET /accounts/{accountId}/tns?page=&size= returns JSON. These support JSON. Are both documented and safe enough? For check_setup, I want to verify the credentials against the Numbers API (list sites) using the SDK—since it's already tested (returns 401 without credentials), it's better than guessing at JSON endpoints I haven't verified.
OK. Also regarding messaging: the user needs a BANDWIDTH_MESSAGING_APPLICATION_ID. Note: many accounts have a default "messaging application". OK.
Now, let's write the files:
Structure:
/sandbox/repo
├── README.md
├── .env.example
├── requirements.txt
├── bwexample/
│ ├── __init__.py
│ ├── config.py # env-var loading + client factory
│ ├── numbers.py # search/order helpers (bandwidth_numbers SDK)
│ └── messaging.py # send/status helpers (bandwidth_sdk)
└── scripts/
├── check_setup.py
├── search_numbers.py
├── order_number.py
└── send_sms.py
Keep it simpler: put the scripts at the top level? A flat layout is easiest:
bw_demo.py — a single CLI with subcommands?
I think 4 small scripts + 1 config module is clearer for the reader. Make it flat:
- config.py
- numbers_api.py
- messaging_api.py
- check_setup.py, search_numbers.py, order_number.py, send_sms.py
Hmm, naming: bw_config.py etc. Let's use a package bandwidth_demo/ + run.py? Keep it flat with a bw_ prefix. That's fine.
Actually, simplest for the user is: 1 module bw.py (config + helpers), 4 scripts. Let's write this.
Then test: run each script with fake credentials → expect a clean "401" error message rather than a traceback. And a --dry-run that displays the request without calling the API? Add a BW_DRY_RUN env var to skip network calls. This lets us validate the code paths even without credentials.
Let me write the code carefully so that the SDK call signatures are correct.
About messaging's create_message:
import bandwidth
from bandwidth.models import MessageRequest
cfg = bandwidth.Configuration(username=..., password=...)
with bandwidth.ApiClient(cfg) as api:
messages = bandwidth.MessagesApi(api)
msg = messages.create_message(account_id, MessageRequest(application_id=..., to=[...], var_from=..., text=...))
Does MessageRequest's constructor accept aliases? It's a Pydantic model from openapi-generator: by default the constructor takes python attribute names, but is populate_by_name set? Let me test constructing MessageRequest with var_from and from.
Also the Message model: fields are id, time, etc.
Test it now.
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"{'applicationId': '1234', 'to': ['+15551234567'], 'from': '+15557654321', 'text': 'hi'}\nalias ok {'applicationId': '1', 'to': ['+155512345…ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
from bandwidth.models import MessageRequest
m = MessageRequest(application_id='1234', to=['+15551234567'], var_from='+15557654321', text='hi')
print(m.to_dict())
try:
m2 = MessageRequest(**{'applicationId':'1','to':['+15551234567'],'from':'+15557654321','text':'hi'})
print('alias ok', m2.to_dict())
except Exception as e:
print('alias fail', type(e).__name__, str(e)[:120])
"
{"content":[{"text":"{'applicationId': '1234', 'to': ['+15551234567'], 'from': '+15557654321', 'text': 'hi'}\nalias ok {'applicationId': '1', 'to': ['+15551234567'], 'from': '+15557654321', 'text': 'hi'}\n","type":"text"}]}›Thinking…
Both work. Next, check the authentication type for MessagesApi (Basic + OAuth?) and verify that the 401 error handling path functions properly. Also, check the pagination parameters of list_messages (page_size, page_token?). Let me quickly check the signature parameters for the remaining part.
✓$ cd /sandbox/repo && sed -n '383,400p' .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py; grep -n "_auth_settings: List\[str\] = \[" -A 3 .venv/lib/python3.12/site-packages/bandwidth/api…✓└{"content":[{"text":" campaign_id: Annotated[Optional[StrictStr], Field(description=\"The campaign ID of the message.\")] = None,\n from_bw_latenc…ctrl+o
cd /sandbox/repo && sed -n '383,400p' .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py; grep -n "_auth_settings: List\[str\] = \[" -A 3 .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | head -12
{"content":[{"text":" campaign_id: Annotated[Optional[StrictStr], Field(description=\"The campaign ID of the message.\")] = None,\n from_bw_latency: Annotated[Optional[StrictInt], Field(description=\"The minimum Bandwidth latency of the message in seconds. Only available for accounts with the Advanced Quality Metrics feature enabled.\")] = None,\n bw_queued: Annotated[Optional[StrictBool], Field(description=\"A boolean value indicating whether the message is queued in the Bandwidth network.\")] = None,\n product: Annotated[Optional[ProductTypeEnum], Field(description=\"Messaging product associated with the message.\")] = None,\n location: Annotated[Optional[StrictStr], Field(description=\"Location Id associated with the message.\")] = None,\n carrier_queued: Annotated[Optional[StrictBool], Field(description=\"A boolean value indicating whether the message is queued in the carrier network. Only available for OUTBOUND messages from accounts with the Advanced Quality Metrics feature enabled.\")] = None,\n from_carrier_latency: Annotated[Optional[StrictInt], Field(description=\"The minimum carrier latency of the message in seconds. Only available for OUTBOUND messages from accounts with the Advanced Quality Metrics feature enabled.\")] = None,\n calling_number_country_a3: Annotated[Optional[StrictStr], Field(description=\"Calling number country in A3 format.\")] = None,\n called_number_country_a3: Annotated[Optional[StrictStr], Field(description=\"Called number country in A3 format.\")] = None,\n from_segment_count: Annotated[Optional[StrictInt], Field(description=\"Segment count (start range).\")] = None,\n to_segment_count: Annotated[Optional[StrictInt], Field(description=\"Segment count (end range).\")] = None,\n from_message_size: Annotated[Optional[StrictInt], Field(description=\"Message size (start range).\")] = None,\n to_message_size: Annotated[Optional[StrictInt], Field(description=\"Message size (end range).\")] = None,\n sort: Annotated[Optional[StrictStr], Field(description=\"The field and direction to sort by combined with a colon. Direction is either asc or desc.\")] = None,\n page_token: Annotated[Optional[StrictStr], Field(description=\"A base64 encoded value used for pagination of results.\")] = None,\n limit: Annotated[Optional[StrictInt], Field(description=\"The maximum records requested in search result. Default 100. The sum of limit and after cannot be more than 10000.\")] = None,\n limit_total_count: Annotated[Optional[StrictBool], Field(description=\"When set to true, the response's totalCount field will have a maximum value of 10,000. When set to false, or excluded, this will give an accurate totalCount of all messages that match the provided filters. If you are experiencing latency, try using this parameter to limit your results.\")] = None,\n _request_timeout: Union[\n346: _auth_settings: List[str] = [\n347- 'Basic', \n348- 'OAuth2'\n349- ]\n--\n1094: _auth_settings: List[str] = [\n1095- 'Basic', \n1096- 'OAuth2'\n1097- ]\n","type":"text"}]}›Thinking…
Preparation complete. Now I'll write the files. I'll write clean, minimal code with dry-run support and friendly error handling.
Files:
requirements.txt.env.examplebw_config.pybw_numbers.pybw_messaging.pycheck_setup.pysearch_numbers.pyorder_number.pysend_sms.pyREADME.md
Let's design the env vars:
- BANDWIDTH_ACCOUNT_ID (7 digits)
- BANDWIDTH_USERNAME / BANDWIDTH_PASSWORD (Basic auth) — or OAuth BANDWIDTH_CLIENT_ID/SECRET
- BANDWIDTH_MESSAGING_APPLICATION_ID
- BANDWIDTH_FROM_NUMBER
- BANDWIDTH_TO_NUMBER
- BW_DRY_RUN=1
Contents of bw_config.py:
- Loading dotenv
- dataclass Settings
- numbers_client() -> bandwidth_numbers.Client + Account
- messaging_api() -> (ApiClient, MessagesApi) context manager? Even simpler: a function
messaging_client()that returns (api_client, MessagesApi); the caller closes it. - Error formatting helper:
describe_error(e)that maps 401 → "Auth credentials were rejected...".
Alright, let's write it.
Network + SDK checks are done. Now writing the example.
✓write /sandbox/repo/requirements.txt✓└{"content":[{"text":"Successfully wrote 265 bytes to /sandbox/repo/requirements.txt","type":"text"}]}
✓write /sandbox/repo/.env.example✓└{"content":[{"text":"Successfully wrote 779 bytes to /sandbox/repo/.env.example","type":"text"}]}
›Thinking…
Check whether .env is included in .gitignore? The .gitignore shown earlier doesn't have .env in it. I should add it. Let me add .env and .venv/ (.venv/ is already there). I'll add .env.
Next, write bw_config.py.
✓write /sandbox/repo/bw_config.py✓└{"content":[{"text":"Successfully wrote 4322 bytes to /sandbox/repo/bw_config.py","type":"text"}]}
›Thinking…
Oops: typo "DASHBOAD_API" → renamed to DASHBOARD_API. I'll fix it when writing the other files.
Next, the scripts. check_setup.py: validate environment variables, list sites (numbers API), display the count of in-service numbers, and try fetching the messaging list (source_tn filter required... at least one query parameter is needed; use message_direction OUTBOUND, limit 1). Handle dry-run.
search_numbers.py: arguments --area-code, --state, --city, --zip, --quantity; call account.available_numbers.list(params). The params keys follow Bandwidth's availableNumbers query: areaCode, city, state, zip, quantity, enableTNDetail, enableLCA, enabledRateCenter? For safety, support areaCode/city/state/zip/quantity.
Let me check the search params returned in the AvailableNumbersData map.
✓$ cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/data/available_numbers.py; cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/available_numbers.py…✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.base_resource import BaseResource\nfrom bandwidth_numbers.models.data.telephone_numb…ctrl+o
cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/data/available_numbers.py; cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/available_numbers.py 2>/dev/null | head -40
{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.base_resource import BaseResource\nfrom bandwidth_numbers.models.data.telephone_number_list import TelephoneNumberList\nfrom bandwidth_numbers.models.data.telephone_number_detail_list import \\\n TelephoneNumberDetailList\nfrom bandwidth_numbers.models.maps.available_numbers import AvailableNumbersMap\n\nclass AvailableNumbersData(AvailableNumbersMap):\n\n @property\n def total_count(self):\n return self.result_count\n @total_count.setter\n def total_count(self, total_count):\n self.result_count = total_count\n\n def __init__(self):\n self.telephone_number_detail_list = TelephoneNumberDetailList()\n self.telephone_number_list = TelephoneNumberList()\n#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\n\nclass AvailableNumbersMap(BaseMap):\n\n result_count = None\n telephone_number_detail_list = None\n telephone_number_list = None\n","type":"text"}]}›Thinking…
Got it. Query parameters are passed through as-is. Bandwidth's availableNumbers API supports the following: areaCode, city, state, zip, localVanity, tollFreeVanity, quantity, rateCenter, enableTNDetail, enableLCA. I'll support a few of them.
Now I'll write the script.
✓write /sandbox/repo/check_setup.py✓└{"content":[{"text":"Successfully wrote 2093 bytes to /sandbox/repo/check_setup.py","type":"text"}]}
✓write /sandbox/repo/search_numbers.py✓└{"content":[{"text":"Successfully wrote 1943 bytes to /sandbox/repo/search_numbers.py","type":"text"}]}
›Thinking…
Next up is order_number.py. Ordering requires site_id and peer_id (the order payload includes "site_id", "peer_id", "existing_telephone_number_order_type"). Let's check the order data map for the supported fields.
✓$ cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/order.py; echo ===; cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/site.py; echo ===; cat…✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\n\nclass OrderMap(BaseMap):\n\n area_code_search_and…ctrl+o
cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/order.py; echo ===; cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/site.py; echo ===; cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/maps/sip_peer.py
{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\n\nclass OrderMap(BaseMap):\n\n area_code_search_and_order_type = None\n back_order_requested = None\n close_order = None\n count_of_t_ns = None\n customer_order_id = None\n city_search_and_order_type = None\n existing_telephone_number_order_type = None\n last_modified_date = None\n lata_search_and_order_type = None\n name = None\n npanxx_search_and_order_type = None\n order_create_date = None\n order_date = None\n order_id = None\n order_status = None\n order_type = None\n peer_id = None\n partial_allowed = None\n quantity = None\n rate_center_search_and_order_type = None\n reservation_id_list = None\n site_id = None\n state_search_and_order_type = None\n telephone_number_list = None\n toll_free_vanity_search_and_order_type = None\n toll_free_wild_char_search_and_order_type = None\n user_id = None\n zip_search_and_order_type = None\n===\n#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\n\nclass SiteMap(BaseMap):\n\n address = None\n customer_name = None\n customer_provided_id = None\n description = None\n id = None\n name = None\n===\n#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.maps.base_map import BaseMap\n\nclass SipPeerMap(BaseMap):\n\n address = None\n calling_name = None\n description = None\n final_destination_uri = None\n is_default_peer = None\n peer_id = None\n peer_name = None\n short_messaging_protocol = None\n sms_hosts = None\n termination_hosts = None\n voice_hosts = None\n","type":"text"}]}›Thinking…
Notes:
peer_idfield on the SipPeer map (notid). In the check_setup script I usedpeer.id— should usepeer.peer_id. Also, does Site have asip_peersproperty? Check sip_peers in models/site.py.- Order payload: name, site_id, peer_id, existing_telephone_number_order_type{telephone_number_list{telephone_number:[...]}}.
Let's check site.py.
✓$ cd /sandbox/repo && sed -n '1,60p' .venv/lib/python3.12/site-packages/bandwidth_numbers/models/site.py✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwi…ctrl+o
{"content":[{"text":"#!/usr/bin/env python\n\nfrom __future__ import division, absolute_import, print_function\nfrom future.builtins import super\n\nfrom bandwidth_numbers.models.base_resource import BaseResource, BaseResourceList\nfrom bandwidth_numbers.models.data.site import SiteData\nfrom bandwidth_numbers.models.orders import Orders\nfrom bandwidth_numbers.models.portins import PortIns\nfrom bandwidth_numbers.models.sip_peers import SipPeers\nfrom bandwidth_numbers.models.site_totaltns import SiteTotaltns\n\n\nXPATH_SITE = \"/{}\"\n\nclass Site(BaseResource, SiteData):\n\n \"\"\"Account site\"\"\"\n\n _xpath = XPATH_SITE\n\n @property\n def orders(self):\n return self._orders\n\n @property\n def portins(self):\n return self._portins\n\n @property\n def sip_peers(self):\n return self._sip_peers\n\n @property\n def totaltns(self):\n return self._totaltns\n\n def __init__(self, parent=None, client=None):\n super().__init__(parent, client)\n SiteData.__init__(self)\n self._orders = Orders(self, client)\n self._portins = PortIns(self, client)\n self._sip_peers = SipPeers(self, client)\n self._totaltns = SiteTotaltns(self, client)\n\n def get(self, id=None):\n return self._get_data(id)\n","type":"text"}]}›Thinking…
Good: site.sip_peers.list().items, peer.peer_id. Fix check_setup.py so it uses peer.peer_id, with a fallback.
Next, write order_number.py and send_sms.py, fix the typo in bw_config (DASHBOAD_API), and also fix peer.id in check_setup.
✓edit /sandbox/repo/bw_config.py✓└{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/bw_config.py.","type":"text"}],"details":{"diff":" ...\n 18 from dotenv import load_…ctrl+o
{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/bw_config.py.","type":"text"}],"details":{"diff":" ...\n 18 from dotenv import load_dotenv\n 19 \n 20 load_dotenv()\n 21 \n- 22 DASHBOAD_API = \"https://dashboard.bandwidth.com/api\"\n+ 22 DASHBOARD_API = \"https://dashboard.bandwidth.com/api\"\n 23 \n 24 \n 25 @dataclass\n 26 class Settings:\n ...\n 71 from bandwidth_numbers import Client\n 72 from bandwidth_numbers.models.account import Account\n 73 \n 74 client = Client(\n- 75 url=DASHBOAD_API,\n+ 75 url=DASHBOARD_API,\n 76 account_id=settings.account_id,\n 77 username=settings.username,\n 78 password=settings.password,\n 79 client_id=settings.client_id,\n ...","patch":"--- /sandbox/repo/bw_config.py\n+++ /sandbox/repo/bw_config.py\n@@ -18,9 +18,9 @@\n from dotenv import load_dotenv\n \n load_dotenv()\n \n-DASHBOAD_API = \"https://dashboard.bandwidth.com/api\"\n+DASHBOARD_API = \"https://dashboard.bandwidth.com/api\"\n \n \n @dataclass\n class Settings:\n@@ -71,9 +71,9 @@\n from bandwidth_numbers import Client\n from bandwidth_numbers.models.account import Account\n \n client = Client(\n- url=DASHBOAD_API,\n+ url=DASHBOARD_API,\n account_id=settings.account_id,\n username=settings.username,\n password=settings.password,\n client_id=settings.client_id,\n","firstChangedLine":22}}✓edit /sandbox/repo/check_setup.py✓└{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/check_setup.py.","type":"text"}],"details":{"diff":" ...\n 25 for site in site…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/check_setup.py.","type":"text"}],"details":{"diff":" ...\n 25 for site in sites:\n 26 print(f\" site id={site.id} name={site.name}\")\n 27 peers = (site.sip_peers.list().items or []) if hasattr(site, \"sip_peers\") else []\n 28 for peer in peers:\n-29 print(f\" sip peer id={peer.id} name={peer.peer_name}\")\n+29 print(f\" sip peer id={peer.peer_id} name={peer.peer_name}\")\n 30 if not sites:\n 31 print(\" (no sites yet - order_number.py needs a site + sip peer)\")\n 32 except Exception as exc: # noqa: BLE001\n 33 print(f\"\\nNumbers API FAILED -> {describe_error(exc)}\")\n ...","patch":"--- /sandbox/repo/check_setup.py\n+++ /sandbox/repo/check_setup.py\n@@ -25,9 +25,9 @@\n for site in sites:\n print(f\" site id={site.id} name={site.name}\")\n peers = (site.sip_peers.list().items or []) if hasattr(site, \"sip_peers\") else []\n for peer in peers:\n- print(f\" sip peer id={peer.id} name={peer.peer_name}\")\n+ print(f\" sip peer id={peer.peer_id} name={peer.peer_name}\")\n if not sites:\n print(\" (no sites yet - order_number.py needs a site + sip peer)\")\n except Exception as exc: # noqa: BLE001\n print(f\"\\nNumbers API FAILED -> {describe_error(exc)}\")\n","firstChangedLine":29}}›Thinking…
Wait a sec: does site.sip_peers.list() return a list of SipPeer objects, or does it return a wrapper with .items? In the README: sites = account.sites.list() followed by for site in sites.items — so list() returns an object that has .items (a BaseResourceList?). Let me check the return value of sites.list: return self._get_data().site — the .site attribute is a BaseResourceList that has .items. And sip_peers.list → self._get_data().sip_peer is probably also a list object. In the README: sip_peers = site.sip_peers.list(); let me check the README's description around line 472.
Actually, README lines 441-474: account.sites.list().items[0].sip_peers.create({...}), and "Get list of SIP peers" — let's take a look.
✓$ cd /tmp && sed -n '470,530p' nreadme.md✓└{"content":[{"text":"```\n\n#### Getting a list of SIP peers\n\n```python\nsip_peers = account.sites.list().items[0].sip_peers.list()\n```\n\n#### Deleting SIP …ctrl+o
{"content":[{"text":"```\n\n#### Getting a list of SIP peers\n\n```python\nsip_peers = account.sites.list().items[0].sip_peers.list()\n```\n\n#### Deleting SIP peers\n\n```python\nsip_peer.delete()\n```\n\n#### Moving telephone numbers\n\n```python\nsip_peer.movetns.add(\"9192000046\")\nsip_peer.movetns()\n```\n\n#### Getting peer telephone numbers\n\n```python\ntns = sip_peer.tns.list()\n```\n\n#### Getting a single phone number\n\n```python\ntn = sip_peer.tns.get(\"8183386251\")\n```\n\n#### Getting total number of numbers for a SIP peer\n\n```python\ncount = sip_peer.totaltns.get()\n```\n\n#### Setting telephone number options\n\n```python\ntn = sip_peer.tns.get(\"8183386251\")\ntn.rpid_format = \"e164\"\ntn.save()\n```\n\n### Sites\n\n#### Creating a site\n\n```python\nsite = acc.sites.create({\n \"name\": \"test123456\",\n \"address\": {\n \"city\": \"Raleigh\",\n \"address_type\": \"Service\",\n \"house_number\": \"1\",\n \"street_name\": \"Avenue\",\n \"state_code\": \"NC\",\n \"zip\": \"27606\"\n","type":"text"}]}›Thinking…
Since sip_peers = ...sip_peers.list() is returned directly, it's likely a BaseResourceList that has .items. To be safe, I'll write a small helper as_items(result) that returns .items if it exists, otherwise returns the list itself. I'll put this in bw_config and use it in check_setup and order_number.
Let's add the helper to bw_config and use it.
✓edit /sandbox/repo/bw_config.py✓└{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/bw_config.py.","type":"text"}],"details":{"diff":" ...\n 97 return api_client, b…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/bw_config.py.","type":"text"}],"details":{"diff":" ...\n 97 return api_client, bandwidth.MessagesApi(api_client)\n 98 \n 99 \n 100 # ------------------------------------------------------------------- helpers\n+101 def as_items(result):\n+102 \"\"\"The numbers SDK returns either a plain list or a wrapper with `.items`.\"\"\"\n+103 if result is None:\n+104 return []\n+105 items = getattr(result, \"items\", None)\n+106 if items is not None and not callable(items):\n+107 return list(items)\n+108 return list(result)\n+109 \n+110 \n 101 def die(message: str, code: int = 1) -> \"NoReturn\": # type: ignore[name-defined]\n 102 print(f\"ERROR: {message}\", file=sys.stderr)\n 103 raise SystemExit(code)\n 104 \n ...","patch":"--- /sandbox/repo/bw_config.py\n+++ /sandbox/repo/bw_config.py\n@@ -97,8 +97,18 @@\n return api_client, bandwidth.MessagesApi(api_client)\n \n \n # ------------------------------------------------------------------- helpers\n+def as_items(result):\n+ \"\"\"The numbers SDK returns either a plain list or a wrapper with `.items`.\"\"\"\n+ if result is None:\n+ return []\n+ items = getattr(result, \"items\", None)\n+ if items is not None and not callable(items):\n+ return list(items)\n+ return list(result)\n+\n+\n def die(message: str, code: int = 1) -> \"NoReturn\": # type: ignore[name-defined]\n print(f\"ERROR: {message}\", file=sys.stderr)\n raise SystemExit(code)\n \n","firstChangedLine":101}}›Thinking…
Let's check what .items is in BaseResourceList — is items an attribute or a method? In base_resource.py, BASE_PROP_ITEMS = "items" is listed as a property to skip. Let's check the definition of BaseResourceList.
✓$ cd /sandbox/repo && grep -n "class BaseResourceList" -A 60 .venv/lib/python3.12/site-packages/bandwidth_numbers/models/base_resource.py | head -80✓└{"content":[{"text":"118:class BaseResourceList(BaseResourceSimpleList):\n119-\n120- \"\"\"\n121- List of instances of \"class_type\" passed to constructo…ctrl+o
{"content":[{"text":"118:class BaseResourceList(BaseResourceSimpleList):\n119-\n120- \"\"\"\n121- List of instances of \"class_type\" passed to constructor.\n122- \"parent\" used to link BaseResource instances and pass their \"client\"\n123- properties.\n124- \"\"\"\n125-\n126- @property\n127- def class_type(self):\n128- return self._class_type\n129-\n130- @property\n131- def parent(self):\n132- return self._parent\n133-\n134- def __init__(self, class_type, parent=None):\n135- BaseResourceSimpleList.__init__(self)\n136- self._class_type = class_type\n137- self._parent = parent\n138-\n139- def add(self, initial_data=None):\n140- if self.parent is not None:\n141- item = self.class_type(self.parent)\n142- else:\n143- item = self.class_type()\n144- item.set_from_dict(initial_data)\n145- self.items.append(item)\n146- return item\n147-\n148-class BaseResource(BaseData):\n149-\n150- \"\"\"\n151- REST resource.\n152-\n153- \"_node_name\" - corresponding XML element name,\n154- \"_save_post\" - uses POST if True, PUT - otherwise,\n155- \"_xpath_save\" - if set, uses this for saving,\n156- \"client\" does http requests,\n157- \"xpath\" returns the REST resource's relative path.\n158- \"\"\"\n159-\n160- _id = None\n161- _parent = None\n162- _node_name = None\n163- _node_name_save = None\n164- _save_post = False\n165- _xpath = \"\"\n166- _xpath_save = None\n167-\n168- @property\n169- def client(self):\n170- return self._client\n171- @client.setter\n172- def client(self, client):\n173- self._client = client\n174-\n175- @property\n176- def id(self):\n177- return self._id\n178- @id.setter\n","type":"text"}]}›Thinking…
items is a list attribute. OK, as_items will work.
However, in AvailableNumbers.list(params), when tnDetail is not requested, it returns self.telephone_number_list.telephone_number — this is probably a list of strings (a list of simple strings). Let me check TelephoneNumberList. If it's BaseResourceSimpleList, then .items returns strings.
Let me check quickly.
✓$ cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/data/telephone_number_list.py; grep -n "class BaseResourceSimpleList" -A 25 .venv/lib/python3.12/site-packages/bandw…✓└{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.base_resource import BaseData, BaseResourceSimpleList\nfrom bandwidth_numbers.models…ctrl+o
cd /sandbox/repo && cat .venv/lib/python3.12/site-packages/bandwidth_numbers/models/data/telephone_number_list.py; grep -n "class BaseResourceSimpleList" -A 25 .venv/lib/python3.12/site-packages/bandwidth_numbers/models/base_resource.py | head -30
{"content":[{"text":"#!/usr/bin/env python\n\nfrom bandwidth_numbers.models.base_resource import BaseData, BaseResourceSimpleList\nfrom bandwidth_numbers.models.maps.telephone_number_list import TelephoneNumberListMap\n\nclass TelephoneNumberList(TelephoneNumberListMap, BaseData):\n\n @property\n def items(self):\n return self.telephone_number.items\n\n def __init__(self):\n self.telephone_number = BaseResourceSimpleList()\n\n def add(self, phone_number=None):\n return self.telephone_number.add(phone_number)\n98:class BaseResourceSimpleList(object):\n99-\n100- \"\"\"\n101- Used to store simple values.\n102- \"\"\"\n103-\n104- @property\n105- def items(self):\n106- return self._items\n107-\n108- def __init__(self):\n109- self._items = []\n110-\n111- def add(self, value):\n112- self.items.append(value)\n113- return self.items[-1]\n114-\n115- def clear(self):\n116- del self.items[:]\n117-\n118-class BaseResourceList(BaseResourceSimpleList):\n119-\n120- \"\"\"\n121- List of instances of \"class_type\" passed to constructor.\n122- \"parent\" used to link BaseResource instances and pass their \"client\"\n123- properties.\n","type":"text"}]}›Thinking…
In other words, available_numbers.list(params) returns self.telephone_number_list.telephone_number, which is a BaseResourceSimpleList — meaning it's not a plain list, but has .items (a list of strings). Therefore, in search_numbers.py we should use as_items(). Let's update search_numbers.py to use as_items, and check check_setup too.
Also, for detailed results, enableTNDetail: "true" returns telephone_number_detail_list. Let's keep it simple: just the plain numbers.
Update the scripts accordingly. Next, write order_number.py and send_sms.py.
Order payload (from the README):
order = account.orders.create({
"name": "...",
"site_id": "2297",
"peer_id": "...",
"customer_order_id": "...",
"existing_telephone_number_order_type": {
"telephone_number_list": {"telephone_number": ["9193752369"]}
}
})
After that, order.order_status, order.id.
Note: create returns an order object; does the order_status attribute exist on Order? OrderData has order_status. Good.
send_sms.py: Arguments --from, --to, --text, --media (repeatable), --tag, --application-id. Use MessagesApi.create_message, output the message ID/time, then optionally poll for status with list_messages(message_id=...) a few times (short duration, not long-running — max ~10 seconds). Keep the polling short (3 attempts, 2-second sleep) and note that it's optional. The user instructed to avoid long-running commands, so a 10-second status poll is fine, but make it opt-in with a --wait flag.
Alright, let's write both files.
✓edit /sandbox/repo/search_numbers.py✓└{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/search_numbers.py.","type":"text"}],"details":{"diff":" ...\n 6 \"\"\"\n 7 \n 8 imp…ctrl+o
{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/search_numbers.py.","type":"text"}],"details":{"diff":" ...\n 6 \"\"\"\n 7 \n 8 import argparse\n 9 \n-10 from bw_config import describe_error, die, load_settings, numbers_account\n+10 from bw_config import as_items, describe_error, die, load_settings, numbers_account\n 11 \n 12 \n 13 def main() -> None:\n 14 p = argparse.ArgumentParser(description=\"Search available Bandwidth phone numbers.\")\n ...\n 40 return\n 41 \n 42 try:\n 43 account = numbers_account(s)\n-44 numbers = account.available_numbers.list(params)\n+44 numbers = as_items(account.available_numbers.list(params))\n 45 except Exception as exc: # noqa: BLE001\n 46 die(describe_error(exc))\n 47 \n-48 numbers = numbers or []\n 49 if not numbers:\n 50 print(\"No numbers matched that search. Try a different area code or state.\")\n 51 return\n 52 print(f\"{len(numbers)} number(s) available:\")\n ...","patch":"--- /sandbox/repo/search_numbers.py\n+++ /sandbox/repo/search_numbers.py\n@@ -6,9 +6,9 @@\n \"\"\"\n \n import argparse\n \n-from bw_config import describe_error, die, load_settings, numbers_account\n+from bw_config import as_items, describe_error, die, load_settings, numbers_account\n \n \n def main() -> None:\n p = argparse.ArgumentParser(description=\"Search available Bandwidth phone numbers.\")\n@@ -40,13 +40,12 @@\n return\n \n try:\n account = numbers_account(s)\n- numbers = account.available_numbers.list(params)\n+ numbers = as_items(account.available_numbers.list(params))\n except Exception as exc: # noqa: BLE001\n die(describe_error(exc))\n \n- numbers = numbers or []\n if not numbers:\n print(\"No numbers matched that search. Try a different area code or state.\")\n return\n print(f\"{len(numbers)} number(s) available:\")\n","firstChangedLine":10}}✓edit /sandbox/repo/check_setup.py✓└{"content":[{"text":"Successfully replaced 3 block(s) in /sandbox/repo/check_setup.py.","type":"text"}],"details":{"diff":" ...\n 2 \n 3 Run: python check…ctrl+o
{"content":[{"text":"Successfully replaced 3 block(s) in /sandbox/repo/check_setup.py.","type":"text"}],"details":{"diff":" ...\n 2 \n 3 Run: python check_setup.py\n 4 \"\"\"\n 5 \n- 6 from bw_config import describe_error, load_settings, messaging_api, numbers_account\n+ 6 from bw_config import (\n+ 7 as_items,\n+ 8 describe_error,\n+ 9 load_settings,\n+10 messaging_api,\n+11 numbers_account,\n+12 )\n 7 \n 8 \n 9 def main() -> None:\n 10 s = load_settings()\n ...\n 19 \n 20 # 1) Numbers API (also proves the account id + credentials are good)\n 21 try:\n 22 account = numbers_account(s)\n-23 sites = account.sites.list().items or []\n+29 sites = as_items(account.sites.list())\n 24 print(f\"\\nNumbers API OK - {len(sites)} site(s):\")\n 25 for site in sites:\n 26 print(f\" site id={site.id} name={site.name}\")\n-27 peers = (site.sip_peers.list().items or []) if hasattr(site, \"sip_peers\") else []\n+33 peers = as_items(site.sip_peers.list())\n 28 for peer in peers:\n 29 print(f\" sip peer id={peer.peer_id} name={peer.peer_name}\")\n 30 if not sites:\n 31 print(\" (no sites yet - order_number.py needs a site + sip peer)\")\n ...","patch":"--- /sandbox/repo/check_setup.py\n+++ /sandbox/repo/check_setup.py\n@@ -2,9 +2,15 @@\n \n Run: python check_setup.py\n \"\"\"\n \n-from bw_config import describe_error, load_settings, messaging_api, numbers_account\n+from bw_config import (\n+ as_items,\n+ describe_error,\n+ load_settings,\n+ messaging_api,\n+ numbers_account,\n+)\n \n \n def main() -> None:\n s = load_settings()\n@@ -19,13 +25,13 @@\n \n # 1) Numbers API (also proves the account id + credentials are good)\n try:\n account = numbers_account(s)\n- sites = account.sites.list().items or []\n+ sites = as_items(account.sites.list())\n print(f\"\\nNumbers API OK - {len(sites)} site(s):\")\n for site in sites:\n print(f\" site id={site.id} name={site.name}\")\n- peers = (site.sip_peers.list().items or []) if hasattr(site, \"sip_peers\") else []\n+ peers = as_items(site.sip_peers.list())\n for peer in peers:\n print(f\" sip peer id={peer.peer_id} name={peer.peer_name}\")\n if not sites:\n print(\" (no sites yet - order_number.py needs a site + sip peer)\")\n","firstChangedLine":6}}›Thinking…
Wait: Account.sites returns Sites. But Account needs an xpath that includes the account ID: _xpath = "/accounts/{}" is probably formatted with the id. OK.
Also, about sites.list() for Account: Sites(self, client) has parent as account → xpath becomes "/accounts/{id}/sites". Good.
Next up is order_number.py.
✓write /sandbox/repo/order_number.py✓└{"content":[{"text":"Successfully wrote 3316 bytes to /sandbox/repo/order_number.py","type":"text"}]}
✓write /sandbox/repo/send_sms.py✓└{"content":[{"text":"Successfully wrote 3539 bytes to /sandbox/repo/send_sms.py","type":"text"}]}
›Thinking…
Check whether the list_messages response model has .messages, and whether Message has message_status. Also check whether bandwidth.MessageRequest is exported from the top-level bandwidth package. Let's test it out.
×$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 4, in <module>\nImportError: cannot import name 'MessageList' from 'bandwidth…ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
import bandwidth
print(hasattr(bandwidth,'MessageRequest'), hasattr(bandwidth,'MessagesApi'), hasattr(bandwidth,'ApiClient'), hasattr(bandwidth,'Configuration'))
from bandwidth.models import MessageList
print(MessageList.model_fields.keys())
from bandwidth.models import Message
print('message_status' in Message.model_fields)
"
{"content":[{"text":"Traceback (most recent call last):\n File \"<string>\", line 4, in <module>\nImportError: cannot import name 'MessageList' from 'bandwidth.models' (/sandbox/repo/.venv/lib/python3.12/site-packages/bandwidth/models/__init__.py). Did you mean: 'MessagesList'?\nTrue True True True\n\n\nCommand exited with code 1","type":"text"}],"details":{}}›Thinking…
MessageList → MessagesList. Let me check its fields and the return type of list_messages.
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"dict_keys(['total_count', 'page_info', 'messages', 'additional_properties'])\nFalse\n43: def __init__(self, api_client=None) -> None:\n6…ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
from bandwidth.models import MessagesList, Message
print(MessagesList.model_fields.keys())
print('message_status' in Message.model_fields)
"; grep -n ") -> \(.*\):" .venv/lib/python3.12/site-packages/bandwidth/api/messages_api.py | sed -n '1,6p'
{"content":[{"text":"dict_keys(['total_count', 'page_info', 'messages', 'additional_properties'])\nFalse\n43: def __init__(self, api_client=None) -> None:\n66: ) -> Message:\n146: ) -> ApiResponse[Message]:\n226: ) -> RESTResponseType:\n293: ) -> RequestSerialized:\n412: ) -> MessagesList:\n","type":"text"}]}›Thinking…
list_messages returns a MessagesList that has .messages (a list of Message). But Message doesn't have a message_status field?! Let me check the fields of the Message model — earlier I output: id, owner, application_id, time, segment_count, direction, to, var_from, media, text, tag, priority, expiration. There's no message_status/error_code. Hmm — in other words, the status doesn't appear in the model of the search results. The --wait polling will fail with AttributeError.
Options: use list_messages_with_http_info? The same model gets returned. Better approach: to get the status, use additional_properties? The API returns messageStatus in the JSON, but the generated model doesn't include it... unless the additional_properties dict captures unknown fields (openapi generator using pydantic: does the model_config allow additional properties?). Let me test it: create a Message from a dict containing messageStatus, and check additional_properties.
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"{'id': 'x', 'messageStatus': 'DELIVERED', 'errorCode': 0, 'messageSize': 1}\n{'messageStatus': 'DELIVERED', 'errorCode': 0, 'messageSize': …ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
from bandwidth.models import Message, MessagesList
m = Message.from_dict({'id':'x','messageStatus':'DELIVERED','errorCode':0,'messageSize':1})
print(m.to_dict())
print(m.additional_properties)
"
{"content":[{"text":"{'id': 'x', 'messageStatus': 'DELIVERED', 'errorCode': 0, 'messageSize': 1}\n{'messageStatus': 'DELIVERED', 'errorCode': 0, 'messageSize': 1}\n","type":"text"}]}›Thinking…
Additional properties are preserved. Therefore, I can read the status with getattr(m, 'message_status', None) or m.additional_properties.get('messageStatus').
The simplest and most robust approach: use to_dict() and read messageStatus. Update the wait loop in send_sms.py:
found = messages.list_messages(s.account_id, message_id=message.id)
statuses = [m.to_dict().get("messageStatus") for m in (found.messages or [])]
Let's apply this fix. Also, add .env to .gitignore.
After that, write a README that includes pricing and manageability notes.
Then run tests: syntax check + dry-run execution + execution with fake credentials, and verify the 401 error handling.
✓edit /sandbox/repo/send_sms.py✓└{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/send_sms.py.","type":"text"}],"details":{"diff":" ...\n 72 if args.wait:\n…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/send_sms.py.","type":"text"}],"details":{"diff":" ...\n 72 if args.wait:\n 73 for attempt in range(3):\n 74 time.sleep(2)\n 75 found = messages.list_messages(s.account_id, message_id=message.id)\n-76 statuses = [m.message_status for m in (found.messages or [])]\n+76 # `messageStatus` is not a declared field on the generated model,\n+77 # but the SDK keeps unknown response fields in additional_properties.\n+78 statuses = [m.to_dict().get(\"messageStatus\") for m in (found.messages or [])]\n 77 print(f\"status : {statuses or 'unknown (still in flight)'}\")\n 78 if statuses and statuses[0] in {\"DELIVERED\", \"FAILED\", \"UNDELIVERED\"}:\n 79 break\n 80 finally:\n ...","patch":"--- /sandbox/repo/send_sms.py\n+++ /sandbox/repo/send_sms.py\n@@ -72,9 +72,11 @@\n if args.wait:\n for attempt in range(3):\n time.sleep(2)\n found = messages.list_messages(s.account_id, message_id=message.id)\n- statuses = [m.message_status for m in (found.messages or [])]\n+ # `messageStatus` is not a declared field on the generated model,\n+ # but the SDK keeps unknown response fields in additional_properties.\n+ statuses = [m.to_dict().get(\"messageStatus\") for m in (found.messages or [])]\n print(f\"status : {statuses or 'unknown (still in flight)'}\")\n if statuses and statuses[0] in {\"DELIVERED\", \"FAILED\", \"UNDELIVERED\"}:\n break\n finally:\n","firstChangedLine":76}}✓$ cd /sandbox/repo && printf '\n# Secrets\n.env\n' >> .gitignore && tail -4 .gitignore✓└{"content":[{"text":".dart_tool/\n\n# Secrets\n.env\n","type":"text"}]}
›Thinking…
Alright, let's test it: dry-run mode with fake environment variables, and live mode with fake credentials (expecting a 401).
Also, I'll fix check_setup: list_messages(message_direction="OUTBOUND", limit=1) — this parameter is of type ListMessageDirectionEnum, and a string might work via pydantic's type conversion. Let's test constructing it. I'll try running it with dry-run.
✓$ cd /sandbox/repo && cat > .env <<'EOF'✓└{"content":[{"text":"=== check_setup.py\nsyntax ok\n=== search_numbers.py\nsyntax ok\n=== order_number.py\nsyntax ok\n=== send_sms.py\nsyntax ok\n--- dry runs\n…ctrl+o
cd /sandbox/repo && cat > .env <<'EOF'
BANDWIDTH_ACCOUNT_ID=1234567
BANDWIDTH_USERNAME=fake_user
BANDWIDTH_PASSWORD=fake_pass
BANDWIDTH_MESSAGING_APPLICATION_ID=1111111-2222-3333-4444-555555555555
BANDWIDTH_FROM_NUMBER=+19195551234
BANDWIDTH_TO_NUMBER=+19195559876
BW_DRY_RUN=1
EOF
for f in check_setup.py search_numbers.py order_number.py send_sms.py; do echo "=== $f"; .venv/bin/python -c "import ast,sys; ast.parse(open('$f').read())" && echo "syntax ok"; done
echo "--- dry runs"; BW_DRY_RUN=1 .venv/bin/python check_setup.py; BW_DRY_RUN=1 .venv/bin/python search_numbers.py --area-code 919 --quantity 3; BW_DRY_RUN=1 .venv/bin/python order_number.py --number 19195551234; BW_DRY_RUN=1 .venv/bin/python send_sms.py --to +15551234567 --text hi
{"content":[{"text":"=== check_setup.py\nsyntax ok\n=== search_numbers.py\nsyntax ok\n=== order_number.py\nsyntax ok\n=== send_sms.py\nsyntax ok\n--- dry runs\naccount_id : 1234567\nauth : basic\ndry_run : True\n\n[dry-run] would call:\n GET https://dashboard.bandwidth.com/api/v2/accounts/1234567/sites\n GET https://messaging.bandwidth.com/api/v2/accounts/1234567/messages?limit=1\n[dry-run] GET /v2/accounts/1234567/availableNumbers?{'quantity': 3, 'areaCode': '919'}\n[dry-run] POST /v2/accounts/1234567/orders\n site_id=<first site> peer_id=<default peer>\n body={'name': 'quickstart order', 'customer_order_id': '33c01f25d2bc', 'existing_telephone_number_order_type': {'telephone_number_list': {'telephone_number': ['19195551234']}}}\n[dry-run] POST https://messaging.bandwidth.com/api/v2/accounts/1234567/messages\n body={'applicationId': '1111111-2222-3333-4444-555555555555', 'from': '+19195551234', 'to': ['+15551234567'], 'text': 'hi'}\n","type":"text"}]}›Thinking…
Dry run is working. Next is the live run with fake credentials → expecting an easy-to-understand 401 message (no traceback).
✓$ cd /sandbox/repo && echo "=== check_setup (fake creds)"; BW_DRY_RUN=0 timeout 90 .venv/bin/python check_setup.py; echo "exit=$?"; echo "=== search_numbers"; BW_DRY_RUN=0 timeout 60 .venv/bin/python se…✓└{"content":[{"text":"=== check_setup (fake creds)\naccount_id : 1234567\nauth : basic\ndry_run : False\n\nNumbers API FAILED -> 401 Unauthorized - Band…ctrl+o
cd /sandbox/repo && echo "=== check_setup (fake creds)"; BW_DRY_RUN=0 timeout 90 .venv/bin/python check_setup.py; echo "exit=$?"; echo "=== search_numbers"; BW_DRY_RUN=0 timeout 60 .venv/bin/python search_numbers.py --area-code 919 --quantity 3; echo "exit=$?"; echo "=== send_sms"; BW_DRY_RUN=0 timeout 60 .venv/bin/python send_sms.py --to +15551234567 --text hi; echo "exit=$?"
{"content":[{"text":"=== check_setup (fake creds)\naccount_id : 1234567\nauth : basic\ndry_run : False\n\nNumbers API FAILED -> 401 Unauthorized - Bandwidth rejected the credentials (check username/password or client id/secret).\n\nMessaging API FAILED -> AttributeError: 'ApiClient' object has no attribute 'close'\n\nNext: python search_numbers.py --area-code 919 --quantity 5\n python send_sms.py --to +15551234567 --text 'hello from Bandwidth'\nexit=0\n=== search_numbers\nERROR: 401 Unauthorized - Bandwidth rejected the credentials (check username/password or client id/secret).\nexit=1\n=== send_sms\nERROR: AttributeError: 'ApiClient' object has no attribute 'close'\nexit=1\n","type":"text"}]}›Thinking…
Two issues:
ApiClientdoesn't haveclose(). openapi-generator's ApiClient hasclose(), but... apparently this generated version doesn't. Let me check the methods. It might have__enter__/__exit__orrest_client.pool_manager.clear(). Let me investigate.- Also, I want the 401 from messaging to surface correctly (it was being hidden by the close error inside finally). Fix the way it's closed.
Let me check the ApiClient's API.
✓$ cd /sandbox/repo && .venv/bin/python -c "✓└{"content":[{"text":"['NATIVE_TYPES_MAPPING', 'PRIMITIVE_TYPES', '_ApiClient__deserialize', '_ApiClient__deserialize_date', '_ApiClient__deserialize_datetime', …ctrl+o
cd /sandbox/repo && .venv/bin/python -c "
import bandwidth
c = bandwidth.ApiClient(bandwidth.Configuration(username='a',password='b'))
print([m for m in dir(c) if not m.startswith('__')])
"
{"content":[{"text":"['NATIVE_TYPES_MAPPING', 'PRIMITIVE_TYPES', '_ApiClient__deserialize', '_ApiClient__deserialize_date', '_ApiClient__deserialize_datetime', '_ApiClient__deserialize_enum', '_ApiClient__deserialize_file', '_ApiClient__deserialize_model', '_ApiClient__deserialize_object', '_ApiClient__deserialize_primitive', '_apply_auth_params', '_default', '_pool', 'call_api', 'client_side_validation', 'configuration', 'cookie', 'default_headers', 'deserialize', 'files_parameters', 'get_default', 'param_serialize', 'parameters_to_tuples', 'parameters_to_url_query', 'response_deserialize', 'rest_client', 'sanitize_for_serialization', 'select_header_accept', 'select_header_content_type', 'set_default', 'set_default_header', 'update_params_for_auth', 'user_agent']\n","type":"text"}]}›Thinking…
There's no close(). There is rest_client (with pool_manager). Should I add a close_messaging(api_client) helper to bw_config that calls api_client.rest_client.pool_manager.clear() if it exists, and wrap it to be fault-tolerant, or alternatively just not close it at all (the process is going to exit anyway)? The simplest approach is to use a contextlib.suppress-style safe close helper. Let me add close_client(api_client) to bw_config and use it in both scripts.
✓edit /sandbox/repo/bw_config.py✓└{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/bw_config.py.","type":"text"}],"details":{"diff":" ...\n 97 return api_client, b…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/bw_config.py.","type":"text"}],"details":{"diff":" ...\n 97 return api_client, bandwidth.MessagesApi(api_client)\n 98 \n 99 \n 100 # ------------------------------------------------------------------- helpers\n+101 def close_client(api_client) -> None:\n+102 \"\"\"Release the messaging SDK's connection pool (it has no .close()).\"\"\"\n+103 try:\n+104 close = getattr(api_client, \"close\", None)\n+105 if callable(close):\n+106 close()\n+107 return\n+108 pool = getattr(getattr(api_client, \"rest_client\", None), \"pool_manager\", None)\n+109 if pool is not None and hasattr(pool, \"clear\"):\n+110 pool.clear()\n+111 except Exception: # noqa: BLE001 - teardown must never mask the real error\n+112 pass\n+113 \n+114 \n 101 def as_items(result):\n 102 \"\"\"The numbers SDK returns either a plain list or a wrapper with `.items`.\"\"\"\n 103 if result is None:\n 104 return []\n ...","patch":"--- /sandbox/repo/bw_config.py\n+++ /sandbox/repo/bw_config.py\n@@ -97,8 +97,22 @@\n return api_client, bandwidth.MessagesApi(api_client)\n \n \n # ------------------------------------------------------------------- helpers\n+def close_client(api_client) -> None:\n+ \"\"\"Release the messaging SDK's connection pool (it has no .close()).\"\"\"\n+ try:\n+ close = getattr(api_client, \"close\", None)\n+ if callable(close):\n+ close()\n+ return\n+ pool = getattr(getattr(api_client, \"rest_client\", None), \"pool_manager\", None)\n+ if pool is not None and hasattr(pool, \"clear\"):\n+ pool.clear()\n+ except Exception: # noqa: BLE001 - teardown must never mask the real error\n+ pass\n+\n+\n def as_items(result):\n \"\"\"The numbers SDK returns either a plain list or a wrapper with `.items`.\"\"\"\n if result is None:\n return []\n","firstChangedLine":101}}×edit /sandbox/repo/check_setup.py✓└{"content":[{"text":"Could not find edits[0] in /sandbox/repo/check_setup.py. The oldText must match exactly including all whitespace and newlines.","type":"tex…ctrl+o
{"content":[{"text":"Could not find edits[0] in /sandbox/repo/check_setup.py. The oldText must match exactly including all whitespace and newlines.","type":"text"}],"details":{}}✓edit /sandbox/repo/send_sms.py✓└{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/send_sms.py.","type":"text"}],"details":{"diff":" ...\n 8 \n 9 import argparse\n 10 …ctrl+o
{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/send_sms.py.","type":"text"}],"details":{"diff":" ...\n 8 \n 9 import argparse\n 10 import time\n 11 \n-12 from bw_config import describe_error, die, load_settings, messaging_api\n+12 from bw_config import close_client, describe_error, die, load_settings, messaging_api\n 13 \n 14 \n 15 def main() -> None:\n 16 p = argparse.ArgumentParser(description=\"Send an SMS/MMS via Bandwidth.\")\n ...\n 79 print(f\"status : {statuses or 'unknown (still in flight)'}\")\n 80 if statuses and statuses[0] in {\"DELIVERED\", \"FAILED\", \"UNDELIVERED\"}:\n 81 break\n 82 finally:\n-83 api_client.close()\n+83 close_client(api_client)\n 84 except Exception as exc: # noqa: BLE001\n 85 die(describe_error(exc))\n 86 \n 87 \n ...","patch":"--- /sandbox/repo/send_sms.py\n+++ /sandbox/repo/send_sms.py\n@@ -8,9 +8,9 @@\n \n import argparse\n import time\n \n-from bw_config import describe_error, die, load_settings, messaging_api\n+from bw_config import close_client, describe_error, die, load_settings, messaging_api\n \n \n def main() -> None:\n p = argparse.ArgumentParser(description=\"Send an SMS/MMS via Bandwidth.\")\n@@ -79,9 +79,9 @@\n print(f\"status : {statuses or 'unknown (still in flight)'}\")\n if statuses and statuses[0] in {\"DELIVERED\", \"FAILED\", \"UNDELIVERED\"}:\n break\n finally:\n- api_client.close()\n+ close_client(api_client)\n except Exception as exc: # noqa: BLE001\n die(describe_error(exc))\n \n \n","firstChangedLine":12}}›Thinking…
The import block in check_setup.py is a multi-line parenthesized import. Please fix it accordingly.
✓edit /sandbox/repo/check_setup.py✓└{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/check_setup.py.","type":"text"}],"details":{"diff":" ...\n 4 \"\"\"\n 5 \n 6 from b…ctrl+o
{"content":[{"text":"Successfully replaced 2 block(s) in /sandbox/repo/check_setup.py.","type":"text"}],"details":{"diff":" ...\n 4 \"\"\"\n 5 \n 6 from bw_config import (\n 7 as_items,\n+ 8 close_client,\n 8 describe_error,\n 9 load_settings,\n 10 messaging_api,\n 11 numbers_account,\n ...\n 45 resp = messages.list_messages(s.account_id, message_direction=\"OUTBOUND\", limit=1)\n 46 total = getattr(resp, \"total_count\", None)\n 47 print(f\"\\nMessaging API OK - {total if total is not None else 'some'} outbound message(s) on record.\")\n 48 finally:\n-49 api_client.close()\n+50 close_client(api_client)\n 50 except Exception as exc: # noqa: BLE001\n 51 print(f\"\\nMessaging API FAILED -> {describe_error(exc)}\")\n 52 \n 53 print(\n ...","patch":"--- /sandbox/repo/check_setup.py\n+++ /sandbox/repo/check_setup.py\n@@ -4,8 +4,9 @@\n \"\"\"\n \n from bw_config import (\n as_items,\n+ close_client,\n describe_error,\n load_settings,\n messaging_api,\n numbers_account,\n@@ -45,9 +46,9 @@\n resp = messages.list_messages(s.account_id, message_direction=\"OUTBOUND\", limit=1)\n total = getattr(resp, \"total_count\", None)\n print(f\"\\nMessaging API OK - {total if total is not None else 'some'} outbound message(s) on record.\")\n finally:\n- api_client.close()\n+ close_client(api_client)\n except Exception as exc: # noqa: BLE001\n print(f\"\\nMessaging API FAILED -> {describe_error(exc)}\")\n \n print(\n","firstChangedLine":8}}›Thinking…
Wait, did the second edit in check_setup.py fail earlier? It said edits[0] wasn't found — since the first edit failed, maybe the second one wasn't applied either (atomic behavior). Let me check the file contents.
✓$ cd /sandbox/repo && grep -n "close\|import" check_setup.py | head -20; echo "=== rerun"; BW_DRY_RUN=0 timeout 90 .venv/bin/python check_setup.py; echo "exit=$?"; BW_DRY_RUN=0 timeout 60 .venv/bin/pyth…✓└{"content":[{"text":"6:from bw_config import (\n8: close_client,\n50: close_client(api_client)\n=== rerun\naccount_id : 1234567\nauth : basi…ctrl+o
cd /sandbox/repo && grep -n "close\|import" check_setup.py | head -20; echo "=== rerun"; BW_DRY_RUN=0 timeout 90 .venv/bin/python check_setup.py; echo "exit=$?"; BW_DRY_RUN=0 timeout 60 .venv/bin/python send_sms.py --to +15551234567 --text hi; echo "exit=$?"; BW_DRY_RUN=0 timeout 60 .venv/bin/python order_number.py --number 19195551234; echo "exit=$?"
{"content":[{"text":"6:from bw_config import (\n8: close_client,\n50: close_client(api_client)\n=== rerun\naccount_id : 1234567\nauth : basic\ndry_run : False\n\nNumbers API FAILED -> 401 Unauthorized - Bandwidth rejected the credentials (check username/password or client id/secret).\n\nMessaging API FAILED -> 401 Unauthorized - Bandwidth rejected the credentials (check username/password or client id/secret).\n\nNext: python search_numbers.py --area-code 919 --quantity 5\n python send_sms.py --to +15551234567 --text 'hello from Bandwidth'\nexit=0\nERROR: 401 Unauthorized - Bandwidth rejected the credentials (check username/password or client id/secret).\nexit=1\nERROR: 401 Unauthorized - Bandwidth rejected the credentials (check username/password or client id/secret).\nexit=1\n","type":"text"}]}›Thinking…
All four scripts run and correctly report the 401 blocker (we don't have a Bandwidth account here).
Also, test the OAuth path and the "no credentials" path, and the dry-run send_sms with media/tag. Furthermore, test the argument validation error messages.
After that, write a README including pricing and manageability.
Quick test:
✓$ cd /sandbox/repo && echo "== no creds"; env -u BANDWIDTH_USERNAME -u BANDWIDTH_PASSWORD BW_DRY_RUN=1 .venv/bin/python -c "✓└{"content":[{"text":"== no creds\naccount_id : 1234567\nauth : basic\ndry_run : True\n\n[dry-run] would call:\n GET https://dashboard.bandwidth.com/a…ctrl+o
cd /sandbox/repo && echo "== no creds"; env -u BANDWIDTH_USERNAME -u BANDWIDTH_PASSWORD BW_DRY_RUN=1 .venv/bin/python -c "
import os
os.environ.pop('BANDWIDTH_USERNAME',None); os.environ.pop('BANDWIDTH_PASSWORD',None)
import check_setup; check_setup.main()" ; echo "exit=$?"; echo "== oauth dry"; BW_DRY_RUN=1 BANDWIDTH_USERNAME= BANDWIDTH_PASSWORD= BANDWIDTH_CLIENT_ID=cid BANDWIDTH_CLIENT_SECRET=sec .venv/bin/python check_setup.py; echo "== media dry"; BW_DRY_RUN=1 .venv/bin/python send_sms.py --to +15551234567 --text look --media https://example.com/cat.png --tag demo; echo "== no filter"; BW_DRY_RUN=1 .venv/bin/python search_numbers.py; echo "exit=$?"
{"content":[{"text":"== no creds\naccount_id : 1234567\nauth : basic\ndry_run : True\n\n[dry-run] would call:\n GET https://dashboard.bandwidth.com/api/v2/accounts/1234567/sites\n GET https://messaging.bandwidth.com/api/v2/accounts/1234567/messages?limit=1\nexit=0\n== oauth dry\naccount_id : 1234567\nauth : oauth2\ndry_run : True\n\n[dry-run] would call:\n GET https://dashboard.bandwidth.com/api/v2/accounts/1234567/sites\n GET https://messaging.bandwidth.com/api/v2/accounts/1234567/messages?limit=1\n== media dry\n[dry-run] POST https://messaging.bandwidth.com/api/v2/accounts/1234567/messages\n body={'applicationId': '1111111-2222-3333-4444-555555555555', 'from': '+19195551234', 'to': ['+15551234567'], 'text': 'look', 'media': ['https://example.com/cat.png'], 'tag': 'demo'}\n== no filter\nERROR: Give at least one search filter, e.g. --area-code 919 or --state NC.\nexit=1\n","type":"text"}]}›Thinking…
The "no credentials" test loaded .env, so the fake credentials were still there — that's fine, dotenv's load reads .env. Not an actual problem.
Next, the README: contents to include
- What this is
- Prerequisites (Bandwidth account, credentials, a phone number associated with a messaging application; for US A2P SMS, 10DLC campaign registration is required)
- Setup instructions
- How to use each script
- Pricing (from bandwidth.com/pricing, retrieved today): Messaging: US 10DLC $0.004/SMS, $0.015/MMS; short code $0.008/$0.020; toll-free $0.007/$0.020 Voice: US local outbound $0.0100/min, inbound $0.0055/min, 6-second increments; recording $0.002/min; conference $0.0015/min; TTS standard $0.0007/100 chars; AMD $0.006/call; media streaming $0.0035–0.004/min Auth: 2FA $0.05/auth, call verification $0.15/call SIP trunking: custom quote Plus: monthly recurring fee for phone numbers + one-time order fee (quote/dashboard), taxes and regulatory recovery fees excluded, volume discounts and committed use available.
- Blockers: what I confirmed (without account credentials, both APIs return 401; no sandbox credentials in this environment). Also mention that a Bandwidth account requires signup/sales contact → can't be self-provisioned.
- Manageability assessment: is it easy for me (the agent) to manage?
Manageability assessment points: Good: plain REST + official SDKs, basic auth/OAuth, no local stack, idempotent-ish JSON, good error codes, message status queryable via API. Friction:
- Two SDKs with different styles (bandwidth-sdk is modern/openapi; bandwidth-numbers-sdk is legacy XML, apimatic, pins old dependencies like certifi==2019.11.28,
future) — the Node SDK is marked deprecated on npm. - Numbers API is XML-based; ordering requires pre-created sites/SIP peers, typically configured in the dashboard, not fully possible via API alone.
- Real money and telecom compliance gates: to send SMS in the US you need a provisioned number, an application, plus 10DLC brand/campaign registration (days-long manual review, trust score). Inbound requires a public HTTPS webhook URL — can't be run from a sandbox.
- No free sandbox/test credentials → can't verify end-to-end without a paid account. So: "Managing with an agent is moderately easy if credentials and a pre-registered number exist; the compliance/registration steps and dashboard-only configuration are the friction points."
Also mention that I couldn't verify things like price pagination. Keep the README concise.
Also note: the message status field is not in the generated models (additional_properties) — mention this in the README as a caveat.
Let's write the README.md.
✓write /sandbox/repo/README.md✓└{"content":[{"text":"Successfully wrote 6460 bytes to /sandbox/repo/README.md","type":"text"}]}
›Thinking…
Alright, final checks:
- .env is gitignored — should I delete my test .env (the one with fake credentials) or leave it? Since it's gitignored, leaving it as a starting point is fine, but BW_DRY_RUN=1 is set — that's safe. Keep it and mention it. Actually, would it be better to just leave .env.example and delete the test .env? Keeping a .env with fake credentials + dry-run could confuse the user when running live. But dry-run protects them. Hmm — cleaner approach: delete .env and have them copy from example. Though in that case check_setup would immediately fail with "no credentials" — which is the expected UX.
Delete .env.
- Verify that the scripts still work after deletion (should fail immediately with a clear message).
- Check the state of the repo, run a final quick lint (python -m compileall), and re-verify that the README's description about the
certifi==2019.11.28pinning is accurate (it was indeed listed in requires_dist). Yes, confirmed. - Also confirm the claim that "the Node SDK is deprecated on npm" — confirmed via npm's deprecation warning. No problem.
Additionally, re-check that the messaging list_messages call inside check_setup with the message_direction string works without a type error (it was masked by a 401). I can't verify the type conversion without credentials. The generated pydantic enum accepts strings ("OUTBOUND") for ListMessageDirectionEnum — openapi-generator's python typically deserializes but passes strings through as-is, serializing them to query params. The risk is low. Let me do a quick sanity check on whether param_serialize handles the enum string: just call list_messages with a fake config and catch the exception — I already confirmed the 401, meaning the request was constructed successfully (no validation error before sending). Yes — the earlier check_setup run reached a 401. That means serialization succeeded. Good evidence.
Let me wrap this up.
✓$ cd /sandbox/repo && rm -f .env && .venv/bin/python -m compileall -q . -x '\.venv' && echo "compile ok"; echo "== no .env"; BW_DRY_RUN=0 .venv/bin/python check_setup.py; echo "exit=$?"; ls -la; git sta…✓└{"content":[{"text":"compile ok\n== no .env\nERROR: BANDWIDTH_ACCOUNT_ID is not set. Copy .env.example to .env and fill it in.\nexit=1\ntotal 64\ndrwxr-xr-x 5 r…ctrl+o
cd /sandbox/repo && rm -f .env && .venv/bin/python -m compileall -q . -x '\.venv' && echo "compile ok"; echo "== no .env"; BW_DRY_RUN=0 .venv/bin/python check_setup.py; echo "exit=$?"; ls -la; git status --short
{"content":[{"text":"compile ok\n== no .env\nERROR: BANDWIDTH_ACCOUNT_ID is not set. Copy .env.example to .env and fill it in.\nexit=1\ntotal 64\ndrwxr-xr-x 5 root root 4096 Sep 29 08:25 .\ndrwxr-xr-x 8 root root 4096 Sep 29 08:17 ..\n-rw------- 1 root root 781 Sep 29 08:22 .env.example\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05 .git\n-rw-r--r-- 1 root root 424 Sep 29 08:24 .gitignore\ndrwx------ 5 root root 4096 Sep 29 08:18 .venv\n-rw------- 1 root root 6476 Sep 29 08:25 README.md\ndrwx------ 2 root root 4096 Sep 29 08:25 __pycache__\n-rw------- 1 root root 5119 Sep 29 08:24 bw_config.py\n-rw------- 1 root root 2113 Sep 29 08:24 check_setup.py\n-rw------- 1 root root 3316 Sep 29 08:24 order_number.py\n-rw------- 1 root root 265 Sep 29 08:22 requirements.txt\n-rw------- 1 root root 1935 Sep 29 08:23 search_numbers.py\n-rw------- 1 root root 3751 Sep 29 08:24 send_sms.py\n M .gitignore\n?? .env.example\n?? README.md\n?? bw_config.py\n?? check_setup.py\n?? order_number.py\n?? requirements.txt\n?? search_numbers.py\n?? send_sms.py\n","type":"text"}]}›Thinking…
Clean up pycache (is it covered by gitignore? .gitignore has __pycache__/ — yes). For tidiness, let's delete the pycache generated by compileall anyway.
Done. Time to summarize.
✓$ cd /sandbox/repo && rm -rf __pycache__ && git status --short && ls✓└{"content":[{"text":" M .gitignore\n?? .env.example\n?? README.md\n?? bw_config.py\n?? check_setup.py\n?? order_number.py\n?? requirements.txt\n?? search_number…ctrl+o
{"content":[{"text":" M .gitignore\n?? .env.example\n?? README.md\n?? bw_config.py\n?? check_setup.py\n?? order_number.py\n?? requirements.txt\n?? search_numbers.py\n?? send_sms.py\nREADME.md\nbw_config.py\ncheck_setup.py\norder_number.py\nrequirements.txt\nsearch_numbers.py\nsend_sms.py\n","type":"text"}]}›Thinking…
Done. Summary: what I built, pricing, ease of management, blockers. Concise but complete.
Built and verified a Bandwidth quickstart in /sandbox/repo (hosted APIs only — no local stacks, no long-running processes).
Files
bw_config.py— env loading, client factories for both official SDKs, error translationcheck_setup.py— validates creds against both APIs, lists sites/SIP peerssearch_numbers.py—GET /availableNumbers(Numbers API)order_number.py—POST /orders(buys a number; needs a site + SIP peer)send_sms.py—POST /messagesviabandwidth-sdk(--waitpolls status a few seconds)README.md,.env.example,requirements.txt;.envadded to.gitignore
Bandwidth ships two API surfaces, so two first-party SDKs: bandwidth-sdk 23.3.0 (Messaging/Voice/MFA, modern OpenAPI) and bandwidth-numbers-sdk 1.2.0 (Numbers, legacy XML). The Node SDKs are marked deprecated on npm — noted in the README.
Verified: all scripts compile and run; BW_DRY_RUN=1 prints the exact request instead of calling the API. Live runs reach Bandwidth and return a clean, translated error rather than a traceback.
Blocked: every live call returns 401 Unauthorized — there are no Bandwidth credentials/account id in this environment and Bandwidth has no anonymous sandbox, so I could only verify up to the auth boundary. I also can't self-provision an account. With valid creds the code should work unchanged; the next real-world gates are site/SIP peer existence, number↔application association, and 10DLC campaign approval (human-reviewed, days).
Pricing (published US list rates, excludes taxes/regulatory fees): SMS $0.004/msg on 10DLC, $0.007 toll-free, $0.008 short code; MMS $0.015/$0.020/$0.020. Voice $0.0100/min outbound, $0.0055/min inbound (6-second increments), recording $0.0020/min, conferencing $0.0015/min, AMD $0.0060/call, TTS $0.0007–$0.0030 per 100 chars, transcription ~$0.045/min. 2FA $0.05/auth, call verification $0.15/call. SIP trunking is quote-only. Numbers add a one-time order fee plus a monthly recurring fee per number (dashboard/quote, not public), with volume/committed-use discounts negotiated. No free tier.
Easy for me to manage? Moderately. Read-only ops (search numbers, list messages/status by id) are cheap and safe to automate with no webhook needed. Friction: two SDK eras (the Numbers SDK is XML with awkward field names and pins old deps), several prerequisites I can't create through code (site/SIP peer, application association, 10DLC registration), and every write bills real money with no sandbox to test against. Once someone hands me credentials plus one SMS-enabled, campaign-registered number, ongoing management is straightforward; getting the account to that state needs a human.
AX Fundamentals
23 checks. Open an area for details.
This check is a quick preview. Test more tasks and models with Gauge Agents.
Test your product with Gauge Agents