> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pazy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Track invoice payments with webhooks

> Confirm invoice payments as they settle, fail or reverse, instead of polling the API

After you record or trigger a payment, you need to know how it ended. Webhooks tell you the moment a payment against an invoice reaches a final outcome, so you don't have to poll. This guide shows how to keep your own invoice records in step with Pazy.

## Before you start

* [Set up a webhook endpoint](/webhooks/setup) subscribed to `payment.invoice.*`.
* Your handler [verifies signatures](/webhooks/verify-signatures) and responds quickly.

## The events

| Event | Meaning |
| - | - |
| `payment.invoice.completed` | A payment against the invoice succeeded |
| `payment.invoice.failed` | A payment failed |
| `payment.invoice.reversed` | A platform payment was refunded |

All three share the payload described in [Payment events](/webhooks/events/payment). The fields you'll use most are `paymentId`, `resource.id`, `amount`, `isFinalSettlement` and `source`. A reversal also carries `originalTransactionId`.

## Walkthrough

<Steps>
  <Step title="Record a payment">
    Call [Mark Invoice as Paid](/apis/mark-as-paid) with an `x-idempotency-key`. The `slug` you send is the invoice `id`. You can also use this guide for payments made through Pazy itself.
  </Step>

  <Step title="Receive the event">
    Pazy sends `payment.invoice.completed` with `resource.id` equal to your invoice `id`. The `source` field says how it was paid: `PLATFORM`, `MARK_AS_PAID` or `ERP`.
  </Step>

  <Step title="Match it to your record">
    Look up the invoice by `data.resource.id`. Save `paymentId` against it so you recognize a duplicate later.
  </Step>

  <Step title="Update the balance">
    A payment can be partial. Add `amount` to what you've recorded as paid. When `isFinalSettlement` is `true`, the invoice is fully settled.
  </Step>
</Steps>

<Note>
  If you also record payments with the Mark Invoice as Paid API, don't count the same payment twice: dedupe on `paymentId` and check `source`, so you can tell payments you recorded yourself from payments made in Pazy or synced from your accounting system.
</Note>

## Example handler

```javascript theme={null}
async function handleEvent(event) {
  switch (event.type) {
    case 'payment.invoice.completed':
      return recordPayment(event.data);
    case 'payment.invoice.failed':
      return flagPaymentFailed(event.data);
    case 'payment.invoice.reversed':
      return reversePayment(event.data);
    default:
      return; // ignore everything else
  }
}

async function recordPayment(p) {
  // Idempotent on paymentId: the same payment is never applied twice
  const inserted = await db.payments.insertIfAbsent({
    paymentId: p.paymentId,
    invoiceId: p.resource.id,
    amount: p.amount,
    currency: p.currency,
    utr: p.utr,
    paidAt: p.paymentDate,
  });
  if (!inserted) return; // duplicate delivery

  if (p.isFinalSettlement) {
    await db.invoices.update(p.resource.id, { status: 'PAID' });
  }
}

async function reversePayment(p) {
  // A reversal references the payment it undoes
  await db.payments.markReversed({
    invoiceId: p.resource.id,
    originalTransactionId: p.originalTransactionId,
    amount: p.amount,
  });
  await db.invoices.update(p.resource.id, { status: 'OPEN' });
}
```

## Things to get right

* **Dedupe on `paymentId`.** It's unique per payment and stable across retries.
* **Don't assume `completed` means fully paid.** Check `isFinalSettlement`.
* **Handle reversals.** They can arrive for platform payments. Payments marked as paid are never reversed.
* **Don't depend on order.** A `reversed` event can in theory be retried past a later `completed`. Compare the envelope's `createdAt` before overwriting state.
* **`failed` has no bank detail.** To check what happened to the payment, fetch the invoice with [Invoice Details](/apis/invoice-details). Its `paymentDetails` shows the payment's UTR, amount and state (`PENDING`, `FINISHED` or `FAILED`). For anything more, look at the payment in the Pazy app or ask support.
* **Reconcile occasionally.** Webhooks are reliable but not infinite-retry. A periodic [Invoice List](/apis/invoice-list) check catches anything missed during an outage, and `paymentDetails` in [Invoice Details](/apis/invoice-details) lets you confirm a payment's state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.