Skip to main content

Machinera Python SDK

Python client for the Machinera speech-to-text API, with a blocking client (Machinera) and a native asyncio client (AsyncMachinera). Requires Python 3.10 or newer.

Quick start

pip install machinera
export MACHINERA_API_KEY="your-api-key"
from machinera import Machinera

with Machinera() as client:
    result = client.transcribe_file("recording.wav", model="transcribe-v1")
    print(result.text)

transcribe-v1 is the model ID. Each call waits for the finished transcript and returns a TranscriptionResult. To transcribe audio that is already online, pass a direct audio URL instead:

with Machinera() as client:
    result = client.transcribe_url("https://audio.example/recording.wav", model="transcribe-v1")

Supported formats are the file suffixes in machinera.SUPPORTED_MEDIA_SUFFIXES; content without a supported name is identified from its MIME type or container signature (see File inputs). Large files are uploaded automatically (see Large files). Live calls submit audio for transcription and may incur usage charges.

Asyncio

AsyncMachinera takes the same configuration and has the same methods; await them:

import asyncio

from machinera import AsyncMachinera


async def main() -> str:
    async with AsyncMachinera() as client:
        result = await client.transcribe_file("recording.wav", model="transcribe-v1")
        return result.text


text = asyncio.run(main())

Recover an interrupted transcription

A call can end before it returns a transcript (a deadline, Ctrl+C, a network failure, or task cancellation) while the service keeps working on the job. The SDK never cancels a server job or deletes its input. To pick the work up again, save an operation key before calling and pass it as idempotency_key:

import uuid

from machinera import APIError, Machinera

key = uuid.uuid4().hex
save_recovery_state(key=key, job_id=None)  # your own durable storage

with Machinera() as client:
    try:
        result = client.transcribe_file("recording.wav", model="transcribe-v1", idempotency_key=key)
    except APIError as error:  # includes DeadlineExceededError and TranscriptionInterrupted
        save_recovery_state(key=key, job_id=error.job_id)
        raise

Later, even from a new process, continue from the saved state:

with Machinera() as client:
    if saved_job_id is not None:
        result = client.resume(saved_job_id)  # polls the accepted job; never submits
    else:
        result = client.transcribe_file(  # replays the same submission
            "recording.wav", model="transcribe-v1", idempotency_key=saved_key
        )

The rules behind this recipe:

  • Keyed calls are durable jobs. An idempotency_key (or transport="job") submits a server job that can be resumed. Without one, a file whose encoded request fits Limits.sync_inline_body_bytes is sent as a single synchronous request. If that request's response is lost, the SDK raises AmbiguousSubmissionError: the transcription may already have run, so reconcile (check what you already received or were billed for) instead of resubmitting.
  • Reuse a key only for the identical request: same file bytes, metadata, options, credentials, and endpoint. Use a fresh key for every independent transcription. Every APIError carries the call's key in operation_key, including the random key generated when you passed none.
  • Once a job ID is known, use resume(job_id). It only polls and never creates a replacement job. get_job(job_id) reads a single status snapshot (JobSnapshot).
  • A failed job raises TerminalJobError and is never resubmitted automatically.
  • Save results promptly. The service retains results for a limited time, and recovery does not extend it. An expired result or replay requires reconciliation, not a new submission disguised as a retry. The SDK keeps no journal on disk.

What to do after a failure; use the first row that matches:

Exception Meaning Next step
TerminalJobError The job failed. Inspect code before deciding on a new job.
AmbiguousSubmissionError An unkeyed synchronous request may have run. Reconcile; do not resubmit.
AuthenticationError, PermissionDeniedError The credential was refused. Check the API key and its access.
BadRequestError, UnprocessableEntityError, PayloadTooLargeError The request is invalid. Correct the input.
IntegrityError, other UploadError The file changed, or storage or the service refused the upload. Keep the file unchanged and see Large files.
TranscriptionInterrupted with ambiguous true An unkeyed synchronous request was interrupted and may have run. Reconcile; do not resubmit.
Any other APIError except RateLimitError with phase == "sync_submit" An unkeyed synchronous request failed after it may have run. Reconcile; do not resubmit.
NotFoundError, ConflictError The job is unknown to this credential and endpoint, or the service cannot replay the key. Reconcile; do not resubmit under a new key.
Any other APIError with job_id set The job was accepted and may still be running. resume(job_id)
DeadlineExceededError, TranscriptionInterrupted, APIConnectionError, APIResponseValidationError, RateLimitError, InternalServerError Admission was not confirmed. Repeat the identical call with idempotency_key=error.operation_key, after a pause for rate limits and server errors.
Any other APIError The service refused the operation. Inspect code and reconcile before any new submission.

With AsyncMachinera, task cancellation propagates the original asyncio.CancelledError with operation_key, upload_id, phase, job_id, and last_status attached once the call has started. Read them with getattr(error, "job_id", None) inside the coroutine that directly awaits the SDK method, save them, and re-raise. On Python 3.10, a task boundary can replace the cancellation exception, so another task may not see these attributes.

For files above the inline limit, see Large files. Complete recovery programs: blocking, asyncio, and large file.

Configuration

All constructor arguments are keyword-only. Explicit non-None values take precedence over the environment, and invalid values raise ValueError without falling back.

  • api_key defaults to MACHINERA_API_KEY. A missing or empty credential raises ValueError before any request.
  • base_url defaults to MACHINERA_BASE_URL, then https://api.machinera.com/v1. An origin without /v1 is accepted and normalized.
  • timeout, max_retries, and retry_policy are described under Timeouts and deadlines and Retries.
  • default_headers is copied and merged into API requests. It can override non-reserved defaults such as User-Agent; invalid headers and attempts to set a header the SDK owns (_RESERVED in _files.py) raise ValueError before any request.
  • transport="job" sends every call as a durable job. The default, "auto", picks synchronous, inline durable-job, or staged submission by encoded size.
  • limits=Limits(...) changes the encoded request sizes behind that choice; it does not change service limits. Durable jobs above Limits.job_inline_body_bytes use staged uploads (see Large files).
  • max_concurrency optionally bounds the number of active calls; waiting for a slot counts toward the call's deadline.

No dotenv files are loaded and no endpoints are probed. Client configuration is immutable. One Machinera client can be shared by threads, and one AsyncMachinera client by tasks on one event loop. close() / aclose() (or leaving the with block) waits for active calls and closes only an HTTP client the SDK created.

File inputs

transcribe_file accepts a path (str or PathLike; bare strings always mean paths), bytes, a seekable binary handle, or a tuple (filename, content[, content_type[, headers]]) whose content is any of those. Content is read from the handle's current offset to its end and streamed without decoding or conversion.

with Machinera() as client:
    result = client.transcribe_file(audio_bytes, model="transcribe-v1", filename="clip.wav")
    result = client.transcribe_file(
        ("clip.flac", audio_bytes, "audio/flac", {"X-Part-Label": "recording"}),
        model="transcribe-v1",
    )

The upload name is resolved in this order:

  1. An explicit filename= or tuple filename; an unsupported suffix raises ValueError. Conflicting non-None tuple and keyword values also raise.
  2. The basename of a path or handle name, when its suffix is supported. Numeric handle names and unsupported suffixes are ignored.
  3. For unnamed content, a supplied MIME type, mapped by _MIME_SUFFIXES in _files.py.
  4. Otherwise, a container signature in the leading bytes, read by Multipart.prepare in _multipart.py and matched by sniff in _files.py.

Content that cannot be identified raises ValueError before any request. Names, content types, and part headers are validated against header injection, and part headers cannot carry credentials or cookies or replace multipart framing.

Binary handles stay open, and their offset is restored after an ordinary completion. Do not modify a file during a call or share one handle between simultaneous calls.

With Machinera, a deadline or interruption can end the call while a read or seek on your handle is still blocked. Call error.wait_for_file_release(timeout) on the raised APIError and require True before reusing, seeking, or closing the handle (timeout=0 only checks). The SDK does not restore the offset in that case.

With AsyncMachinera, task cancellation raises asyncio.CancelledError, which has no wait_for_file_release. None is needed: before the cancellation propagates, the SDK awaits any in-flight file operation and restores a caller-owned handle's original offset (or closes a file it opened). See the asyncio cleanup contract.

Timeouts and deadlines

Every call is bounded by a total deadline; the defaults are declared by the TimeoutPolicy fields. The deadline is monotonic and covers preparation, waiting for a concurrency slot, requests, retry sleeps, and polling. Each poll request also has its own limit.

timeout= Effect
omitted Inherit the client's policy (the method) or TimeoutPolicy() (the constructor).
seconds Set the connect, write, read, and pool phases to that value.
httpx.Timeout(...) Copy its four phases, including disabled (None) ones.
None Disable HTTP phase limits only.
TimeoutPolicy(...) Replace all six values.

The scalar, httpx.Timeout, and None forms keep the inherited poll-request and total-deadline bounds, so calls stay bounded. transcribe_file, transcribe_url, and resume also accept deadline=seconds to override the total budget for one call. HTTP phase values follow httpx timeout semantics.

A deadline raises DeadlineExceededError (or AmbiguousSubmissionError for an unkeyed synchronous request that may have started) and keeps the operation key and any accepted job ID for recovery.

Retries

Retry defaults are declared by the RetryPolicy fields. max_retries=n allows n + 1 attempts per replay-safe step, and 0 disables retries; pass retry_policy=RetryPolicy(...) instead for full control (supplying both raises ValueError). Polling has its own interval and count budget.

Network failures, HTTP 429/502/503/504, and service refusals marked retryable are retried only when replaying the request is safe. Backoff is min(initial_delay * 2**retry_index, max_delay) times a random factor in [0.75, 1]; a Retry-After header sets a minimum wait, and a wait that cannot finish before the deadline raises DeadlineExceededError. Retries resend the same encoded body and operation key. Authentication and permission failures and failed jobs are never retried. The service's retryable flag takes precedence over the SDK's error-code table, which in turn takes precedence over the status-code rule.

Results

transcribe_file, transcribe_url, and resume return TranscriptionResult; get_job returns JobSnapshot. Both are frozen Pydantic models with attribute access. Unknown response fields are kept in raw, and malformed responses raise APIResponseValidationError rather than a Pydantic ValidationError. JobSnapshot.status is a plain string, so a status added by the service later still parses; while polling, any status other than queued, processing, or completed raises TerminalJobError.

with Machinera() as client:
    snapshot = client.get_job(saved_job_id)
    print(snapshot.status)

result.text preserves whitespace and empty strings exactly. output follows the requested response_format: to_json() returns the text and any usage, to_text() the exact text, and to_verbose_json() every returned field. Durable jobs return verbose fields whatever format was requested; a synchronous response contains only what the service sent. elapsed_seconds is the whole call as seen by the client, including preparation, retries, and polling.

Errors

Every SDK failure derives from MachineraError, and every API or transport failure from APIError. Local argument errors raise ValueError or TypeError before any request. APIError carries status_code (alias status), code, retryable, request_id, a sanitized body, and the recovery context operation_key, job_id, upload_id, phase, and last_status.

HTTP failures raise APIStatusError subclasses (BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, PayloadTooLargeError, UnprocessableEntityError, RateLimitError, InternalServerError). APIConnectionError and its subclass APITimeoutError cover transport failures. APIResponseValidationError means the service returned a body the SDK cannot use and is never retried automatically. A failed job whose uploaded file did not match raises TerminalIntegrityError, which is both an IntegrityError and a TerminalJobError. TranscriptionInterrupted is both an APIError and a KeyboardInterrupt. The recovery table gives the next step for each class, the full hierarchy is in the API reference, and handle_errors.py prints guidance for a failed call.

Exceptions and their chains never contain service free text, raw HTTP objects, URLs, HTML, audio, or transcripts. Protect credentials, source URLs, operation keys, audio, and transcripts in your own logging; see Logging for the SDK's own records.

Logging

The SDK logs to the standard logging logger named machinera: retry decisions at DEBUG (method, path template, HTTP status, error code, attempt, delay, and request ID) and job status changes while polling at INFO (job ID and status). Records never contain URLs with query strings, credentials, file paths, or transcript text. Set MACHINERA_LOG=debug or MACHINERA_LOG=info to set that level when a client is created; a stderr handler is attached only if the logger has none. Configure the logger directly for anything else.

Large files

When the encoded request exceeds the inline limit for durable jobs (Limits.job_inline_body_bytes, which defaults to the SDK's staged-upload threshold STAGED_UPLOAD_THRESHOLD_BYTES in _types.py), transcribe_file uploads the file to storage and then submits a job that references it. This happens with either transport setting; a body exactly at the limit is still sent inline. The service supplies the upload size limit and expiry window. If the service refuses the upload with staged_uploads_unavailable before granting it, the SDK submits the same body once as an inline durable job under the same operation key, provided it fits the service's inline limit; a larger file fails with that error. Byte limits are separate from audio duration and account limits.

File preparation and streaming are implemented in _multipart.py, and the staged flow and its inline fallback in _core.py. Changes to the file's size or content raise IntegrityError. Keep the input unchanged through retries and recovery.

To recover a staged file whose job ID is not yet known, pass the same file, options, and key to resume. It replays initialization, upload, and submission idempotently and never generates a new key:

with Machinera() as client:
    result = client.resume(
        file="recording.flac",
        model="transcribe-v1",
        operation_key=saved_key,
        upload_id=saved_upload_id,  # optional, from error.upload_id
    )

Pass the original response_format and language as well: when omitted, recovery before admission uses "json" and no language hint. Options that differ from the original call raise an error with code idempotency_payload_mismatch instead of returning the earlier result. Once a job ID is known, resume(saved_job_id) only polls.

Storage failures raise UploadError, whose storage_code holds only a sanitized storage error code. Service errors whose code starts with upload_ also raise UploadError, with status_code set, except rate limits, which raise RateLimitError. An expired upload grant is refreshed automatically within the fixed upload window, and an incomplete upload is retried once. Integrity mismatches, expired or already-bound uploads, and key mismatches need reconciliation; an expired upload cannot be reopened under the same key.

HTTP client

The SDK chooses its HTTP clients in Core.__init__ in _core.py. An injected http_client or custom transport carries every request, and its own pool behavior applies. Otherwise uploads and submissions use a client whose pool (EXCHANGE_POOL) keeps no idle connections, so a connection closed at a deadline is never reused, and job status reads use a separate client with POLL_POOL. The blocking client builds that status-read transport with keepalive_transport in _io.py, which falls back to EXCHANGE_POOL when it cannot install its connection hook. Clients the SDK creates ignore environment proxies, do not follow redirects, and use httpx's default of zero transport retries. close() and aclose() close only the clients the SDK created.

An injected http_client=httpx.Client(...) (or httpx.AsyncClient for AsyncMachinera) keeps its own pool, proxies, URL mounts, and lifetime, and is used for both API calls and storage uploads. Upload requests are sent with auth=None and without redirects, so the client's default headers, authentication, and cookies are not added to them. Request and response hooks still run for uploads: they must keep the upload headers, add no credentials or cookies, and must not log the signed upload URL. The SDK suppresses httpx's own request log for uploads.

For offline tests, pass an httpx.BaseTransport (or httpx.AsyncBaseTransport) as transport; clock, sleeper, wall_clock, and random_source are injectable too.

When a call is cancelled, the SDK closes the in-flight connection or response stream and makes no further requests. A custom transport that cannot be interrupted may finish in a background daemon thread, and its late result is discarded. In AsyncMachinera, file hashing, inspection, and reads run in worker threads, so slow local I/O can delay cancellation without blocking the event loop.

Versioning and requirements

Releases use semantic versions. During 0.x, minor releases may change the public API, and patch releases contain compatible fixes; from 1.0, incompatible changes require a major release. Read the changelog when upgrading, and pin an exact version where installs must be reproducible.

The SDK is tested on Python 3.10 through 3.14. Its runtime dependencies are declared in pyproject.toml; it needs no audio decoder or conversion tools.

See the API reference, the runnable examples, CONTRIBUTING.md for development, and SECURITY.md for reporting vulnerabilities.

Metadata

Release files for machinera 0.1.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 machinera 0.1.0
File Size Uploaded
machinera-0.1.0.tar.gz 184.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for machinera 0.1.0
File Interpreter ABI Platform
machinera-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 232.2 kB

Release files / machinera-0.1.0.tar.gz

Download URL machinera-0.1.0.tar.gz
Size 184.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a40eeaa33d03c6df76f0443ccdf1973d33246ddeaff4d0602f313119df2d4cfd
BLAKE2b-256 checksum
How to use checksums
0429bbc3a9b051dd9b113b8b4d3feebaa76328ab539a2fc6950158cc4ca6d19c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Oct 2, 2026.

Transparency log

Release files / machinera-0.1.0-py3-none-any.whl

Download URL machinera-0.1.0-py3-none-any.whl
Size 48.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d4d36c0280a870726902bc57c551e5e3e9376c9e4351c3252058a6878c79f74c
BLAKE2b-256 checksum
How to use checksums
4335f8e598ae20f1901ea61a552558ecaf3fd6d328ff1ef5ab155dc206868c87
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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