underwriting:write permission (the underwriter role and default API keys hold it).
Reading them needs jobs:read. A job can be corrected once it has finished.
What you can correct
Every correction takes an optional
reason. Send one or several in one request; they apply in order:
201) lists the corrections saved and a summary of what the corrections in force change.
Transactions
A row is named by its statement’sdocument_id and its index in statements[].transactions. Both are fixed
for the life of the job.
categorymust fit the row’s direction: a deposit takes a credit category (revenue,transfer_in,mca_funding,loan_proceeds,refund_reversal,other_credit,returned_item,unknown), a withdrawal a debit category. The row then counts under the category you set. Its month is recomputed by the monthly summary itself: the row’s figures as the month counted them come out, and its figures under the new category go in. Re-categorizing a 2,020 and raises its transfers in by $2,020, and the averages and withhold follow.counterpartyis shown on the row, and does not regroup the rows into positions. Which rows form a position is decided per payment stream, mostly by the model’s reading of the whole packet, and a correction never calls the model. Detecting positions again from a corrected name would also change the position keys that overrides name. Correct a position with a position override instead.- Only a verified statement the result is computed from changes the figures. A correction on an unverified, duplicate or excluded statement is saved and shown on its row, but never brings that statement into the numbers.
counted_as, on anmca_paymentrow: the category its month counted it under. It ismca_payment, unless a position decision on its payment stream counted it otherwise:loan_paymentwhen the stream is a business loan, or the stream’s own category (for exampleoperating_expense) when it was decided not to be a position. Re-categorizing such a row moves it from that category to the one you set.mca_projected, on a row of a position (mca_position_id):truewhen the month projects the position’s burden from its cadence instead of adding up its pulls. A projected pull stays projected when you re-categorize it, because positions are not detected again. The month’s MCA debits keep the projection, and the row also counts under its new category.
positions module was off, the result shows no positions, but its months and its existing MCA
burden were computed with them all the same. A correction recomputes the underwriting with that same burden.
There is no position to override, so nothing changes it.
Results stored before rows carried
counted_as and mca_projected, and months by_account, are corrected the
same way, with what they hold. Four cases need what those results did not record, and answer 422:
re-categorizing an mca_payment row outside every position (its stream may have been decided not to be a
position); re-categorizing an mca_payment row of a position on a job whose positions module was off; leaving
one account out of a month with two or more accounts; and any correction that recomputes the underwriting of a
job that had the positions module off and priced an offer with a score. Run the job again to correct those.Positions
A position is named by itskey (positions[].key), which is derived from the position itself: pk_ and the
first 16 hex digits of the SHA-256 of <account_key>|<counterparty_signature>|<first_date>. It does not change
with the order positions are listed in.
A field left
null keeps the computed value. An override changes the existing MCA burden, the current
withhold, the active and stacked positions, the score bands and the affordability pricing, through the same
math as POST /v1/underwriting/withhold. It does not change the pulls each month observed:
the months’ MCA debits and holdback stay what the bank statement shows.
Months
{"type": "month_exclusion", "month": "2026-02", "excluded": true} leaves a month out of the averages;
"excluded": false counts one that was left out, such as a partial month. At least one month must stay in the
averages (422 otherwise).
A month is merged across the packet’s accounts. When it has two or more, months[].by_account lists each
account’s own figures for it, which the month adds up (balances summed; negative and low-balance days the most
of any account). To leave one account’s part of a month out, name the account by its accounts[].account_key:
by_account, with included_in_averages: false, and leaves the month’s accounts.
Merging again also recomputes the month’s partial and month-to-date flags from the accounts left: a month that
was partial only because of the account you left out is counted in the averages again, unless you exclude the
whole month. An unknown account is 404; an account with no figures in that month, or the last account left in
a month, is 422 (exclude the whole month instead).
Reading the corrected job
GET /v1/jobs/{id} returns the corrected result. It is recomputed from the stored ledgers each time, with no
model call, and is the same every time. When corrections are in force:
result.correctionssummarizes them: how many are in force and reverted, the rows, positions and months they touch, and each underwriting figure that changed.- Each changed transaction, position and month carries
correction, with the computed value next to the corrected one:
GET /v1/jobs/{id}?view=computed returns the result exactly as the pipeline produced it.
Corrections never change a job’s status, a statement’s verification, or review_reasons. A job that needed
review still says so.
History and reverting
GET /v1/jobs/{id}/corrections lists every correction, oldest first, reverted ones included.
Telling your CRM
Subscribe a webhook tojob.corrected to hear about every correction and revert:
current_withhold_pct, positions, review_reasons, …) and
corrections, the summary above. Fetch result_path for the corrected result.
job.corrected is opt-in: an endpoint registered with an empty topics list does not receive it, so a CRM that
only expects finished jobs never sees a second event for the same job. See Webhooks.
The CRM integrations take job.corrected too, and update the deal with the corrected figures: every mapping
reads the corrected result. HubSpot and Salesforce integrations installed from now on take it by default; a
generic CRM push, and any integration installed earlier, take it once you add it to their config.events. See
Integrations.
What cannot be corrected, and why
Amounts, dates and balances cannot be edited, and rows cannot be added or removed. A verified ledger reconciles to the cent with the bank’s printed totals and running balances; editing it would produce numbers that look verified and are not. If a statement was read wrong, send a better copy of it in a new job.Erasure
Corrections belong to their job:DELETE /v1/jobs/{id} erases them with it, and a tenant’s retention period
erases them when it erases the job.