Unofficial Python SDK for the byteful Public User API
Project description
byteful-sdk
Unofficial — this project is a community-built client for byteful and is not affiliated with, endorsed by, or supported by byteful. All trademarks belong to their respective owners.
A typed Python SDK for the byteful Public User API.
Covers the full 50+ endpoint surface — proxies, proxy users, ACLs, services,
checkout, generated proxy lists (mobile/residential), availability,
analytics, logs, ledgers, geographic resources (country/city/subdivision/zip
code/continent/ASN), service adjustments, and more. Responses are parsed
into dataclasses with proper types (datetime, int, bool, enums) instead
of raw JSON.
Status: alpha (
0.1.0a1). The public API may change before0.1.0. See CHANGELOG.md for what shipped.
Install
uv add byteful-sdk
# or
pip install byteful-sdk
The distribution name is byteful-sdk; the import name is byteful:
from byteful import BytefulClient
Requires Python 3.13+.
Quick start
from byteful import BytefulClient, ProxyType
# Pass the keys explicitly...
with BytefulClient(
api_public_key="YOUR_PUBLIC_KEY",
api_private_key="YOUR_PRIVATE_KEY",
) as client:
...
# ...or set BYTEFUL_API_PUBLIC_KEY + BYTEFUL_API_PRIVATE_KEY in the
# environment and omit them.
with BytefulClient() as client:
me = client.customer_retrieve()
print(f"Hello {me.customer_first_name}, balance is {me.credit_balance}")
# Find available residential capacity in the US
avail = client.residential_availability_count(country_id="us")
print(avail.data)
# Buy a small ISP service
order = client.checkout_create(
product_type="isp",
product_protocol="ipv4",
country_id="us",
quantity=5,
cycle_interval="month",
cycle_interval_count=1,
)
print(order.data)
# List the proxies that just landed
for p in client.proxy_search(proxy_type=ProxyType.ISP, country_id="us"):
print(p.proxy_id, p.proxy_ip_address, p.proxy_http_port)
Authentication
The byteful API requires two header-based keys:
| Header | Where it comes from |
|---|---|
X-API-Public-Key |
Identifies your account |
X-API-Private-Key |
Verifies your identity — keep this secret |
The SDK reads them in this order:
- The
api_public_key=/api_private_key=constructor arguments - The
BYTEFUL_API_PUBLIC_KEY/BYTEFUL_API_PRIVATE_KEYenvironment variables
If either is missing, BytefulClient() raises ValueError. The SDK does
not load .env files itself; if you keep your keys in .env, use
python-dotenv:
from dotenv import load_dotenv
from byteful import BytefulClient
load_dotenv()
client = BytefulClient()
Generate keys at dashboard.byteful.com/developer/api-key.
API surface
Methods on BytefulClient mirror the URL path 1:1, so you can find any
documented endpoint by its URL:
| URL | Method |
|---|---|
GET /customer/retrieve |
customer_retrieve() |
GET /proxy/retrieve/{id} |
proxy_retrieve(proxy_id) |
GET /proxy/search |
proxy_search(...) |
GET /proxy/list_by_search |
proxy_list_by_search(...) |
POST /proxy/list_by_id |
proxy_list_by_id(proxy_ids, ...) |
POST /proxy/list/options |
proxy_list_options(...) |
GET /proxy_user/retrieve/{id} |
proxy_user_retrieve(id) |
GET /proxy_user/search |
proxy_user_search(...) |
POST /proxy_user/create |
proxy_user_create(...) |
PATCH /proxy_user/edit/{id} |
proxy_user_edit(id, ...) |
DELETE /proxy_user/delete/{id} |
proxy_user_delete(id) |
| `GET /proxy_user_acl/retrieve | search` |
POST /proxy_user_acl/create |
proxy_user_acl_create(...) |
DELETE /proxy_user_acl/delete/{id} |
proxy_user_acl_delete(id) |
| `GET /service/retrieve | search` |
PATCH /service/edit/{id} |
service_edit(id, ...) |
DELETE /service/cancel/{id} |
service_cancel(id, ...) |
| `GET /service_adjustment/retrieve | search` |
GET /checkout/catalog |
checkout_catalog() |
POST /checkout/quote |
checkout_quote(...) |
POST /checkout/create |
checkout_create(...) |
GET /mobile/list |
mobile_list(...) |
GET /mobile/summary |
mobile_summary() |
| `GET /mobile_availability/count | search` |
| `GET /mobile_ledger/retrieve | search` |
GET /residential/list |
residential_list(...) |
GET /residential/summary |
residential_summary() |
| `GET /residential_availability/count | search` |
| `GET /residential_ledger/retrieve | search` |
GET /product/search |
product_search(...) |
| `GET /analytics/breakdown | graph` |
| `GET /log/retrieve | search` |
| `GET /log_summary/retrieve | search` |
GET /proxy_test_server/search |
proxy_test_server_search(...) |
| `GET /country/retrieve | search` |
| `GET /city/retrieve | search` |
| `GET /subdivision/retrieve | search` |
| `GET /zip_code/retrieve | search` |
| `GET /continent/retrieve | search` |
| `GET /asn/retrieve | search` |
GET /ip_address/geolocate/{ip} |
ip_address_geolocate(ip) |
Convenience helpers built on top of proxy_search:
| Method | Purpose |
|---|---|
proxies(refresh?) |
Cached view of the full pool |
invalidate_proxy_cache() |
Drop the pool cache |
select_proxy(...) |
Pick one proxy matching filters, raises if none |
requests_session(...) |
One-shot: pick a proxy → requests.Session |
httpx_client(...) |
One-shot: pick a proxy → httpx.Client |
httpx_async_client(...) |
One-shot: pick a proxy → httpx.AsyncClient |
aiohttp_kwargs(...) |
One-shot: pick a proxy → kwargs for aiohttp |
Pagination
Search responses are paginated and wrapped in a PageResult:
page = client.proxy_search(per_page=100, page=1)
print(page.total_count, page.item_count, page.has_more)
for proxy in page:
print(proxy.proxy_id)
# Walk every page
all_proxies = []
page_n = 1
while True:
p = client.proxy_search(per_page=500, page=page_n)
all_proxies.extend(p.data)
if not p.has_more:
break
page_n = p.next_page
For the common "give me my pool, I'll filter locally" workflow there's
proxies(), which walks every page once and reuses the result:
pool = client.proxies() # one walk, every page
us_isp = pool.filter(country_id="us", proxy_type="isp")
proxy = us_isp.random()
client.proxies() # served from cache
- Default TTL is 24 hours. Override with
BytefulClient(proxy_cache_ttl=3600)(seconds) or passNoneto disable. - The cache is automatically invalidated after any state-changing call
(
checkout_*,proxy_user_*,proxy_user_acl_*,service_*). - Force a refresh with
client.proxies(refresh=True)or drop the cache explicitly withclient.invalidate_proxy_cache().
Using your proxies in an HTTP client
The SDK ships convenience helpers at two levels so you don't have to stitch
proxy URLs together by hand. byteful authenticates proxies with a
proxy user;
each Proxy carries the default proxy user credentials, so by default no
extra setup is needed.
One-liner on the client — picks a proxy from the cached pool
(auto-filtered to IN_USE by default) and hands back a ready-to-use client:
with BytefulClient() as c:
with c.requests_session(country_id="us") as s:
r = s.get("https://api.ipify.org?format=json", timeout=10)
print(r.json()["ip"])
Per-proxy — pick the proxy yourself, produce a client from it:
with BytefulClient() as c:
proxy = c.select_proxy(country_id="us", proxy_type="isp")
with proxy.requests_session() as s:
r = s.get("https://api.ipify.org?format=json", timeout=10)
Both forms share the cache.
Examples per library
requests
from byteful import BytefulClient
with BytefulClient() as c:
with c.requests_session(country_id="us") as s:
print(s.get("https://api.ipify.org?format=json", timeout=10).json()["ip"])
httpx (sync)
from byteful import BytefulClient
with BytefulClient() as c:
with c.httpx_client(country_id="us", timeout=10) as client:
print(client.get("https://api.ipify.org?format=json").json()["ip"])
httpx (async)
import asyncio
from byteful import BytefulClient
async def main() -> None:
with BytefulClient() as c:
async with c.httpx_async_client(country_id="us", timeout=10) as client:
r = await client.get("https://api.ipify.org?format=json")
print(r.json()["ip"])
asyncio.run(main())
aiohttp — aiohttp has no session-level proxy setting:
import asyncio
import aiohttp
from byteful import BytefulClient
async def main() -> None:
with BytefulClient() as c:
kw = c.aiohttp_kwargs(country_id="us") # {"proxy": "http://u:p@host:port"}
async with aiohttp.ClientSession() as s:
async with s.get("https://api.ipify.org?format=json", **kw) as r:
print((await r.json())["ip"])
asyncio.run(main())
subprocess / curl / wget — env-var format for shelling out:
import os
import subprocess
from byteful import BytefulClient
with BytefulClient() as c:
proxy = c.select_proxy(country_id="us")
subprocess.run(
["curl", "https://api.ipify.org"],
env={**os.environ, **proxy.as_env()},
)
Using a custom proxy user
Pass a ProxyUser (or a raw (username, password) tuple) to any of the
URL builders to authenticate as that user instead of the proxy's default:
pu = client.proxy_user_create(proxy_user_access_type="all").data
url = proxy.auth_url(pu, protocol="http", family="v4")
Just give me the URL / dict — if you're plugging into something else:
proxy.http_url() # "http://user:pass@host:8080"
proxy.socks5_url() # "socks5://user:pass@host:1080"
proxy.auth_url(protocol="socks5") # same as socks5_url
proxy.as_requests_dict() # {"http": url, "https": url}
proxy.aiohttp_kwargs() # {"proxy": url}
httpx and aiohttp are imported lazily — they only need to be installed
if you actually call the corresponding helper.
Generated proxy lists
For mobile and residential traffic (and for "give me a chunk of formatted strings" workflows on ISP/datacenter), use the generated-list endpoints:
# 1000 sticky US residential proxies, formatted ip:port:user:pass
batch = client.residential_list(
country_id="us",
list_count=1000,
list_format="standard",
list_session_type="sticky",
)
for line in batch:
print(line)
Same shape for mobile_list(), proxy_list_by_search() (filter by location
or service) and proxy_list_by_id() (specific UUIDs).
Enums
from byteful import (
ProxyType, # DATACENTER / ISP / RESIDENTIAL / MOBILE
ProxyProtocol, # IPV4 / IPV6 / DUAL
ProxyStatus, # AVAILABLE / IN_USE / RESERVED / WAITING / PENDING_DELETION
ServiceStatus, # ACTIVE / CANCELED / AWAITING_FULFILLMENT / ...
ServiceType, # DATACENTER / ISP / RESIDENTIAL / MOBILE / OFF_CATALOG
CycleInterval, # YEAR / MONTH / WEEK / DAY
ListFormat, # STANDARD / HTTP / HTTPS / SOCKS5 / SOCKS5H
ListSessionType, # STICKY / ROTATING
ListMode, # GENERAL / SIZE / SPEED
ListAuthentication,# USERNAME_AND_PASSWORD / IP_ADDRESS / PROXY_SPECIFIC
CancelFeedback, # TOO_EXPENSIVE / SWITCHED_SERVICE / ...
)
Unknown values from the server are passed through as raw strings rather than raising, so a newly-added enum variant doesn't break the SDK.
Errors
The API uses an envelope of {"error":"...","message":"...","api_request_id":"..."}
on non-2xx HTTP responses. Each documented HTTP status code has its own
exception subclass:
from byteful import (
BytefulAPIError,
BadRequestError,
UnauthorizedError,
ForbiddenError,
TwoFactorAuthenticationRequired,
NotFoundError,
MethodNotAllowedError,
ConflictError,
UnprocessableError,
RateLimitedError,
InternalServerError,
)
try:
client.checkout_create(product_type="isp", country_id="us", quantity=5)
except UnauthorizedError:
rotate_keys()
except TwoFactorAuthenticationRequired as e:
prompt_for_2fa(e.two_factor_authentication_target)
except UnprocessableError as e:
log.warning("byteful rejected the request: %s (req %s)", e.message, e.api_request_id)
except BytefulAPIError as e:
log.error("byteful %s: %s", e.status_code, e.message)
| Code | Exception | Meaning |
|---|---|---|
| 400 | BadRequestError |
Invalid or improperly formatted request |
| 401 | UnauthorizedError |
Missing or invalid auth credentials |
| 403 | ForbiddenError |
Authenticated but lacks permission |
| 403 | TwoFactorAuthenticationRequired |
2FA challenge issued (2FA fields populated) |
| 404 | NotFoundError |
Resource does not exist |
| 405 | MethodNotAllowedError |
HTTP method not supported on the URL |
| 409 | ConflictError |
Conflicts with current state |
| 422 | UnprocessableError |
Failed business-logic validation |
| 429 | RateLimitedError |
Rate limit exceeded |
| 500 | InternalServerError |
Server-side fault |
The raw documented messages are also exposed as the byteful.ERROR_CODES
dict.
Rate limiting
The API allows 10 requests per second per customer; over that limit it
returns HTTP 429. The SDK ships with a thread-safe sliding-window limiter
that all BytefulClient instances share by default, so you don't have to
think about it — concurrent calls just block long enough to stay under the
cap.
from byteful import BytefulClient, RateLimiter
# Default — uses the process-wide DEFAULT_RATE_LIMITER (10/s).
client = BytefulClient()
# Custom limit (e.g. you have a higher-tier agreement).
client = BytefulClient(rate_limiter=RateLimiter(max_requests=30, period=1.0))
# Disable entirely (you're handling throttling yourself).
client = BytefulClient(rate_limiter=None)
Verifying a proxy against a live IP-check service
ProxyVerifier routes a request through a proxy and asks a public
"what's my IP" service what it sees, so you can confirm the proxy is
egressing as expected and your real IP isn't leaking. By default it walks
through four built-in providers in order and returns the first success,
so one provider being down or blocking your IP doesn't break the check.
from byteful import BytefulClient, ProxyVerifier
with BytefulClient() as c:
proxy = c.proxies().filter(country_id="us").random()
with ProxyVerifier() as v:
leak = v.check_leak(proxy)
print(leak.result.ip, leak.result.country, leak.leaked, leak.result.provider)
check_leak() returns a LeakCheck whose leaked property is True
whenever the IP the service saw differs from the proxy's expected egress
(proxy.proxy_ip_address for IPv4 / proxy.proxy_ip_address_v6 for IPv6).
Built-in providers (the default chain, in order):
| Provider | Endpoint | Data returned |
|---|---|---|
IpifyProvider |
api.ipify.org / api6.ipify.org |
IP only — smallest moving part |
IcanhazipProvider |
ipv4.icanhazip.com / ipv6.icanhazip.com |
IP only, plain text |
IfconfigCoProvider |
ifconfig.co/json |
IP + country + region + city + ASN |
IpinfoIoProvider (IPv4 only) |
ipinfo.io/json |
IP + country + region + city + ASN |
Pin a single provider (no fallback) or control the chain explicitly:
# Single provider — failures raise instead of falling back.
ProxyVerifier(provider=IpifyProvider()).check(proxy)
# Custom fallback chain, tried in order.
ProxyVerifier(providers=[my_checker, IpifyProvider(), IcanhazipProvider()]).check(proxy)
Using a custom session
Inject any requests.Session (e.g. with retry policies or a proxy of your
own):
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=Retry(
total=5, backoff_factor=0.5, status_forcelist=(429, 500, 502, 503, 504)
)))
client = BytefulClient(session=session)
Development
uv sync
uv run pytest # unit tests only (integration is gated)
uv run pytest -m integration # hit the real API; needs the BYTEFUL_API_* keys
Put your keys in a local .env; tests/conftest.py loads it automatically.
User-visible changes are tracked in CHANGELOG.md — add an
entry under [Unreleased] whenever you ship something that affects the
public API.
License
MIT — see LICENSE. This is an unofficial project and ships with no warranty of fitness for any particular purpose.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file byteful_sdk-0.1.0a1.tar.gz.
File metadata
- Download URL: byteful_sdk-0.1.0a1.tar.gz
- Upload date:
- Size: 38.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a40fb04a4abf5ffdd2f33378dda025b378afa3589e89668d2ca7341c34b1ff92
|
|
| MD5 |
b6b1963913502dd4b8ddc31a6c31fc49
|
|
| BLAKE2b-256 |
b86325e416d217f844bcdc82ef11bf7c53cbf4062b891667141737f189c9d098
|
Provenance
The following attestation bundles were made for byteful_sdk-0.1.0a1.tar.gz:
Publisher:
release.yml on AsyncAlchemist/byteful-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
byteful_sdk-0.1.0a1.tar.gz -
Subject digest:
a40fb04a4abf5ffdd2f33378dda025b378afa3589e89668d2ca7341c34b1ff92 - Sigstore transparency entry: 1871491823
- Sigstore integration time:
-
Permalink:
AsyncAlchemist/byteful-sdk@a7364a7f45877f08dfc4fccf382796ee0d269d54 -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/AsyncAlchemist
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a7364a7f45877f08dfc4fccf382796ee0d269d54 -
Trigger Event:
release
-
Statement type:
File details
Details for the file byteful_sdk-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: byteful_sdk-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0888b8187aabe063c08d2d9bc65730323f885f855981efa0fb5369e3b79147dd
|
|
| MD5 |
89dd82c86d6a4238db81ad10d0852e56
|
|
| BLAKE2b-256 |
26e3ec8d3b4cb78bd4244c7f3b868c59d10a1765ef38c7531dcc2faf4da1f2a1
|
Provenance
The following attestation bundles were made for byteful_sdk-0.1.0a1-py3-none-any.whl:
Publisher:
release.yml on AsyncAlchemist/byteful-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
byteful_sdk-0.1.0a1-py3-none-any.whl -
Subject digest:
0888b8187aabe063c08d2d9bc65730323f885f855981efa0fb5369e3b79147dd - Sigstore transparency entry: 1871491841
- Sigstore integration time:
-
Permalink:
AsyncAlchemist/byteful-sdk@a7364a7f45877f08dfc4fccf382796ee0d269d54 -
Branch / Tag:
refs/tags/v0.1.0a1 - Owner: https://github.com/AsyncAlchemist
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a7364a7f45877f08dfc4fccf382796ee0d269d54 -
Trigger Event:
release
-
Statement type: