TypeScript
The official TypeScript / Node SDK, published to npm as @spreadspace/sdk.
It wraps the API Reference with a typed error
hierarchy, automatic retries, idempotency, and cursor pagination as a native
for await … of async iterable. ESM + CommonJS, full type declarations, zero
runtime dependencies. Requires Node >= 18 (uses the platform fetch and Web
Crypto).
Install
Construct the client
The constructor throws 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.
Paginate (auto-paged borrowers)
List methods return a lazy async iterable that fetches pages on demand and stops
when the cursor is exhausted (next_cursor === null). There’s no has_more or
total; just iterate.
Upload a document and wait
Upload mints a presigned URL, PUTs the bytes to that URL, then confirms;
raw bytes never transit a SpreadSpace endpoint body. file may be a path
string, raw bytes, a Blob / File, or a Readable / iterable of chunks. With
wait: true the helper also polls to a terminal job status before resolving.
Files up to 100 MB; a ZIP’s members up to 50 MB each.
Terminal job statuses are COMPLETED / FAILED (PENDING / PROCESSING
are in flight). A FAILED job rejects wait() with UploadError.
Create an extraction export and wait
Long-running work is an async operation: create() enqueues it and resolves
to a handle; wait() polls to a terminal status. A failed operation rejects
with AsyncOperationError; a timeout rejects with AsyncOperationTimeout.
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.
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 (a plain array: newest first, capped at 200 and not
paged), retrieve any one snapshot by id. All three are typed: get returns
an AttributesEnvelope, versions an AttributeVersions, retrieve an
AttributeSnapshot. Its payload and manifest carry the attributes.v1
types (LoanAttributesPayload, LoanAttributesManifest), exported from the
package alongside them. 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.
Verify a webhook signature
Verify the SpreadSpace-Signature header against the exact raw request body
you received on the wire, never a re-serialized JSON object, or the HMAC won’t
match. verifyAndParseWebhook throws 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(endpointId, { status, eventType, loanId })
pages the delivery log filtered. Every row carries the loan its payload named.
Handle errors
Every API error maps to a typed error. Match on the class; for the stable
machine code read err.type (the wire error.type), never the message. Each
error carries requestId (from the X-Request-ID response header). Quote it
in support tickets.
The HTTP-status errors derive from SpreadSpaceError; the webhook verifier
throws the standalone WebhookSignatureError. 429, 5xx, and transport
errors retry automatically with exponential backoff + full jitter; other 4xx
never retry. Tune with maxRetries in the constructor.
Money is exact
On the reads that send money as a JSON number (loans, borrowers, jobs), amounts
decode to a decimal.js Decimal (exact, never IEEE-754 float64), matching
the Python (Decimal) and .NET (System.Decimal) SDKs. The SDK parses
responses with a lossless JSON reader, so cents never drift even when you sum
many amounts. Those wires are unchanged; only the decoded JS type differs.
Attribute snapshots and export payloads send money as a string with exactly two
decimals instead, per the value conventions, and the SDK
hands that string through. Parse it with a decimal type, never Number.
Non-money numbers (counts, limits, progress) stay plain numbers. One edge:
inside the opaque payload blob (spreads / P&L / cash-flow / aging), money is
matched by field name; the current vocabulary is covered exactly; a brand-new
payload money key the SDK doesn’t yet recognize falls back to a plain number
until the set is extended.