> 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/reconcile-payments-back-to-accounting.md).

# Reconcile payments back to accounting

When a debtor pays — either by card through the debtor portal (processed via Stripe) or via an out-of-band wire / check recorded against the placement — the platform mints a `Remittance` ledger row, snapshots `placement.recovered_amount` and `placement.fee_amount`, and (if the placement was fully recovered) transitions it to `paid`. From there, two paths put the payment into your accounting system.

> **Status:** the only first-class accounting integration today is **QuickBooks Online**. Xero / NetSuite / Codat / Rutter / Merge are not implemented. If you use anything other than QBO, run the webhook path below.

## Path A — QuickBooks Online (recommended if you use QBO)

Connect once via OAuth, after which the platform writes payments back to QBO automatically.

### Connect

```bash
# 1. Get an authorize URL
curl -X POST https://api.moderncollections.io/v1/integrations/qbo/connect-url \
  -H "Authorization: Bearer $MC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"return_to": "/settings"}'
# response: {"authorize_url": "https://appcenter.intuit.com/...", "state": "...", "expires_at": "..."}

# 2. Open authorize_url in a browser, sign in to QBO, authorize the app.
#    Intuit posts the code back to /v1/integrations/qbo/callback, which
#    redirects the browser to the dashboard with `?qbo=connected`.
```

### Check status

```bash
curl https://api.moderncollections.io/v1/integrations/qbo \
  -H "Authorization: Bearer $MC_API_KEY"
# response: {"connected": true, "realm_name": "Acme Corp QBO", "realm_id": "...",
#            "last_sync_at": "...", "expires_at": "...", "status": "connected"}
```

### Disconnect

```bash
curl -X DELETE https://api.moderncollections.io/v1/integrations/qbo \
  -H "Authorization: Bearer $MC_API_KEY"
```

### Observe the writeback

Every QBO operation appears in the sync log:

```bash
curl 'https://api.moderncollections.io/v1/integrations/qbo/sync-log?filter=payment' \
  -H "Authorization: Bearer $MC_API_KEY"
```

The platform writes `QBO Payment` records against the originating invoice, snapshots the writeback timestamp on the placement, and runs a periodic drift-detection job that compares the platform's running balance against QBO's open `Balance`. Drifts above the threshold surface as audit events; the platform never auto-corrects QBO — drift resolution is a human call.

## Path B — webhook handler (for non-QBO AR systems)

If you run Xero, NetSuite, or your own AR system, listen for outbound webhooks and update your books from your handler.

### The flow

```
payment lands against the placement
  -> we credit the placement (recovered_amount / fee_amount) and mint a Remittance row
       -> placement.status_changed webhook is dispatched when the placement
          reaches a terminal status via the payment-received API or a recall
       -> your handler verifies signature
       -> looks up your internal invoice by mc_placement_id
       -> marks invoice paid in your AR system
       -> records the contingency fee as an expense
```

{% hint style="warning" %}
**Portal card payments do not dispatch `placement.status_changed` today.** The webhook fires when a payment is recorded via `POST /v1/placements/{id}/payment-received` (and the placement reaches `paid`), or on recall. For payments made through the debtor portal, poll `GET /v1/payments` / the remittance endpoints or watch the audit trail (`payment_succeeded` events) — don't rely on the webhook alone.
{% endhint %}

### Step 1 — verify the webhook

See [signature verification](/webhooks/signature-verification.md).

### Step 2 — handle the `placement.status_changed` event

The event today carries `placement_id`, `previous_status`, `new_status`, `recovered_amount`, and `fee_amount` — **at the top level of the JSON body** (alongside `event` and `timestamp`; there is no nested `payload` wrapper). There is no standalone `payment.received` outbound webhook — payment events surface as `placement.status_changed` (when the placement reaches `paid` via the payment-received API) and `payment.commitment_made` (when a debtor commits to a date / installment plan).

The signature arrives in the `X-Signature` header (HMAC-SHA256 over `"{timestamp}.{body}"`, with the timestamp from `X-Webhook-Timestamp`):

```python
@app.post("/webhooks/moderncollections")
async def handle_webhook(request):
    body = await request.body()
    timestamp = request.headers.get("X-Webhook-Timestamp")
    if not verify_signature(body, timestamp, request.headers.get("X-Signature"), ...):
        raise HTTPException(401)

    event = json.loads(body)

    if event["event"] == "placement.status_changed" and event["new_status"] == "paid":
        await reconcile_payment(event, timestamp, body)

    return {"ok": True}


async def reconcile_payment(event, timestamp, body):
    placement_id = event["placement_id"]

    # 1. Find your internal invoice
    invoice = await find_invoice_by_mc_placement_id(placement_id)
    if not invoice:
        log_unmatched(event)
        return

    # 2. Idempotency: retries re-send the SAME body and timestamp, so a
    #    hash of (timestamp, body) uniquely identifies one delivery.
    delivery_key = sha256(timestamp.encode() + b"." + body).hexdigest()
    if await already_processed(delivery_key):
        return

    # 3. Update your AR system
    await mark_invoice_paid(
        invoice_id=invoice["id"],
        paid_amount=event["recovered_amount"],
    )

    # 4. Record the contingency fee as an expense
    if event.get("fee_amount", 0) > 0:
        await record_collections_fee(
            invoice_id=invoice["id"],
            fee=event["fee_amount"],
        )

    # 5. Mark the delivery processed
    await mark_delivery_processed(delivery_key)
```

### Step 3 — pull the full remittance ledger

For monthly reconciliation, the partner remittance endpoints summarise everything the webhook stream gave you:

```bash
curl 'https://api.moderncollections.io/v1/payments?page=1&page_size=50' \
  -H "Authorization: Bearer $MC_API_KEY"
```

The response includes `commitments` and `attempts`; attempt rows carry `amount_cents`, `status`, and `stripe_payment_intent_id` for cross-referencing. The remittance ledger itself — `recovered_amount`, `fee_amount`, `net_remit_amount` per payout — is at `GET /v1/creditors/me/remittances` (or per placement at `GET /v1/placements/{id}/remittance`).

### Step 4 — handle partial payments

Partial recoveries do not flip the placement to `paid` and therefore do not currently dispatch a `placement.status_changed` webhook. If you need to react to every payment (partial or full), poll the partner payments endpoints or watch the audit trail (`payment.received` event\_type written by the payment-received API path; `payment_succeeded` written by the Stripe webhook path).

## Idempotency checklist

Webhooks may arrive more than once. Be idempotent:

* The payload carries no delivery id; retries re-send the identical body and `X-Webhook-Timestamp`. Key the OUTER dedupe on a hash of `(timestamp, body)` — or on `(event, placement_id, new_status)`, which is naturally idempotent for terminal transitions.
* Cross-check by `stripe_payment_intent_id` (when present) for the INNER dedupe.
* The platform's own remittance ledger guarantees one ledger row per payment attempt, so duplicates can't originate on our side.

## What to do when reconciliation fails

If your accounting system rejects the entry:

1. Return 5xx from your webhook handler so the platform retries.
2. Log the `X-Webhook-Timestamp`, `event`, and `placement_id`.
3. Alert your team.
4. Fix the underlying issue.
5. The webhook follows the [retry policy](/webhooks/retries.md).

If it still fails after the retry window, the delivery is dead-lettered — open a support ticket to have it replayed.

## See also

* [Signature verification](/webhooks/signature-verification.md)
* [REST API: payments](/rest-api/payments.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/reconcile-payments-back-to-accounting.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.
