documents:read; uploading and requesting need documents:write.
Upload a document
Three steps: register the document,PUT the bytes, finalise.
1
Register the document
Response 201
upload_pending: true) but stays hidden from the app and from GET /v1/deals/{id}/documents until you finalise it. upload_url is valid for about two hours, until expires_at; if it lapses, register again.2
PUT the file to upload_url
Send the bytes with exactly the
headers from the response and nothing else. In particular, no Authorization header: the URL carries its own signature. Content-Type must match the mime_type you registered.3
Finalise
Response 200
document.uploaded and, for bank statements, recomputes the deal’s statement coverage. If nothing was uploaded yet it answers 409 with code: "upload_not_found". Bank statements are not analysed automatically (analysis is billed per file): an underwriter starts it from the deal’s Underwrite tab in the app. When that runs, document.processed fires per file and statement.analyzed when the deal’s analysis is complete, and GET /v1/deals/{id}/statement-analysis fills in.Finalising is what makes an upload count. If your process dies between the
PUT and the finalise call, the file is in storage but invisible; call finalise again, it is safe to repeat.Document types
type is one of:
List and download
GET /v1/deals/{id}/documents lists finalised documents with their type and processing state (ocr_status), optionally filtered by ?type=. GET /v1/documents/{id} returns the metadata plus download_url, a signed link that works for 15 minutes (download_url_expires_at says exactly when). Fetch a fresh one each time you need the file rather than storing the link.
Request documents from the merchant
POST /v1/deals/{id}/document-requests creates a request the merchant fulfils through a unique upload link, the same flow the app’s “Request documents” button uses. It sends messages, so use an Idempotency-Key.
Response 201
items[].type is one of bank_statement, voided_check, drivers_license, business_license, tax_returns, proof_of_ownership or utility_bill (underscores, unlike the hyphenated document type); label overrides the text the merchant sees. person_id defaults to the deal’s primary owner, then the business’s primary contact. expires_in_days (1 to 90, default 30) sets expires_at.
delivery reports what actually happened; channel echoes what you asked for. Files the merchant uploads through the link become documents on the deal, fire document.uploaded and are matched to the requested items automatically: items[].status becomes received with received_document_id, and the request’s own status moves from pending through partial to complete.
Statement analysis
GET /v1/deals/{id}/statement-analysis returns what the underwriter sees after bank statements are parsed: per-month figures, a summary with the revenue trend, the deal’s MCA positions, and coverage per account. It answers status: "not_analyzed" with empty figures when no statement has been analysed yet (not an error), and pending or processing while analysis is still running.
Response 200
- Deposits (
months[].deposits) are true deposits: transfers and MCA credits are excluded when the parser identified them.summary.avg_monthly_depositsis the bank-verified revenue that lender matching uses instead of the statedannual_revenuewhenever statements are analysed, which is why uploading statements before matching changes the results. - Average daily balance and ending balance are summed across accounts for the month;
summary.avg_daily_balanceis the mean of the monthly figures. - NSF count is the number of returned or non-sufficient-funds items. Negative days is the number of days the account closed below zero (the maximum across accounts for the month). Both feed the computed paper grade and many lender gates.
- Positions are existing advances on the deal:
detected: truefor those found from recurring debits to known funders in the statements,falsefor positions entered on the deal. Lenders use the count and the total payment when deciding on a new position. - Coverage says whether the months the location requires are on file for every account: the last
required_monthsfull calendar months (3 by default, a location setting) plus a month-to-date statement whenmtd_requiredis true.statusiscomplete,missing(seemissing[]andaccounts[].months_missing),needs_analysis(statements uploaded but not yet parsed;unanalyzed_documentscounts them) orno_statements. - Trend (
summary.revenue_trend) compares deposits in the most recent half of the analysed period with the earlier half:upat +10% or more,downat −10% or less, otherwiseflat;nullwith fewer than two months.