Skip to main content

SpreadSpace Python SDK

Official Python client for the SpreadSpace API — document extraction and financial spreading for lending.

Install

pip install spreadspace

Requires Python 3.9+.

Authentication

from spreadspace import SpreadSpace

client = SpreadSpace(api_key="ss_test_...")   # or omit and set SPREADSPACE_API_KEY

The key prefix selects the environment:

  • ss_test_... — routes to your sandbox tenant (seed data, safe to experiment).
  • ss_live_... — routes to your live tenant (real data).

If api_key is omitted, the client reads SPREADSPACE_API_KEY from the environment. Never hard-code a live key; never commit any key.

Client options

client = SpreadSpace(
    api_key="ss_test_...",
    base_url="https://api.spreadspace.app",   # override for a private deployment
    api_version="2026-05-03",                # pins the SpreadSpace-Version header
    timeout=60.0,                            # seconds, per request
    max_retries=2,                           # 429 + 5xx + transport errors
)

API version pinning

Every request sends a dated SpreadSpace-Version header. The SDK pins a default version per release (decoupled from the SDK's own semver). Pin it explicitly to insulate your integration from server-side changes, and override per call when you need a newer surface:

client.borrowers.list(api_version="2026-06-01")   # one call on a newer version

Pagination (lazy, auto cursor)

List endpoints return a lazy iterator that walks cursors for you — it fetches the next page only as you consume it.

for borrower in client.borrowers.list():
    print(borrower["borrower_id"])

# Filters pass straight through and persist across pages:
for job in client.jobs.list(limit=50):
    print(job["job_id"])

Core lists yield typed Borrower, Loan, and JobStatus dictionaries. They remain ordinary dictionaries at runtime.

borrower = client.borrowers.create({"name": "Example borrower"})
loan = client.loans.create(borrower["borrower_id"], {"name": "Example loan", "requested_amount": "250000.00"})
current_loan = client.loans.get(loan["loan_id"])

Extraction export + wait

Exports run asynchronously. create returns a handle; wait polls to completion (raising ExportFailedError on failed, ExportTimeoutError on timeout). Terminal statuses are succeeded / failed / cancelled; cancelling — a cancel that landed mid-bundle — is NOT terminal, so wait polls through it. format is json, csv, or xlsx; xlsx is available for bank statements only.

borrower_id, loan_id and document_ids are required, as they are on the server.

export = client.exports.create(
    borrower_id="abc123",
    loan_id="def456",
    document_ids=["7a1d4e9c2b3f4a5d8e6c1b0f", "2a3b4c5d6e7f809112345678"],
    format="json",
)
result = export.wait(timeout=300)   # seconds
print(result.status, result.download_url)   # fresh download link on each retrieval
print(result.document_counts)               # {"requested": .., "exported": .., "skipped": ..}
print(result.bundle)                        # {"name": .., "size_bytes": .., "line_item_count": ..}

# Or ask for a fresh link by id (raises ExportNotReadyError until succeeded).
# The expiry comes off the same read, so it describes this link:
link = client.exports.download_link(result.export_id)
print(link.url, link.expires_at)

# List exports as ExtractionExport rows (items live under `exports`, not `data`):
for row in client.exports.list(status="running"):
    print(row.export_id, row.status)

# Cancel a still-running export (cancelling a finished one raises ConflictError;
# cancelling an already-cancelled one is an idempotent success):
export.cancel()
# ...or by id: client.exports.cancel(result.export_id)

Upload a document + wait for processing

job = client.documents.upload(
    "statement.pdf", loan_id="def456", borrower_id="abc123",
    operation_key="application-123-document-1",  # persist before the first attempt
)
print(job.id)  # persist the accepted job ID alongside the operation key
status = client.jobs.get(job.id)  # JobStatus dictionary
final = job.wait(timeout=600)   # seconds
print(final["status"])

The upload helper requests a presigned URL, PUTs the file bytes directly to storage with the matching Content-Type (part of the V4 signature), then returns a job handle you can wait on. Every upload belongs to a loan, so loan_id is required; borrower_id is optional.

Persist the operation key and returned job ID. Reusing the key produces stable, distinct presign and confirm idempotency keys. After accepted confirm, use client.jobs.get(job_id) to resume status checks. Presigned URLs expire, so a replayed presign response does not permit indefinite upload restarts. API version and retry overrides apply to both API stages and stay off the storage PUT.

Read an extracted document (typed)

client.extractions has one get_<family> / list_<family>s pair per document family (get_tax_return / list_tax_returns, get_bank_statement / list_bank_statements, ...), annotated with TypedDicts from spreadspace.extractions, plus get_report_data for any category in the shape of its family:

schedule = client.extractions.get_debt_schedule(borrower_id, document_id)
print(schedule["form_variant"], len(schedule["debt_schedule"]))

for row in client.extractions.list_debt_schedules(borrower_id)["schedules"]:
    print(row["extracted_document_id"], row["report_data"]["as_of_date"])

Error handling

All errors derive from SpreadSpaceError. Match on the typed subclass, never on the message string:

from spreadspace import (
    SpreadSpaceError,       # base
    NetworkError,           # transport failure, no HTTP response
    BadRequestError,        # 400
    AuthenticationError,    # 401
    PermissionDeniedError,  # 403
    NotFoundError,          # 404
    ConflictError,          # 409
    RateLimitError,         # 429
    InternalServerError,    # 5xx
)

try:
    client.borrowers.get("missing-id")
except RateLimitError as e:
    print("retry after", e.retry_after, "seconds")
except NotFoundError as e:
    print("not found")
except SpreadSpaceError as e:
    # Every error carries request_id — quote it in support tickets.
    print(e.message, e.status_code, e.request_id)

request_id comes from the X-Request-ID response header (falling back to the error body). Transient failures (429, 5xx, transport errors) are retried automatically up to max_retries with exponential backoff + full jitter, honoring Retry-After.

Money is exact

Every money key in report_data is a JSON string with exactly two decimals, typed str. Attribute snapshots and export payloads use the same two-decimal strings. The SDK preserves them unchanged; parse with Decimal when computing. Other JSON numbers arrive as int or Decimal, never float. Loan requested_amount, extraction query amounts and totals, and EBITDA write-in amounts are also strings. Create or update a loan with "requested_amount": "250000.00"; its allowed range is "0.00" through "999999999999.99". Invalid formats return 400 validation_failed.

from decimal import Decimal

amount = Decimal("128986.06")
statement = client.extractions.get_bank_statement(borrower_id, document_id)
transaction_amount = Decimal(statement["transactions"][0]["amount"])

Development

The generated OpenAPI core lives in src/spreadspace/_generated/ and is built in CI (scripts/generate.sh, Java-based — not run locally). It is gitignored and must never be hand-edited. The hand-written ergonomic layer (transport, helpers, typed errors) is the only code committed by hand.

pip install -e '.[dev]'
pytest

License

MIT

Metadata

Release files for spreadspace 0.6.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 spreadspace 0.6.0
File Size Uploaded Provenance
spreadspace-0.6.0.tar.gz 133.0 kB With provenance Details

Built distribution (wheel)

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

Total release size: 243.7 kB

Release files / spreadspace-0.6.0.tar.gz

Download URL spreadspace-0.6.0.tar.gz
Size 133.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f2550fe923dff21d969929c61a2a2a9989e36c53d3dbc217b02b548d3016757b
BLAKE2b-256 checksum
How to use checksums
c4a27c2d9c9d374782fe428a2cbce3a6539e6b083cba728fb85a0c02c7dd6334
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 Sep 15, 2026.

Transparency log

Release files / spreadspace-0.6.0-py3-none-any.whl

Download URL spreadspace-0.6.0-py3-none-any.whl
Size 110.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
993edffc7a8e70de3c70c990e11b9cad22dd77be276550c084479066c055c4a9
BLAKE2b-256 checksum
How to use checksums
7db6c6df595b0bfa2305ec85d5031523f11e2af5a6ac29bf005dad6a85b4c0dd
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 Sep 15, 2026.

Transparency log

Security for spreadspace 0.6.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 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for spreadspace-0.6.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 Sep 15, 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.

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