Skip to main content

scribe-client (Python)

Python bindings for the Scribe document conversion API. This package is a thin, synchronous wrapper (via PyO3) around the Rust scribe-client crate, so it behaves the same as the Rust client.

  • Synchronous by design. Every method blocks until the request completes; there's no asyncio API. Under the hood each call runs on a shared Tokio runtime with the GIL released, so other Python threads keep running while a request is in flight.
  • Auto-refreshing tokens. ScribeClient holds a TokenSet and refreshes it automatically when it's missing or about to expire, and again if a request unexpectedly comes back 401.

Installing

pip install scribe-client

Wheels are built for Linux, macOS and Windows, for Python 3.9 and later.

To build it from source instead, with maturin:

cd crates/scribe-client-py
uv venv                      # or: python -m venv .venv
source .venv/bin/activate
uv pip install maturin
maturin develop              # builds the extension and installs it into the venv

Authenticating

The server uses OAuth 2.0 Authorization Code + PKCE. There's no password or client-credentials grant, so a human has to approve access once via a browser; after that, ScribeClient handles refreshing the token for you.

from scribe_client import AuthClient, PkceChallenge, ScribeClient

auth = AuthClient(base_url, client_id)
pkce = PkceChallenge()

# Send the user to this URL. After they log in and approve, they're
# redirected to redirect_uri with a `code` query parameter.
url = auth.authorization_url(redirect_uri, pkce)

# Exchange the code (and the verifier from the *same* PkceChallenge) for a
# token set.
tokens = auth.exchange_code(redirect_uri, code, pkce.verifier)

client = ScribeClient(base_url, client_id, tokens)

tokens.access_token, tokens.refresh_token, and tokens.expires_at (Unix seconds, or None) are readable if you want to persist them between runs — reconstruct a TokenSet later with TokenSet(access_token, refresh_token, expires_at).

AuthClient doesn't open a browser or listen for the redirect; getting the code back is up to your application. The server never shows the code on a page — it only ever redirects to redirect_uri — so something has to be listening there. For a desktop app or script, register a loopback URI with a fixed port, such as http://127.0.0.1:8765/callback, and run a small HTTP server on that address while the user signs in. See the top-level README for other kinds of app.

See examples/pkce_flow.py for a runnable end-to-end version of this, including creating a document and converting it. Given a loopback SCRIBE_REDIRECT_URI, it opens the browser and catches the redirect itself:

export SCRIBE_BASE_URL=https://scribeit.io
export SCRIBE_CLIENT_ID=your-client-id
export SCRIBE_REDIRECT_URI=http://127.0.0.1:8765/callback  # as registered for your app
export SCRIBE_DOCUMENT_PATH=report.docx
uv run --project crates/scribe-client-py crates/scribe-client-py/examples/pkce_flow.py

With any other redirect URI, the browser ends up on a page that fails to load after you approve access. That's expected: copy the code parameter from the address bar and paste it into the script. A code works only once and expires after a few minutes.

Documents

# From a local file...
document_id = client.create_document_from_file("report.docx", data)
# ...or have the server fetch it from a URL.
document_id = client.create_document_from_url("https://example.com/report.pdf")
# Both take an optional password, for a file you already know is protected.
document_id = client.create_document_from_file("locked.pdf", data, "hunter2")

Creating a document automatically starts an html_stream conversion (a fast preview), so the caller has something to show immediately even if the account is out of page credits for a full conversion.

result = client.list_documents()
print(result.pages_remaining)  # None if the account isn't page-metered
for doc in result.documents:
    # doc.title and doc.page_count are None until the server has
    # determined them (e.g. briefly, for a URL-sourced document).
    print(doc.id, doc.title, doc.page_count, doc.inserted_at)
    for output in doc.outputs:
        print(" ", output.format, output.stage, output.progress)

client.delete_document(document_id)

list_outputs(document_id) returns an OutputList for one document — .outputs, the same rows list_documents() embeds per-document without the document metadata, plus .is_password_needed; get_settings/update_settings read and partially update a document's conversion settings (language, TTS voice, Braille table, and so on — see Settings's attributes for the full set).

settings = client.get_settings(document_id)
client.update_settings(document_id, {"large_print": True})

update_settings takes a plain dict; only the keys you pass are changed server-side.

Converting to another format

Creating a document starts only the html_stream conversion. Converting to any other format — pdf, epub, daisy, docx, brf, mp3, offline_html, mobi — happens over a real-time channel, not a REST call, so the server can guarantee it's subscribed to progress on whatever it just started converting.

The server runs one conversion of a document at a time, and start_conversion raises ConversionInProgressError while another is running. That includes the html_stream conversion that creating the document started, unless it turned out to be a preview, so a call straight after creating a document will normally raise it. Wait for that conversion's "conversion_complete" event, then call start_conversion again:

channel = client.open_document_channel(document_id)
try:
    while True:
        try:
            output_id = channel.start_conversion("pdf")
            break
        except ConversionInProgressError:
            while True:
                event = channel.next_event()
                if event["type"] == "conversion_complete":
                    break
                if event["type"] == "error":
                    raise RuntimeError(event["reason"])

    while True:
        event = channel.next_event()

        if event["type"] == "status":
            print(f"{event['format']}: {event['stage']} {event['progress']:.0%}")
        elif event["type"] == "conversion_complete" and event["format"] == "pdf":
            data = client.download_output(document_id, "pdf")
            break
        elif event["type"] == "error":
            raise RuntimeError(event["reason"])
finally:
    channel.close()

Calling start_conversion for a format that's already complete just returns that output's existing id without converting it again. Calling it for a format that's still converting raises ConversionInProgressError, like any other running conversion.

next_event() blocks until the next event arrives and returns it as a dict tagged by event["type"]. It sends heartbeats while it waits, which is what keeps the connection open during a long conversion, so keep calling it rather than doing other long work between calls. The server closes a connection it hasn't heard from for about a minute. The connection can still be lost, for example to a network failure, which raises ScribeApiError. The conversion carries on without it: open a new channel on the same document, and don't upload it again.

type other keys meaning
"status" format, stage, progress a conversion's stage or progress changed
"chunk" content a chunk of streamed HTML (html_stream only)
"conversion_complete" format, output_id a format finished converting
"error" reason an out-of-band error unrelated to a specific call
"password_required" — the document is protected and hasn't been unlocked

stage is one of "queue", "start", "convert", "add_image_descriptions", or "complete".

Joining the channel also delivers events for any formats that were already converting (or complete) when you joined — including incomplete parent formats. (Most formats are rendered from the html output, and daisy is additionally assembled from mp3; the server subscribes you to those automatically when they're not finished yet.) Filter on event["format"] if you only care about one format at a time, as in the example above.

Password-protected documents

A protected PDF can't be converted until its password is supplied. The server reports this in three places: password_required is pushed as soon as you join the channel, start_conversion raises PasswordRequiredError rather than queueing work that can only fail, and is_password_needed is set on the document in list_documents() and list_outputs().

Pass the password to start_conversion to unlock and convert in one call. It's stored against the document, so converting it to further formats later doesn't need it again.

try:
    output_id = channel.start_conversion("pdf")
except PasswordRequiredError:
    output_id = channel.start_conversion("pdf", prompt_for_password())

A wrong password isn't rejected here — the server accepts it, the converter fails on it, and an "error" event with reason "invalid_password" arrives from next_event(). Prompt again and call start_conversion again. A locked document has no outputs at all, so there's nothing to clean up between attempts.

Always close() the channel when you're done with it (a try/finally is the easiest way, as above) — it holds an open WebSocket connection.

Errors

All exceptions are subclasses of ScribeApiError:

Exception Raised when
NotFoundError the document (or channel) doesn't exist
ForbiddenError the document exists but isn't owned by the current user
ConversionNotCompleteError download_output was called before that format finished converting
ConversionInProgressError start_conversion was called while a conversion of the document is running
RateLimitedError start_conversion was called too many times too quickly
PasswordRequiredError the document is a protected file and no password has been supplied
NeedsPurchaseError the account doesn't have enough page credits for the requested conversion
InvalidGrantError an OAuth code, refresh token, or PKCE verifier didn't check out
ScribeApiError (base) anything else the server rejected, or a connection-level failure
from scribe_client import NotFoundError

try:
    client.delete_document(document_id)
except NotFoundError:
    print("already gone")

Running the tests

cd crates/scribe-client-py
maturin develop
pytest

Tests don't hit a real server: tests/conftest.py runs a small dependency-free HTTP server (stdlib http.server) for REST calls, and a hand-rolled WebSocket server (stdlib socket, doing the RFC 6455 handshake itself) for exercising DocumentChannel.

Metadata

Release files for scribe-client 0.2.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 scribe-client 0.2.0
File Size Uploaded Provenance Actions
scribe_client-0.2.0.tar.gz 89.1 kB With provenance Details for scribe_client-0.2.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for scribe-client 0.2.0
File Actions
scribe_client-0.2.0-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details for scribe_client-0.2.0-cp39-abi3-win_amd64.whl
scribe_client-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl CPython 3.9 abi3 Linux musl 1.2+ x86-64 Details for scribe_client-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl
scribe_client-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl CPython 3.9 abi3 Linux musl 1.2+ ARM64 Details for scribe_client-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl
scribe_client-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details for scribe_client-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
scribe_client-0.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details for scribe_client-0.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
scribe_client-0.2.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details for scribe_client-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
scribe_client-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details for scribe_client-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl

Total release size: 22.8 MB

Release files / scribe_client-0.2.0.tar.gz

Download URL scribe_client-0.2.0.tar.gz
Size 89.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f40488b96ec2fb00ee577b8eb80d512682723b458a190cfa6444942cdb321e19
BLAKE2b-256 checksum
How to use checksums
a2f26965d84f5fee79f0ae272b3e6acba7115b7eccf0f5a0c37feef767ac424f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / scribe_client-0.2.0-cp39-abi3-win_amd64.whl

Download URL scribe_client-0.2.0-cp39-abi3-win_amd64.whl
Size 2.7 MB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
2127eb6d8ad22f7bd54fd45e890db8b30128f8a0e774c16556288f94612a7770
BLAKE2b-256 checksum
How to use checksums
a92439572ec9f667556c7a3da0389e907138fdd85dbfe4d3a9b0abdd1697cc48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / scribe_client-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl

Download URL scribe_client-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl
Size 3.6 MB
Tags CPython 3.9 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
fad7fea31e48ca2b46f0f40c3b08c9d58a786c04e3600aa5323ad9eeb9c5443a
BLAKE2b-256 checksum
How to use checksums
f2e8e8e2c4cc672a90c4e28c5087b90304722399e5f86af3d3cc0432618ff684
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / scribe_client-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl

Download URL scribe_client-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl
Size 3.5 MB
Tags CPython 3.9 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
8356fff61fa50f86e5e7aadc7a0c492f4244f037903bb518992c2ecf43bd3f04
BLAKE2b-256 checksum
How to use checksums
a9e5661fc4de91a5a6bbf12fd74a9d0866e3aaa0c8c0f482b1717ff4a26007da
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / scribe_client-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl

Download URL scribe_client-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Size 3.4 MB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
944caf747833b4cc3e690bd218153e4bd35008573a9d8d2f20d3beac7a7e81c9
BLAKE2b-256 checksum
How to use checksums
3e0dd2f128017958ef2299c300d48777cf9b8687a3407b53a6d14cccb1142fbc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / scribe_client-0.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl

Download URL scribe_client-0.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Size 3.3 MB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
6fce475e8661d5b25e11848de69b0b78ac4ccd6f174932b153c395dd5cbdea70
BLAKE2b-256 checksum
How to use checksums
6e6075efdd3370abf1626e076d540b701b0c3fe20a9ea56deaeef6bac3f28bdd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / scribe_client-0.2.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL scribe_client-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
Size 3.0 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
a0970e3691a2de005b59adb7c4c11853888285bb75f741998b480c92f2ac7894
BLAKE2b-256 checksum
How to use checksums
177326a6239974455f473b8f99fc318eaf8cee487841a793dc94d08ed3b2a1d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / scribe_client-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL scribe_client-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 3.2 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
136861e1cbafb2dfb0ec9301e5415f4bd5e3b871ce4d7a395e6a36ee2828b3ad
BLAKE2b-256 checksum
How to use checksums
e238dd18e53fae9e9bc6163028db405dd7959c6d2fd47c07c58186bb475c6308
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Security for scribe-client 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 8 files in this release have been uploaded with provenance metadata.

Select from 8 files

Attestation for scribe_client-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

8 release files Full provenance

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