Python
The official Python SDK, published to PyPI as spreadspace. It wraps the
API Reference with a typed error hierarchy, automatic retries,
idempotency, and lazy cursor pagination. Requires Python 3.9+.
Install
Construct the client
The constructor raises when it finds no key, from neither the argument nor the environment variable. Construct the client where a missing key should fail: at startup, or on first use, not at import time in a module every test loads.
Other constructor options: base_url (override the base URL),
api_version (pins the SpreadSpace-Version header), timeout (seconds, per
request), and max_retries.
Paginate (auto-paged borrowers)
List endpoints return a lazy iterator that walks cursors for you; it fetches the next page only as you consume it.
Create and retrieve a loan
Borrower, loan, and job reads return typed dictionaries with their wire identifiers.
Upload a document and wait
The upload helper requests a presigned URL, PUTs the file bytes to that URL
with the Content-Type it was given, then returns a job handle you can wait
on. Every upload belongs to a loan, so loan_id is required; borrower_id is
optional. Files up to 250 MB (a PDF over 1,500 pages is refused when processing
starts); a ZIP up to 250 MB whose members are each up to 100 MB uncompressed.
Accepted formats: PDF, HTML (rendered to PDF before classification), ZIP, Excel
and JSON.
The SDK accepts a caller-owned operation_key. Persist a unique key before
starting each upload operation and reuse it only when retrying that same
operation with the same file and parameters. Persist the accepted job ID before
waiting so a restart can resume status reads. Presigned URLs expire: reusing the
same key does not make an upload restartable indefinitely or create a new
operation. Once a job is accepted, resume that job rather than upload again.
Submit a document through intake
The intake helper creates a submission, PUTs the file bytes directly to
storage with the matching Content-Type, then confirms the upload, with no
borrower or loan modeled first: the API provisions both and returns their ids
on the handle. file accepts the same sources as the upload helper. Requires
an API key holding the intake:write scope; wait() and
client.documents.status() poll GET /api/documents/{job_id}/status, which
needs documents:read on the same key. Once your system holds the verified
business name, borrowers.complete_intake finishes the borrower’s profile; it
needs borrowers:write, and a borrower already completed answers 409.
client_reference groups repeat submissions: the same reference resolves to
the same provisioned loan and borrower instead of creating another pair, and
omitting it creates a fresh pair every time. The operation key yields stable,
distinct create and confirm idempotency keys. After confirm is accepted, resume
status checks with client.documents.status(job_id).
Create an extraction export and wait
An export is a resource you create and poll. create
returns a handle; wait
polls it to a terminal status (raising ExportFailedError on a failed
export, or ExportTimeoutError on timeout). borrower_id, loan_id and
document_ids are required, as they are on the server. format is json,
csv, or xlsx; xlsx is available for bank statements only, so pass it only
when every document in the export is a bank statement.
client.exports.get(export_id) reads one, client.exports.list(status=...)
pages through yours as ExtractionExport rows, and
client.exports.cancel(export_id) stops one still queued or running. The
download link is minted fresh on every read, so read the export again when you
need a new one, or call client.exports.download_link(export_id), which
returns the link together with the expiry from that same read.
Read a loan’s finalized attributes
When an analyst finalizes a spread, its figures freeze
as an attribute snapshot. get returns the latest of a status,
versions the history (an envelope { loan_id, snapshots } whose
snapshots is newest first, at most 200, not paged), get_snapshot any one
snapshot by id. All three are typed: the reads are
annotated AttributesEnvelope / AttributeVersions / AttributeSnapshot, and
every payload and manifest type ships as a TypedDict (AttributeSnapshot,
LoanAttributesPayload, LoanAttributesManifest) exported from spreadspace,
so a type-checker knows the keys. The receiver hands your spread.finalized
handler the same AttributeSnapshot.
versions takes a status of its own. You get the filtered history, the first
row is the loan’s current one of that status, and the 200 cap applies after the
filter.
Read an extracted document
client.extractions reads the extracted documents on a
borrower’s file as typed objects: one get_<family> / list_<family>s pair
per document family (get_tax_return / list_tax_returns,
get_bank_statement / list_bank_statements, get_debt_schedule /
list_debt_schedules, …), named after the family as its object page titles
it. A retrieve returns the family object, annotated with a TypedDict from
spreadspace.extractions that follows the schema its
object page shows; a list returns
{"borrower_id", <items>: [...]} with one row per document. A list takes an
optional loan_id that scopes the rows to one loan; the two aging lists also
take a form (summary or detail). get_report_data reads any category in
the shape of its family; get_report_data_by_category reads the borrower’s
newest document of one category by its slug, on one loan when loan_id is
given. Every money key is a two-decimal JSON string, typed str.
Other JSON numbers arrive as int or Decimal, never float.
Every call needs the extractions:read scope.
Read a statement mapping
A profit and loss statement’s or a balance sheet’s mapping onto the chart of
accounts, read by document id, each statement through its own read:
client.statement_mappings.get_profit_loss(borrower_id, doc_id) returns a
ProfitLossMapping dictionary and .get_balance_sheet(borrower_id, doc_id) a
BalanceSheetMapping. The status is ready or unmappable. A ready mapping
carries a payload whose statement names the kind (profit_and_loss or
balance_sheet) and whose mapping_state is mapped (the tree lists every
section and account with its figure per period, and each printed line names
the node it is filed under) or unfooted (the lines are returned as printed
and foot says by how much each period misses: a profit and loss statement’s
printed bottom line, or on a balance sheet each side’s printed total and the
imbalance between the sides). Every figure is a string with exactly two
decimals, positive in the direction of the node’s effect; parse with
Decimal when computing.
client.statement_mappings.list_profit_loss(borrower_id, loan_id=..., limit=...)
and .list_balance_sheet(borrower_id, loan_id=..., limit=...) page the
borrower’s mappings of that statement newest document first, and each envelope
of .pages() carries pending_document_ids, the documents whose first mapping
is being prepared. All four need extractions:read and the Statement mapping
feature on the workspace’s plan.
A mapping being prepared answers 404 with a Retry-After header: the
NotFoundError carries it as retry_after, so read again after that many
seconds. A document with no mapping, or one of the other statement (a balance
sheet on the profit loss read), is a NotFoundError whose retry_after is
None. A workspace whose plan does not include the feature is a
PermissionDeniedError whose type is statement_mapping_not_in_plan.
The chart of accounts and the export map
client.chart_of_accounts.get() reads the workspace’s chart document
(chart, None until the workspace saves one, when the platform’s accounts
apply), its version, the version of its caption rules and its
active_export_system. get_export_map() reads the active export map: the
workspace’s own code and name for each chart account, in one export system.
replace_export_map({"system": ..., "entries": [...]}) replaces that system’s
whole map and makes the system the active one: an account left out leaves the
map, at most 1,000 entries, and a save that crosses another raises
ConflictError. A mapping read after a save is stale until it is prepared
again with the new codes. The reads need chart_of_accounts:read,
chart_of_accounts:write or extractions:read; the save needs
chart_of_accounts:write.
Verify a webhook signature
Verify the SpreadSpace-Signature header against the exact raw request body
(bytes or str) you received on the wire, not re-serialized JSON, or the HMAC
won’t match. verify_and_parse_webhook raises WebhookSignatureError on any
failure and otherwise returns the parsed, typed event.
Receive webhooks
The receiver does the whole loop (verify, dedupe, dispatch, answer) and
on spread.finalized reads the snapshot the event names for you (by id, never
the loan’s latest: see Spreads). Full option list on
the Webhooks page.
Hand it the raw body. A framework that parses JSON first has already changed the bytes the signature covers.
To audit what we sent, client.webhooks.deliveries(endpoint_id, status=…, event_type=…, loan_id=…) pages the delivery log filtered. Every row carries
the loan its payload named.
Receive extraction.mapped
extraction.mapped fires once for every mapping of a profit and loss statement
or a balance sheet: the first, and each one made again after the extraction,
the chart of accounts, the caption rules or the export map changed, or the
statement was corrected (reason is first, extraction, chart,
correction or logic). A pointer, not the figures: statement says which
statement was mapped, profit_and_loss or balance_sheet, and so which read
returns it, client.statement_mappings.get_profit_loss(event.data["borrower_id"], event.data["document_id"]) or .get_balance_sheet(...). The receiver
dispatches it to on_extraction_mapped; event.data has the keys of
ExtractionMappedPayload.
Embed
client.embed.sessions mints the credentials that authorize an
embedded surface. create_iframe_url returns a single-use
signed_url for the iframe. create returns a loan-scoped embed_token for a
client of your own, and revoke ends that session. Each mint returns the
response as a dictionary.
Both mints need embed:write on the key, so construct a client with the
embed-minting key, which the samples read from SPREADSPACE_EMBED_API_KEY.
Name scopes on every mint: read scopes are granted by mint authority, and a
write scope is granted only when the request names it and only if the minting
key holds it. Mint from your server covers every field.
Handle errors
All errors derive from SpreadSpaceError. Match on the typed subclass, never on
the message string. Every error carries request_id (from the X-Request-ID
response header). Quote it in support tickets.
The HTTP-status errors derive from APIStatusError → SpreadSpaceError;
NetworkError derives from SpreadSpaceError directly. Transient failures (429,
5xx, transport errors) retry 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,
for example Decimal("128986.06"). 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.