Skip to main content
A reviewer’s fixes are saved on the job. Every read of the job applies them, so your team, your CRM and the next person to open the deal see the same corrected numbers. The machine’s result is kept as it was, and every correction has a history: who made it, when, why, and who reverted it. Corrections need the 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:
The answer (201) lists the corrections saved and a summary of what the corrections in force change.

Transactions

A row is named by its statement’s document_id and its index in statements[].transactions. Both are fixed for the life of the job.
  • category must 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,020depositfrom‘revenue‘to‘transferin‘lowersthatmonth′struerevenueandEBITDAby2,020 deposit from `revenue` to `transfer_in` lowers that month's true revenue and EBITDA by 2,020 and raises its transfers in by $2,020, and the averages and withhold follow.
  • counterparty is 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.
Every row of a verified statement the result is computed from can be re-categorized. Two fields on the row say how its month counted it, and the correction starts from them:
  • counted_as, on an mca_payment row: the category its month counted it under. It is mca_payment, unless a position decision on its payment stream counted it otherwise: loan_payment when the stream is a business loan, or the stream’s own category (for example operating_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): true when 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.
On a job whose 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 its key (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:
The month is merged again from its other accounts, so it and the averages lose exactly that account’s figures. The account stays in 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.corrections summarizes 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.
A revert takes the correction out of force and keeps it in the history with who reverted it, when and why. Per field, the latest correction in force wins, so reverting one brings back the value before it: an earlier correction still in force, else the computed value. Reverting every correction of a job restores the computed result exactly. Every correction and revert is recorded in the audit log.

Telling your CRM

Subscribe a webhook to job.corrected to hear about every correction and revert:
Its payload is the job’s summary as corrected (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.