> 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/rest-api/disputes.md).

# Disputes

A **dispute** is a debtor's formal challenge to a placement. When a dispute is opened, the placement transitions to `disputed` status and outreach is paused (the compliance engine refuses calls/emails while the placement is in `disputed`).

## How disputes are created

Disputes are filed by the **debtor through the portal**, not by the creditor through the partner API:

```
POST /v1/portal/disputes
Authorization: Bearer <portal JWT, OTP-verified>
Content-Type: application/json

{
  "reason": "amount_disputed",
  "narrative": "We paid invoice INV-001 on 2026-04-01..."
}
```

The platform writes a `Dispute` row (`status=open`), sets `placement.status=disputed`, sets `placement.dispute_reason`, and writes a `dispute_filed` audit event.

There is no public `POST /v1/disputes` endpoint for the creditor to file a dispute on the debtor's behalf today.

## Endpoints (creditor-facing)

| Method | Path                                                | Purpose                                                                                                            |
| ------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `GET`  | `/v1/placements/{placement_id}/dispute`             | Return the most recent dispute on a placement (with originating audit event as `trigger`). 404 if none.            |
| `POST` | `/v1/placements/{placement_id}/dispute/acknowledge` | Flip the most recent open dispute to `reviewing`. Optional body `{"note": "..."}`.                                 |
| `GET`  | `/v1/disputes`                                      | List all disputes for the tenant. Optional query params: `placement_id=`, `status=`, `limit=` (1–200, default 50). |
| `POST` | `/v1/disputes/{dispute_id}/resolve`                 | Terminal adjudication. Required body: \`{"resolution": "denied"                                                    |
| `GET`  | `/v1/disputes/{dispute_id}/evidence`                | List debtor-uploaded evidence records with short-lived signed download URLs (1-hour TTL).                          |

## Endpoints (debtor portal, JWT-authenticated)

| Method | Path                                | Purpose                                                                                                              |
| ------ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `POST` | `/v1/portal/disputes`               | Debtor opens a dispute. Requires OTP-verified portal session.                                                        |
| `POST` | `/v1/portal/evidence/signed-upload` | Request a short-lived signed upload URL for evidence attached to a dispute.                                          |
| `POST` | `/v1/portal/evidence/confirm`       | Record a completed evidence upload after a successful direct PUT to storage. Idempotent on `(dispute_id, filename)`. |

## Dispute status

`Dispute.status` enum:

* `open` — filed by the debtor; placement is `disputed`; outreach paused.
* `reviewing` — creditor has acknowledged. Placement remains `disputed`; outreach still paused.
* `auto_responded` — the dispute auto-response agent assembled an evidence packet and emailed the debtor a templated reply.
* `awaiting_human` — routed to a human reviewer instead of an automated reply; the placement remains paused.
* `awaiting_evidence` — the auto-response matrix could resolve the dispute if a creditor-supplied document (invoice, proof of service, or contract) were on file, so the platform has emailed the creditor a time-boxed request for those documents (5-day SLA). The dispute is parked pending the creditor's response. If documents arrive, the matrix re-runs and may auto-respond; if the SLA lapses without a response, the dispute hands off to `awaiting_human`.
* `upheld` / `denied` / `withdrawn` — terminal resolutions.

While a dispute sits in `open`, `reviewing`, `awaiting_human`, or `awaiting_evidence`, payout disbursement on the placement is held.

`PATCH /v1/disputes/{id}` and `POST /v1/disputes/{id}/attachments` are not implemented.

## Resolution

`POST /v1/disputes/{dispute_id}/resolve` terminally adjudicates a dispute. Required fields:

| Field                   | Type   | Values                                                                                                                                              |
| ----------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolution`            | string | `denied` \| `upheld` \| `withdrawn`                                                                                                                 |
| `note`                  | string | Required. 2–2000 characters.                                                                                                                        |
| `verification_attested` | bool   | Optional. Set `true` if you sent a §809(b) debt-verification notice out-of-band (only matters when the dispute was never auto-responded in-system). |

Side effects by outcome:

| Outcome                 | Placement                      | Sequences                                                      | Disbursement holds                      |
| ----------------------- | ------------------------------ | -------------------------------------------------------------- | --------------------------------------- |
| `denied` or `withdrawn` | Returns to `in_outreach`       | `paused_dispute` sequences resume (after cadence-resume delay) | Released                                |
| `upheld`                | Closes as `closed_uncollected` | Retired permanently                                            | Pending remittances → `manual_required` |

The response includes `resume_blocked_reason` when outreach couldn't be resumed (e.g. `verification_not_sent`, `compliance_blocked`) so you can take corrective action. A `409` is returned if the dispute is already in a terminal state.

The `dispute.resolved` transition is recorded in the placement audit trail (`GET /v1/placements/{id}/audit`). There is no outbound webhook for this event.

## Acknowledge example

```bash
curl -X POST https://<api-host>/v1/placements/$PLACEMENT_ID/dispute/acknowledge \
  -H "Authorization: Bearer $MC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Investigating — owner is reviewing PO history."}'
```

Response:

```json
{
  "placement_id": "<uuid>",
  "dispute_id": "<uuid>",
  "status": "reviewing",
  "acknowledged_at": "2026-04-20T10:00:00Z"
}
```

Errors:

* `409` `{"detail": "Placement is not disputed (current status: in_outreach)"}` — placement is not in `disputed` status.
* `404` `{"detail": "No dispute record on file for this placement"}` — no dispute row exists.
* `409` `{"detail": {"reason": "already_acknowledged", "current_state": "reviewing"}}` — dispute was already acknowledged.

## Evidence-request fields (on dispute list response)

When a dispute is in `awaiting_evidence`, the dispute object in `GET /v1/disputes` and `GET /v1/placements/{id}/dispute` includes three additional fields:

| Field                      | Type                    | Description                                                                                                                                            |
| -------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `evidence_request_status`  | string \| null          | `requested` — open; `fulfilled` — docs arrived; `expired` — SLA lapsed; `cancelled` — dispute resolved before response. `null` when no request exists. |
| `evidence_requested_types` | string\[] \| null       | Document types requested from the creditor — subset of `invoice`, `proof_of_service`, `contract`.                                                      |
| `evidence_due_at`          | ISO 8601 string \| null | Deadline for the creditor to supply documents.                                                                                                         |

Two additional fields appear on every dispute object regardless of status:

| Field      | Type            | Description                                                                                                                                                                                                        |
| ---------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `age_days` | integer \| null | Whole days since the dispute was filed (`Dispute.created_at`). Useful for triage ordering.                                                                                                                         |
| `overdue`  | bool            | `true` when the dispute is still unresolved and older than the human-review SLA (default 3 days, configurable via `DISPUTE_HUMAN_REVIEW_SLA_DAYS`). Drives dashboard triage — overdue disputes are surfaced first. |

## Evidence attachments

Evidence is uploaded by the debtor via a short-lived signed upload URL (`POST /v1/portal/evidence/signed-upload`), followed by a confirm call (`POST /v1/portal/evidence/confirm`) which registers the upload. Creditors can list the resulting evidence records — including short-lived signed download URLs — via `GET /v1/disputes/{dispute_id}/evidence`. There is no creditor-side evidence-upload API endpoint; creditor documents are supplied in response to an evidence-request email (see `awaiting_evidence` status above).

## SLA and auto-acceptance

Per-pilot SLAs (creditor response window, auto-acceptance under a configurable dollar amount) are not exposed via the public API today.

## See also

* [Webhooks: placement.disputed](/webhooks/events/placement.disputed.md)
* [Guides: handle disputes end-to-end](/integration-guides/handle-disputes-end-to-end.md)
* [Compliance for integrators: FDCPA](/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/rest-api/disputes.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.
