@spreadspace/sdk
v0.1.8
Published
Official TypeScript SDK for the SpreadSpace API.
Downloads
761
Maintainers
Readme
SpreadSpace TypeScript SDK
Official TypeScript SDK for the SpreadSpace API — document ingestion, extraction, and spreads for lending workflows.
- Typed error hierarchy, automatic retries with backoff, and idempotency.
- Cursor pagination as a native
for await ... ofasync iterable. - First-class helpers for the async-operation (export) and document-upload flows.
- ESM + CommonJS, full type declarations, zero runtime dependencies.
Node >= 18 (uses the platform fetch and Web Crypto).
Install
npm i @spreadspace/sdkBuilding from source? The typed request/response substrate is generated from the public OpenAPI spec into
src/generated/(gitignored). Runbash scripts/generate.shonce beforenpm run build/tsc. Consuming the published package needs none of that —dist/is shipped prebuilt.
Quickstart
import { SpreadSpace } from '@spreadspace/sdk';
// ss_test_ -> isolated sandbox tenant; ss_live_ -> real workspace data. Same
// base URL. You can also set SPREADSPACE_API_KEY and call `new SpreadSpace()`.
const client = new SpreadSpace({ apiKey: 'ss_test_...' });List borrowers (auto-paged)
List methods return a lazy async iterable that fetches pages on demand and stops
when the cursor is exhausted (next_cursor === null). There is no has_more or
total — just iterate.
for await (const borrower of client.borrowers.list()) {
console.log(borrower.id);
}
// Filters + page size:
for await (const b of client.borrowers.list({ intake: true, limit: 50 })) { /* ... */ }
// Loans (optionally scoped to a borrower) and jobs iterate the same way:
for await (const loan of client.loans.list({ borrowerId: 'abc123' })) { /* ... */ }
for await (const job of client.jobs.list()) { /* ... */ }Each list() is generic — pass your own row type: client.borrowers.list<Borrower>().
Create an extraction export and wait for it
Long-running work is an async operation: create() enqueues it and resolves
to a handle; wait() polls to a terminal status. Terminal states are
succeeded / failed / cancelled (note the double-L). A failed operation
rejects with AsyncOperationError carrying errorCode / errorMessage.
const operation = await client.exports.create({ borrowerId: 'abc123', format: 'xlsx' });
// Poll until terminal. Rejects with AsyncOperationError on "failed",
// AsyncOperationTimeout if it doesn't finish in time.
const op = await operation.wait({ timeoutMs: 5 * 60_000 });
console.log(op.status); // 'succeeded'
console.log(op.resultUrl); // download link, when present
// Cancel a still-running operation (cancelling a terminal op rejects with
// ConflictError; cancelling an already-cancelled op is an idempotent success):
await client.asyncOperations.cancel(op.operationId);Upload a document and wait for processing
Upload is a three-step dance the helper handles for you: mint a presigned URL,
PUT the bytes straight to S3, then confirm. 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.
// wait: true also polls to a terminal job status before resolving.
const job = await client.documents.upload('./statement.pdf', {
borrowerId: 'abc123',
wait: true,
});
console.log(job.id);
// From bytes / a Blob — fileName + contentType are required there:
await client.documents.upload(bytes, {
fileName: 'statement.pdf',
contentType: 'application/pdf',
});
// Poll a job's status yourself:
const status = await client.documents.status(job.id);Terminal job statuses are COMPLETED / FAILED; PENDING / PROCESSING
are in flight. A FAILED job rejects wait() with UploadError. (REJECTED
is a per-document outcome, not a job status — a failed job is what surfaces as
"rejected" in the UI.)
If billing is inactive, minting the presigned URL returns 402 and the SDK throws (rather than hanging) so you see the billing wall immediately.
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 (preferred from the X-Request-ID response header) — quote
it in support tickets.
| Error | HTTP |
|---|---|
| InvalidRequestError | 400 |
| AuthenticationError | 401 |
| PermissionError | 403 |
| NotFoundError | 404 |
| ConflictError | 409 |
| RateLimitError | 429 (honors Retry-After) |
| ServerError | 5xx |
| NetworkError | transport failure (no HTTP response) |
The HTTP-status errors derive from SpreadSpaceError.
import { RateLimitError, SpreadSpaceError } from '@spreadspace/sdk';
try {
for await (const b of client.borrowers.list()) { /* ... */ }
} catch (err) {
if (err instanceof RateLimitError) {
console.log(`rate limited; retry after ${err.retryAfter}s (request ${err.requestId})`);
} else if (err instanceof SpreadSpaceError) {
console.log(`${err.type}: ${err.message} (request ${err.requestId})`);
}
}429 and 5xx (and transport errors) are retried automatically with exponential
backoff + full jitter, honoring Retry-After. Other 4xx are never retried.
Tune with maxRetries in the constructor.
Idempotency
Non-GET requests automatically get a generated Idempotency-Key (a UUID).
Supply your own (via the request options on the low-level request() escape
hatch) to safely retry a specific call across restarts, or pass null to
suppress it.
Pinning the API version
Every request sends a SpreadSpace-Version header. The SDK pins a known-good
default (DEFAULT_API_VERSION); override it globally with apiVersion in the
constructor, or per call via the apiVersion option on list() and the helpers.
const client = new SpreadSpace({
apiKey: 'ss_test_...',
apiVersion: '2026-05-03',
maxRetries: 4,
});⚠️ Money is a number (not exact — read this)
Monetary amounts decode to a plain JavaScript number (IEEE-754 float64).
TypeScript has no decimal type, so very large amounts or sub-cent arithmetic
can lose precision — do not treat these values as exact decimals, and do not
sum many of them naively where cents must reconcile.
This is a deliberate, temporary stance: the wire currently encodes money as a
JSON number. A future API change will move money to a decimal string for
lossless transport; that will be a coordinated, breaking change and this SDK will
adopt it then. For exact arithmetic today, read the raw string from the response
body yourself, or defer the math to the server-side spread/export. (The .NET SDK
decodes money as System.Decimal and is exact — this caveat is TS-specific.)
Escape hatch
For any endpoint the resource helpers don't wrap, call the raw transport. It
applies all the cross-cutting behavior (auth, version header, retries,
idempotency, error mapping) and resolves to the decoded body (or undefined on a
204):
const body = await client.request('GET', '/api/some/endpoint');License
MIT
