> 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/ingest-overdue-invoices.md).

# Ingest overdue invoices

The most common integration shape: a nightly (or hourly) job on your side selects invoices that have aged past a threshold and forwards them to us as placements.

## Shape

```
your accounting system
  → query "all unpaid invoices where due_date < (today - N days)"
  → for each, check "is this already filed with collections?"
  → for new ones: POST /v1/placements with a unique invoice_number
  → mark in your system: status="submitted_to_collections", placement_id=<returned>
  → react to outbound webhooks (placement.first_contact, placement.status_changed,
    placement.disputed, call.completed, payment.commitment_made) to keep your
    system in sync
```

Four intake channels exist; choose the one that matches your shape:

| Channel                       | Endpoint                                               | Notes                                                                                                                                                                                                                                                                  |
| ----------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Synchronous JSON              | `POST /v1/placements`                                  | One placement per call. Used in this guide.                                                                                                                                                                                                                            |
| HMAC-signed JSON (structured) | `POST /v1/intake/structured`                           | Per-creditor secret rotated via `POST /v1/intake/webhook/rotate-secret`. `X-MC-Signature` HMAC required, `Idempotency-Key` header required, 1 MB body cap. `POST /v1/intake/webhook` is a deprecated alias for this endpoint and continues to work with the same auth. |
| CSV bulk                      | `POST /v1/intake/csv/preview` then `/csv/commit`       | Two-step (preview -> human-confirm column mappings -> commit). 20 MB body cap.                                                                                                                                                                                         |
| File drop (PDF / image)       | `POST /v1/intake/file` then `/file/{draft_id}/confirm` | "Draft, not posted" — extracts a placement draft from an invoice PDF / image; nothing is created until the explicit confirm. PDF cap 10 MB, image cap 5 MB.                                                                                                            |

## Step 1 — pick a threshold

Common policies:

| Policy                  | When to file                                      |
| ----------------------- | ------------------------------------------------- |
| Aggressive              | 30 days past due                                  |
| Standard B2B            | 60 days past due                                  |
| Relationship-preserving | 90 days past due, after internal dunning attempts |

We strongly recommend NOT filing earlier than 30 days. Earlier placements rarely recover meaningfully more than waiting, and they hurt the creditor-debtor relationship.

## Step 2 — query your AR system

Whatever your source, build a payload that matches the `PlacementCreateRequest` schema. Required fields are `invoice_amount` (decimal, >0) and `debtor.company_name`; everything else is optional but recommended.

```python
{
  "invoice_number": "INV-2024-0042",         # used for duplicate detection
  "invoice_amount": 7500.00,                  # required, must be > 0
  "currency": "USD",                          # defaults to USD
  "invoice_date": "2025-11-01",
  "due_date": "2025-12-01",
  "posture": "preserve",                      # "preserve" | "escalate"; falls back to creditor.posture_default
  "debtor": {
    "company_name": "Acme Corp",              # required
    "primary_domain": "acme.com",
    "primary_email": "ap@acme.com",
    "primary_phone": "+15105550101",          # E.164
    "state": "TX",                            # drives quiet hours + state compliance rules
    "city": "Austin",
    "zip": "78701"
  },
  "notes": "October consulting engagement",
  "callback_url": "https://your-app.example.com/webhooks/mc"  # optional per-placement override; HTTPS only, public IP, no RFC1918 / localhost
}
```

## Step 3 — check for existing placements

You DON'T need to check on our side — `POST /v1/placements` returns `409 Duplicate invoice: <invoice_number>` when `(creditor_id, invoice_number)` already exists on a live row. You DO need to know on your side which invoices are already submitted, so you don't redundantly hit our rate limit.

```sql
-- on your side
SELECT invoice_id, mc_placement_id
FROM invoices
WHERE collections_status = 'submitted' AND invoice_id IN (...);
```

If `mc_placement_id IS NOT NULL`, skip — already filed.

## Step 4 — POST /v1/placements

The synchronous JSON endpoint is the most common shape:

```bash
curl -X POST https://api.moderncollections.io/v1/placements \
  -H "Authorization: Bearer $MC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_number": "INV-2024-0042",
    "invoice_amount": 7500.00,
    "due_date": "2025-12-01",
    "debtor": {"company_name": "Acme Corp", "state": "TX", "primary_email": "ap@acme.com"}
  }'
```

A successful response is `201`:

```json
{
  "placement_id": "...",
  "status": "pending_enrichment",
  "debtor_id": "...",
  "estimated_first_contact": "2025-12-02T18:14:00Z",
  "created_at": "2025-12-02T18:00:00Z"
}
```

Key patterns:

* **Duplicate detection is server-side.** Re-POSTing the same `invoice_number` returns `409`. There is no `invoice_number` query filter on `GET /v1/placements` — keep the returned `placement_id` keyed by invoice on your side, or page through `GET /v1/placements` (each list item includes `invoice_number`) to re-match.
* **SOL-time-barred invoices** are routed to the operator review queue instead of minting a placement — the response carries that signal so you can surface it.
* **High-value invoices** (default >= $250,000) get an automatic OFAC/sanctions screen on the debtor entity. Outreach pauses on a hit; the placement itself still succeeds.
* **Compliance evaluation runs synchronously before the response returns.** Outreach is gated by the next `can_call=True` decision but the placement is always created.

### Bulk variant — CSV intake

For nightly batches of more than a handful of placements, the CSV channel is cheaper. Upload, confirm the auto-discovered column mapping, then commit:

```bash
curl -X POST https://api.moderncollections.io/v1/intake/csv/preview \
  -H "Authorization: Bearer $MC_API_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @overdue.csv
# response: { "preview_id": "...", "parsed_count": 142, "mappings": {...},
#             "requires_date_locale": true, "ambiguous_date_rows": [3, 17] }

curl -X POST https://api.moderncollections.io/v1/intake/csv/commit \
  -H "Authorization: Bearer $MC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"preview_id": "...", "column_mappings": {...}, "date_locale": "us", "default_currency": "USD"}'
```

CSV preview reads unambiguous ISO dates (`YYYY-MM-DD`) directly; anything ambiguous (e.g. `04/03/2025`) is surfaced as a row-level warning so the operator can pick `us` (MM/DD) or `eu` (DD/MM) at commit time. **At commit, `date_locale` now defaults to `us` (MM/DD) when omitted** — previously an omitted locale would drop ambiguous US slash-dates to a null due-date. Always send an explicit `date_locale` if your source uses `DD/MM`; the dashboard does this for you.

## Step 5 — handle webhooks

Register a webhook URL once (set `creditor.webhook_url` via `PATCH /v1/settings`, or per-placement `callback_url` at creation). The events actually dispatched by the platform today:

| Event                      | Dispatched when                                                                                                                                                                                                               | Update your system                                                         |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `placement.first_contact`  | First completed call to the debtor (`contact_method: "call"`; email sends do not fire it today)                                                                                                                               | Mark "outreach started"                                                    |
| `placement.status_changed` | Placement reaches `paid` via the payment-received API, or is recalled. (Portal card payments don't dispatch it today — see [Reconcile payments](/integration-guides/reconcile-payments-back-to-accounting.md).)               | Update your `collections_status`; record `recovered_amount` + `fee_amount` |
| `placement.disputed`       | A call or email reply surfaces dispute language. (A dispute filed via the debtor portal flips the placement to `disputed` but does not dispatch this webhook today — watch placement status / the audit trail for that path.) | Pause your own dunning                                                     |
| `call.completed`           | Voice call ends and post-call extraction completes                                                                                                                                                                            | Optionally log transcript + outcome                                        |
| `payment.commitment_made`  | Debtor commits to a payment date or installment plan                                                                                                                                                                          | Pause dunning until commitment date                                        |

Outbound delivery is HMAC-signed (see [signature verification](/webhooks/signature-verification.md)). Per-placement `callback_url` overrides `creditor.webhook_url` and is validated at creation time against the SSRF guard (HTTPS only, no RFC1918 / loopback / link-local).

## Common pitfalls

| Pitfall                                                                                                | Fix                                                                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cron job runs before your AR system has reconciled yesterday's payments → filing already-paid invoices | Run AFTER reconciliation, OR query for `paid_at IS NULL` with a 24h grace window                                                                                                                                                |
| State field is missing or non-US format on some debtors                                                | Pre-filter and require state before submitting; fall back to manual triage                                                                                                                                                      |
| Phone numbers in different formats                                                                     | Normalize to E.164 on your side. The intake channels normalize fail-soft (an unparseable phone is dropped to `null`, not rejected), and the voice dialer expects E.164 — a malformed phone silently costs you the call channel. |
| Domain not set on small-business debtors                                                               | Recommended but not required. We can still file.                                                                                                                                                                                |
| Cron job hits 429 on a big backlog                                                                     | Add exponential backoff and honor `Retry-After` yourself. The `mc_api` client only auto-retries idempotent `GET`s — `POST /v1/placements` is never auto-retried (by design, so a create can't be duplicated).                   |

## See also

* [API reference: placements](/rest-api/placements.md)
* [Webhooks overview](/webhooks/overview.md)
* [Reconcile payments back to accounting](/integration-guides/reconcile-payments-back-to-accounting.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/ingest-overdue-invoices.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.
