Skip to main content

Unofficial Python SDK for the byteful Public User API

Project description

byteful-sdk

CI codecov PyPI version Python 3.13+ License: MIT

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 before 0.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:

  1. The api_public_key= / api_private_key= constructor arguments
  2. The BYTEFUL_API_PUBLIC_KEY / BYTEFUL_API_PRIVATE_KEY environment 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 pass None to 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 with client.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())

aiohttpaiohttp 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

byteful_sdk-0.1.0a1.tar.gz (38.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

byteful_sdk-0.1.0a1-py3-none-any.whl (40.7 kB view details)

Uploaded Python 3

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

Hashes for byteful_sdk-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 a40fb04a4abf5ffdd2f33378dda025b378afa3589e89668d2ca7341c34b1ff92
MD5 b6b1963913502dd4b8ddc31a6c31fc49
BLAKE2b-256 b86325e416d217f844bcdc82ef11bf7c53cbf4062b891667141737f189c9d098

See more details on using hashes here.

Provenance

The following attestation bundles were made for byteful_sdk-0.1.0a1.tar.gz:

Publisher: release.yml on AsyncAlchemist/byteful-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for byteful_sdk-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 0888b8187aabe063c08d2d9bc65730323f885f855981efa0fb5369e3b79147dd
MD5 89dd82c86d6a4238db81ad10d0852e56
BLAKE2b-256 26e3ec8d3b4cb78bd4244c7f3b868c59d10a1765ef38c7531dcc2faf4da1f2a1

See more details on using hashes here.

Provenance

The following attestation bundles were made for byteful_sdk-0.1.0a1-py3-none-any.whl:

Publisher: release.yml on AsyncAlchemist/byteful-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page