> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parser-app.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Review and corrections

> Fix a finished job's categories, positions and months; the fixes stay on the job and the numbers follow.

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

| `type` | Target | Sets |
| - | - | - |
| `transaction` | `document_id` and `index` of a row | `category`, `counterparty` |
| `position_override` | `position_key` | `status`, `frequency`, `payment_amount`, `funder`, `kind` |
| `month_exclusion` | `month` (`YYYY-MM`), and optionally one account's `account_key` | `excluded` |

Every correction takes an optional `reason`. Send one or several in one request; they apply in order:

```bash theme={null}
curl -sS -X POST "$API/v1/jobs/$JOB_ID/corrections" -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{
  "corrections": [
    {"type": "transaction", "document_id": "8fe8417a-...", "index": 0,
     "category": "transfer_in", "counterparty": "OWNER SAVINGS", "reason": "the owner moving savings in"},
    {"type": "position_override", "position_key": "pk_82a6aa691a4f0300",
     "frequency": "weekly", "payment_amount": "400.00", "reason": "renewed at a weekly pull"},
    {"type": "month_exclusion", "month": "2026-02", "excluded": true, "reason": "one-off insurance payout"}
  ]}'
```

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,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.

<Note>
  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.
</Note>

### 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.

| Field | Effect |
| - | - |
| `status` | `active`: counted in the existing MCA burden, whatever the date of its last pull. `paid_off` or `not_a_position`: not counted. |
| `frequency` | `daily`, `weekly` or `biweekly`. The burden converts daily x21, weekly x4, biweekly x2. |
| `payment_amount` | The payment pulled now, in dollars and cents. |
| `funder` | The funder's name. |
| `kind` | `mca` is counted in the withhold; a `loan` is reported, never counted. |

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`:

```json theme={null}
{"type": "month_exclusion", "month": "2026-03", "account_key": "harbor trust bank|8765", "excluded": true,
 "reason": "a partial export of the second account"}
```

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:

```json theme={null}
{"index": 0, "category": "transfer_in", "counterparty": "OWNER SAVINGS",
 "correction": {"correction_ids": ["5f0c..."],
                "changes": {"category": {"computed": "revenue", "corrected": "transfer_in"},
                            "counterparty": {"computed": null, "corrected": "OWNER SAVINGS"}}}}
```

`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.

```bash theme={null}
curl -sS -X POST "$API/v1/jobs/$JOB_ID/corrections/$CORRECTION_ID/revert" -H "X-API-Key: $KEY" \
     -H "Content-Type: application/json" -d '{"reason": "it was a sale after all"}'
```

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:

```bash theme={null}
curl -sS -X POST "$API/v1/webhooks" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
     -d '{"url": "https://crm.example.com/hooks/parser", "topics": ["job.completed", "job.review_required", "job.failed", "job.corrected"]}'
```

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](/guides/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](/guides/integrations#corrections).

## 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.