> 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/webhooks/overview.md).

# Overview

We use **outbound webhooks** to notify your system when something happens on a placement: a call completed, a payment commitment was made, a status transition fired, a dispute was opened. Webhooks are the right way to integrate. Polling the REST API is a fallback.

## Direction matters

| Direction                   | What it is                                                                                                                          |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Outbound** (we → you)     | We POST to a URL you register. This is what most integrators care about.                                                            |
| **Inbound** (provider → us) | We receive webhooks from our upstream voice, email, payment, and accounting providers. These are internal and you don't touch them. |

The rest of this section describes **outbound** webhooks.

## Registering a URL

Two ways:

1. **Per-creditor default.** Set `webhook_url` on your creditor record via `PATCH /v1/settings` with `{"webhook_url": "https://…"}`. Used for all placements unless overridden. To clear it, pass `{"webhook_url": null}`.
2. **Per-placement override.** When you call `POST /v1/placements`, include a `callback_url`. Used just for that placement.

Per-placement override is useful when different invoice types should route to different listeners on your side.

URLs must be **HTTPS** and must not resolve to a private, loopback, link-local, reserved, multicast, or otherwise unsafe IP. Hostnames like `localhost`, `127.0.0.1`, `0.0.0.0`, `::1`, or anything in `169.254.0.0/16` (cloud metadata range) are rejected. URLs are pre-resolved once and the connection is pinned to that IP to defeat DNS rebinding. A URL that fails any of these checks is silently dropped — no delivery is attempted and no retry is queued.

## Envelope

Every webhook POST looks like:

```http
POST https://yourapp.example/webhooks/moderncollections HTTP/1.1
Content-Type: application/json
X-Signature: 7f9c2ba4e88f827d616045507605853ed73b8093a3f1f3a8b9c9c8d6c0b7c1a2
X-Webhook-Timestamp: 1715520000

{
  "event": "placement.status_changed",
  "timestamp": "2026-05-12T14:32:00+00:00",
  "placement_id": "550e8400-e29b-41d4-a716-446655440000",
  "previous_status": "pending_payment",
  "new_status": "paid",
  "recovered_amount": 7500.0,
  "fee_amount": 375.0
}
```

Key header conventions:

* `X-Signature`: HMAC-SHA256 hex digest of `"{timestamp}.{body}"`, signed with the platform webhook signing secret. **Always verify.** See [signature verification](/webhooks/signature-verification.md). The value is a raw hex digest — no `sha256=` prefix.
* `X-Webhook-Timestamp`: Unix epoch seconds (UTC) when we sent the webhook. Reject deliveries whose timestamp is more than 5 minutes from your clock — that's the freshness window that defeats replay attacks.
* `Content-Type`: always `application/json`.

The event name appears in the JSON body as `event`. There is no separate `X-Event` or `X-Delivery-Id` header.

## Body shape

The body is a flat JSON object. Every event includes:

* `event` — the event type string.
* `timestamp` — ISO-8601 UTC timestamp of when the event was constructed. The authoritative timestamp for signature verification is the `X-Webhook-Timestamp` header (Unix seconds).

Event-specific fields are flattened at the top level alongside `event` and `timestamp` — they are NOT nested under a `data` object. See the per-event payload references for the exact field set.

## Acknowledgment rules

* Return **2xx** within our 10-second HTTP timeout. Anything else counts as failure and we retry.
* 4xx responses other than `408` and `429` are treated as **permanent** failures and the delivery moves straight to the dead-letter queue — no retries. The receiver has told us the request is broken.
* 5xx, `408`, `429`, connection errors, and timeouts are **transient** and trigger the retry schedule.
* Don't do business logic inline. Acknowledge fast, enqueue work, then process async.

## Event catalog

The platform currently emits five outbound events:

| Event                                                                      | When                                                                                  |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [`placement.status_changed`](/webhooks/events/placement.status_changed.md) | A placement moved between statuses (e.g. `pending_payment` → `paid`, `* → recalled`). |
| [`placement.first_contact`](/webhooks/events/placement.first_contact.md)   | The placement's first voice call completed (any outcome).                             |
| [`placement.disputed`](/webhooks/events/placement.disputed.md)             | A dispute was opened (via email reply or voice call).                                 |
| [`call.completed`](/webhooks/events/call.completed.md)                     | A voice call ended and the outcome is classified.                                     |
| [`payment.commitment_made`](/webhooks/events/payment.commitment_made.md)   | The debtor committed to a payment or payment plan during a call.                      |

New event types are announced in the [change log](https://changelog.moderncollections.io). Build your handler to ignore unrecognized `event` values.

## See also

* [Signature verification](/webhooks/signature-verification.md)
* [Retries](/webhooks/retries.md)
* [Error handling](/getting-started/error-handling.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/webhooks/overview.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.
