Skip to main content

bytekit-sdk

Official Python SDK for the ByteKit API.

The PyPI distribution is bytekit-sdk; the import name is bytekit.

Generated from the OpenAPI spec using openapi-python-client, filtered to the stable v0.1 operations.

Installation

pip install bytekit-sdk

The installed version is available as bytekit.__version__.

Quick start

from bytekit import AuthenticatedClient
from bytekit.api.scrape import create_scrape
from bytekit.models.scrape_request import ScrapeRequest
from bytekit.models.scrape_request_formats_item import ScrapeRequestFormatsItem
from bytekit.models.scrape_success_envelope import ScrapeSuccessEnvelope

# base_url defaults to https://api.bytekit.com, so only the token is required.
client = AuthenticatedClient(token="sk_live_your_api_key_here")

result = create_scrape.sync(
    client=client,
    body=ScrapeRequest(
        url="https://example.com",
        # Ask for markdown explicitly. `formats` is a list of enum members, not plain
        # strings. Omit it and the server returns raw HTML instead, leaving
        # `formats.markdown` unset.
        formats=[ScrapeRequestFormatsItem.MARKDOWN],
    ),
)

if isinstance(result, ScrapeSuccessEnvelope):
    print(result.formats.markdown)

See Error handling for what result can be when the request fails.

Error handling

The SDK has a two-tier error contract. Both tiers are safe: an operation never raises a bare json.JSONDecodeError or a bare KeyError, even when a server returns an HTML error page or a payload whose shape has drifted from the spec.

Situation What you get
A status the OpenAPI spec documents for that operation (e.g. a 422 or 500 on create_scrape), with a JSON body of the documented shape The typed Error model is returned, not raised. Check with isinstance(result, Error) and read result.error.code / result.error.message.
An undocumented status, or a non-JSON body on a documented status (HTML error page, load-balancer text, empty body) errors.UnexpectedStatus is raised, carrying .status_code and the raw .content. When the body is the documented Error envelope, .code / .message are populated too.
A documented status whose JSON body does not match the documented schema — a field the server renamed, dropped or retyped errors.UnexpectedStatus is raised, same shape as the row above. The underlying parse failure (e.g. KeyError: 'schema_version') is attached as __cause__ for diagnosis.
A 200 from create_scrape carrying a status: "failed" envelope — a terminal upstream failure (unreachable host, refused connection, bot protection that defeats every proxy tier) errors.UnexpectedStatus is raised, with .status_code 200 and .code / .message read off the envelope's error. get_scrape is unchanged: polling a job that failed still returns the typed ScrapeErrorEnvelope.
Any of the above, with raise_on_unexpected_status=False Nothing is raised; the operation returns None.

A failed scrape is a result, not a transport error, so the API reports it on a 200 rather than a 5xx — but a returned failure is exactly the shape a first call misreads as success (result.formats on a ScrapeErrorEnvelope is an AttributeError). create_scrape therefore raises it. The carve-out is that one call: the identical envelope from get_scrape is the correct answer to "how did that job end?" and is still returned.

The table describes the generated operations (create_scrape.sync, get_usage.sync, …), whose return types are Optional[...] accordingly. The hand-written AuthenticatedClient.search(...) convenience method is deliberately outside it: it returns a non-Optional CreateSearchResponse200 and raises errors.UnexpectedStatus on anything else — including documented error statuses, and including a malformed 200 — in both raise modes. raise_on_unexpected_status does not apply to it.

from bytekit import AuthenticatedClient
from bytekit.api.scrape import create_scrape
from bytekit.errors import UnexpectedStatus
from bytekit.models.error import Error
from bytekit.models.scrape_request import ScrapeRequest
from bytekit.models.scrape_request_formats_item import ScrapeRequestFormatsItem
from bytekit.models.scrape_success_envelope import ScrapeSuccessEnvelope

client = AuthenticatedClient(token="sk_live_your_api_key_here")

try:
    result = create_scrape.sync(
        client=client,
        body=ScrapeRequest(
            url="https://example.com",
            # Ask for markdown explicitly — omit `formats` and the server returns raw HTML,
            # leaving `formats.markdown` unset.
            formats=[ScrapeRequestFormatsItem.MARKDOWN],
        ),
    )
except UnexpectedStatus as err:
    # Undocumented status, a non-JSON body on a documented one, or a 200 whose envelope
    # says the scrape itself failed. Never a JSONDecodeError.
    print(f"request failed with status {err.status_code}: {err.code} {err.message}")
else:
    if isinstance(result, Error):
        # Documented 4xx/5xx with a JSON body: RETURNED as a typed model, not raised.
        print(f"api error {result.error.code}: {result.error.message}")
    elif isinstance(result, ScrapeSuccessEnvelope):
        print(result.formats.markdown)
    else:
        # 202 ScrapeQueuedEnvelope — poll get_scrape with this id until it completes.
        print(f"queued as {result.id}")

Transport errors

Both tiers above describe what happens once the server has answered. When it never does, the underlying httpx exception propagates unchanged — the SDK catches neither, on every generated operation and on search(...) alike:

Exception When
httpx.TimeoutException The request was sent and did not finish within the client's timeout (120s by default).
httpx.ConnectError The connection was never established: DNS failure, connection refused, TLS handshake failure.

raise_on_unexpected_status does not apply to either — it governs how a response is handled, and there is no response in these cases. Catch them alongside errors.UnexpectedStatus:

except (UnexpectedStatus, httpx.TimeoutException, httpx.ConnectError) as err: ...

Defaults

The client ships with production-ready defaults so AuthenticatedClient(token=...) works out of the box:

Setting Default Notes
base_url https://api.bytekit.com Pass base_url= to target staging or a proxy.
timeout 120s (httpx.Timeout(120.0)) Finite by default — requests no longer hang indefinitely. Pass timeout= to override.
raise_on_unexpected_status True Undocumented statuses raise errors.UnexpectedStatus instead of silently returning None. Pass raise_on_unexpected_status=False to restore the old opt-out.

Explicit constructor arguments always win over these defaults (explicit arg > default).

Every constructor argument is keyword-only

AuthenticatedClient accepts no positional arguments. token, base_url, prefix, auth_header_name and the rest are all passed by name:

client = AuthenticatedClient(base_url="https://api.bytekit.com", token="sk_live_your_api_key_here")

Migrating from a pre-0.3.0 positional call. base_url used to be the first positional argument, so AuthenticatedClient("https://…", "sk_live_…") was a documented call. Once base_url became a defaulted keyword argument, that same call silently bound the URL to token and the API key to prefix — producing an Authorization: sk_live_… https://… header sent to the default host, i.e. a wrong credential against production with nothing raised. Since 0.3.5 it raises TypeError instead. Add the keywords; nothing else changes.

Async usage

import asyncio
from bytekit import AuthenticatedClient
from bytekit.api.screenshots import create_screenshot
from bytekit.models.screenshot_request import ScreenshotRequest

async def main():
    client = AuthenticatedClient(token="sk_live_your_api_key_here")
    body = ScreenshotRequest(url="https://example.com")
    response = await create_screenshot.asyncio(client=client, body=body)
    print(response)

asyncio.run(main())

Clients and event loops

An httpx.AsyncClient — and therefore its connection pool — belongs to the event loop that created it. The recommended shape is a context manager, which scopes the client to exactly one loop:

async def main():
    async with AuthenticatedClient(token="sk_live_your_api_key_here") as client:
        ...

Two rules cover everything else:

  • A client the SDK builds for you is rebuilt automatically. If you reuse one client across several asyncio.run(...) calls, the SDK notices the running loop has changed and transparently replaces its internal AsyncClient. Your base_url, headers, timeout, httpx_args and authentication are all re-applied, so this is invisible apart from a new connection. Calling get_async_httpx_client() outside any running loop returns the cached client unchanged.
  • A client you pass to set_async_httpx_client(...) is yours. The SDK never rebuilds or closes it, so a client you supply must be created and used on the same loop — that is the one case where crossing loops is still your responsibility.

Available operations

Module Method Description
api.scrape create_scrape, get_scrape Web content extraction
api.screenshots create_screenshot, get_screenshot Page screenshots
api.bulk create_bulk, get_bulk, delete_bulk, list_bulk_screenshots Bulk screenshot jobs
api.scrape_bulk create_scrape_bulk, get_scrape_bulk Bulk scrape jobs
api.fetch get_fetch, post_fetch Raw HTTP fetch
api.fetch_bulk create_fetch_bulk, get_fetch_bulk Bulk fetch jobs
api.monitors create_monitor, list_monitors, get_monitor, update_monitor, delete_monitor, list_monitor_captures Page-change monitors (screenshot + scrape)
api.sitemap create_sitemap, get_sitemap Sitemap crawl
api.search create_search (or the synchronous AuthenticatedClient.search(...); use create_search.asyncio in async code) Web search
api.usage get_usage, get_usage_daily, get_usage_by_endpoint Account usage & billing
api.webhooks list_webhook_deliveries, retry_webhook_delivery Webhook delivery log & retry
api.account get_account Account details

Two things worth knowing before your first call

get_fetch takes url_query=, not url=. The URL to fetch is passed as url_query; it travels as the url query parameter on the wire. The rename comes from the generator, which avoids reusing url for an operation argument. get_fetch.sync(client=client, url="…") is a TypeError:

from bytekit import AuthenticatedClient
from bytekit.api.fetch import get_fetch

client = AuthenticatedClient(token="sk_live_your_api_key_here")
result = get_fetch.sync(client=client, url_query="https://example.com")

AuthenticatedClient.search(...) is synchronous. Calling it from inside a coroutine blocks the event loop for the duration of the request — nothing raises, so it is easy to miss. In async code use the generated operation instead, which is a real coroutine:

from bytekit.api.search import create_search
from bytekit.models.create_search_body import CreateSearchBody

response = await create_search.asyncio(client=client, body=CreateSearchBody(query="climate tech"))

The convenience method's extra behavior — raising errors.UnexpectedStatus on every non-200 rather than returning a typed Error — is its own, and create_search.asyncio follows the two-tier contract described under Error handling like every other generated operation.

License

MIT — see LICENSE.

Release files for bytekit-sdk 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bytekit-sdk 0.7.0
File Size Uploaded
bytekit_sdk-0.7.0.tar.gz 142.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bytekit-sdk 0.7.0
File Interpreter ABI Platform
bytekit_sdk-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 415.9 kB

Release files / bytekit_sdk-0.7.0.tar.gz

Download URL bytekit_sdk-0.7.0.tar.gz
Size 142.9 kB
Tags Source
SHA-256 checksum
How to use checksums
b5667a558445008ca36bbb068b7afa4ece5ac727cd587f13c8ea91997d675898
BLAKE2b-256 checksum
How to use checksums
2ad84206fc36b14a7874ccd42fcb9e4807ac936093bbbd7a747fa44ad4f22d24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / bytekit_sdk-0.7.0-py3-none-any.whl

Download URL bytekit_sdk-0.7.0-py3-none-any.whl
Size 273.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
08338ef8157b5805f032f2ce2436e376f87f8009e7641ba17268da28796a47ad
BLAKE2b-256 checksum
How to use checksums
9c0c226401668cb998e2b69e48e56f95a0e955a45f8c861438be7557bb747c66
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

0.9.0

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

This release

0.7.0 This release

2 release files

0.6.0

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.2

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page