> For the complete documentation index, see [llms.txt](https://docs.moderncollections.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.moderncollections.io/integration-guides/handle-disputes-end-to-end.md).

# Handle disputes end-to-end

Disputes are inevitable. This guide covers the dispute flow that ships today: a debtor opens a dispute through the debtor portal (or surfaces dispute language during a call / email), outreach is paused, and the creditor acknowledges the dispute through the dashboard or API.

## Lifecycle

```
open → reviewing  (via POST /v1/placements/{id}/dispute/acknowledge)
reviewing → denied | upheld | withdrawn  (via POST /v1/disputes/{id}/resolve)
```

`DisputeStatus` accepts: `open`, `reviewing`, `upheld`, `denied`, `withdrawn`, `auto_responded`, `awaiting_human`, `awaiting_evidence`. `auto_responded`, `awaiting_human`, and `awaiting_evidence` are set by internal services; `denied`, `upheld`, and `withdrawn` are set by `POST /v1/disputes/{id}/resolve`.

`awaiting_evidence` means the platform has emailed the creditor requesting validating documents (invoice / proof of service / contract) needed to auto-respond to the dispute. The dispute is parked until documents arrive or the 5-day SLA expires. No action from the integrator is required — the platform handles follow-up automatically.

When a placement's status is `disputed` the compliance engine blocks every outreach attempt — calls and emails alike — until the placement transitions out.

## Step 1 — surface the dispute

Three triggers create a dispute row:

1. **Debtor files via portal:** `POST /v1/portal/disputes` with `{"reason": "...", "narrative": "..."}` (debtor JWT, OTP-verified). The placement is flipped to `status=disputed` in the same transaction; an `audit_event` of type `dispute_filed` is written.
2. **Voice agent captures the dispute:** any of the hard-stop dispute phrases ("I dispute this", "I don't owe this") fire the `capture_dispute` tool inside the call; this also writes the dispute row and pauses outreach.
3. **Email reply classified as a dispute:** the inbound reply pipeline opens a dispute and dispatches the `placement.disputed` outbound webhook.

You learn about it via the `placement.disputed` outbound webhook (not `dispute.opened` — that event name is not dispatched today). All three triggers — portal, call, and email-reply — dispatch it identically today.

## Step 2 — acknowledge

```bash
curl -X POST https://api.moderncollections.io/v1/placements/{placement_id}/dispute/acknowledge \
  -H "Authorization: Bearer $MC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Ops reviewing. Will respond by Friday."}'
```

Effects:

* Flips the most recent open dispute row from `open` -> `reviewing`.
* Writes a `dispute.acknowledged` audit event with `previous_status`, `new_status`, and your optional `note`.
* Outreach stays paused — the placement remains `status=disputed`.

Returns `409` if the placement is not currently `disputed`, or if the most recent dispute is not in `open`. Returns `404` if no dispute row exists for the placement.

## Step 3 — read the dispute

```bash
curl https://api.moderncollections.io/v1/placements/{placement_id}/dispute \
  -H "Authorization: Bearer $MC_API_KEY"
```

Returns the most recent dispute row plus a `trigger` summary derived from the nearest `email.reply_received`, `call.completed`, or `dispute_filed` audit event so you can see what created the dispute.

## Step 4 — debtor uploads evidence

Debtors can attach evidence via the portal:

```
POST /v1/portal/evidence/signed-upload
  body: {"dispute_id": "...", "filename": "signoff.pdf", "mime_type": "application/pdf"}
```

The platform returns a short-lived signed upload URL. Uploaded files are attached to the dispute as evidence records.

There is no creditor-side evidence-upload API endpoint today. When the dispute matrix determines that a creditor-supplied document could resolve the dispute, the platform emails the creditor directly with a request and a deadline; once the documents arrive via the dashboard the matrix re-runs automatically.

## Step 5 — resolution

```bash
curl -X POST https://api.moderncollections.io/v1/disputes/$DISPUTE_ID/resolve \
  -H "Authorization: Bearer $MC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"resolution": "denied", "note": "Invoice INV-001 confirmed valid; debtor notified."}'
```

`resolution` must be one of `denied`, `upheld`, or `withdrawn`:

* **`denied`** — debt stands. Disbursement holds are released. Outreach resumes — placement moves to `in_outreach` and `paused_dispute` sequences restart — **only when FDCPA §809(b) verification is proved**: either the in-system auto-response agent already sent the notice (dispute status was `auto_responded`), or you pass `"verification_attested": true` in this request to attest that you sent it out-of-band. Without proved verification the placement stays in `disputed` status and sequences stay in `paused_dispute`; an operator must review before outreach can continue.
* **`upheld`** — debt invalid. Placement closes as `closed_uncollected`, sequences are retired, and pending remittances move to `manual_required` for operator review.
* **`withdrawn`** — debtor withdrew. Disbursement and outreach-resumption rules are the same as `denied`: holds release unconditionally; outreach resumes only once verification is proved.

`note` is required (2–2000 chars). The response includes a `resume_blocked_reason` field when outreach couldn't resume: `verification_not_sent` means the platform can't confirm §809(b) notice was sent — pass `"verification_attested": true` if you sent it out-of-band; `compliance_blocked` means the compliance engine vetoed the resume regardless of verification status. Returns `409` if the dispute is already resolved.

The `dispute.resolved` event is written to the placement audit trail; there is no outbound webhook for this transition today.

## Outbound webhook today

| Event                | When                                                                                                                                     |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `placement.disputed` | A dispute row is created — via the call path (`capture_dispute`), an email-reply, or a portal filing. All three dispatch it identically. |

`dispute.opened`, `dispute.resolved`, and `plan.installment.charged` are not dispatched as outbound webhooks today. The dashboard surfaces dispute state changes; check the placement's audit trail (`GET /v1/placements/{id}/audit`) for the authoritative status history.

## Common pitfalls

| Pitfall                                                            | Fix                                                                                                                    |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| Forgot to assign disputes to a human owner -> they sit unaddressed | Wire `placement.disputed` to a Slack channel or ticketing system                                                       |
| Continuing your own dunning while the placement is `disputed`      | FDCPA-style risk on consumer / sole-prop debt — coordinate with the platform's pause                                   |
| Acknowledging a dispute twice                                      | `POST /dispute/acknowledge` returns `409 already_acknowledged` if the current status is not `open` — handle gracefully |

## See also

* [REST API: placements](/rest-api/placements.md)
* [Compliance: FDCPA for integrators](/compliance-for-integrators/fdcpa-for-integrators.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.moderncollections.io/integration-guides/handle-disputes-end-to-end.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
