C#
C#
The official .NET SDK, published to NuGet as SpreadSpace. It wraps the
API Reference with a typed exception hierarchy, automatic
retries, idempotency, and cursor pagination exposed as IAsyncEnumerable
(await foreach). Targets net8.0.
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 in a static initializer every test touches.
Paginate (auto-paged borrowers)
List endpoints return a PageIterator, an IAsyncEnumerable<JsonElement> that
fetches pages on demand until the cursor is exhausted. No has_more / total
bookkeeping; iteration simply stops when next_cursor is null.
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. With wait: true the helper
also polls to a terminal job status before returning. 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 throws UploadException from WaitAsync.
Create an extraction export and wait
Long-running work is modeled as an async operation: CreateAsync enqueues
it and returns a handle; WaitAsync polls to a terminal status. A failed
operation throws AsyncOperationException; a timeout throws
AsyncOperationTimeoutException. 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. GetAsync returns the latest of a status,
VersionsAsync the history (newest first, capped at 200 and not paged),
GetSnapshotAsync any one snapshot by id. All three are typed: they return
AttributesEnvelope / AttributeVersions / AttributeSnapshot records, whose
Payload and Manifest carry the attributes.v1 shape, so the figures are
reachable without a JsonElement walk. Each read keeps an untyped twin for
callers who bind the body themselves: GetRawAsync, VersionsRawAsync and
GetSnapshotRawAsync, same parameters, returning Task<JsonElement?>.
VersionsAsync 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, not re-serialized JSON, or the HMAC won’t match.
VerifyAndParse throws WebhookSignatureException on any failure and otherwise
returns the parsed, typed event with typed As*() accessors.
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 exception. Match on the type; for the stable
machine code read the ErrorType property (the wire error.type), never the
message. Each exception carries RequestId (from the X-Request-ID response
header). Quote it in support tickets.
All derive from ApiStatusException → SpreadSpaceException. 429, 5xx, and
transport errors retry automatically with exponential backoff + full jitter;
other 4xx never retry. Tune with MaxRetries on SpreadSpaceClientOptions.
Money is exact
Monetary values decode losslessly to System.Decimal (via
JsonElement.GetDecimal() on the raw number token); there’s no float64
intermediate, so cents are preserved exactly.