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

# Nomba Webhook Events & Payment Reconciliation

> How Vouch uses Nomba webhooks to reconcile payments, handle partial and over-payments, and advance escrow agreements automatically.

Vouch uses a webhook-driven payment reconciliation model. When a buyer transfers funds into a Nomba Virtual Account, Nomba fires a `virtual_account.funded` event directly to the Vouch backend. Vouch validates the signature, reconciles the payment, and advances the escrow state machine automatically — no polling or manual confirmation needed from your integration.

***

## How It Works

```
Buyer transfers funds
        │
        ▼
 Nomba Virtual Account
        │
        ▼
 Nomba fires POST → /escrow/webhooks/nomba
        │
        ├─ Signature verified (HMAC-SHA256)
        ├─ 200 acknowledged immediately
        └─ Async reconcile:
              ├─ Match account number → Agreement
              ├─ Idempotency check (nombaReference unique)
              ├─ Accumulate amountReceived
              ├─ Derive new status (PARTIAL / FUNDED / OVERFUNDED)
              ├─ Auto-refund excess if OVERFUNDED
              └─ Advance escrow state machine
```

You do not need to register or configure anything for this to work. The webhook endpoint is already configured on the Nomba dashboard for your sub-account.

***

## Signature Verification

Every incoming Nomba request is signed with **HMAC-SHA256** over the raw request body, delivered in the `nomba-signature` header. Vouch verifies this before processing any event — requests with a missing or mismatched signature are rejected with `401`.

<Note>
  The signature is computed over the raw request bytes. The Vouch backend applies `express.raw()` middleware to this route to prevent JSON parsing from modifying the body before the signature check.
</Note>

***

## Supported Event Types

| Event Type               | Handled                                   |
| ------------------------ | ----------------------------------------- |
| `virtual_account.funded` | ✅ Fully handled — triggers reconciliation |
| All other types          | Acknowledged and ignored                  |

***

## Partial Payments

If a buyer sends multiple transfers that collectively equal the agreed amount, Vouch handles it automatically. Each `virtual_account.funded` event increments the running `amountReceived` total:

| `amountReceived` vs `totalAmount`     | Status       |
| ------------------------------------- | ------------ |
| `amountReceived < totalAmount`        | `PARTIAL`    |
| `amountReceived >= totalAmount`       | `FUNDED`     |
| `amountReceived > totalAmount × 1.01` | `OVERFUNDED` |

Your integration only needs to watch for `FUNDED` before allowing work to begin. You do not need to track individual transfers.

***

## Overpayment Handling

When the accumulated `amountReceived` exceeds `totalAmount` by more than 1%, Vouch:

1. Sets the agreement to `OVERFUNDED`
2. Calculates `excess = amountReceived - totalAmount`
3. Reads the sender's bank details from the webhook payload
4. Immediately calls the Nomba transfer API to refund the excess to the original sender
5. Logs the outcome in your developer audit log as `OVERPAYMENT_FLAGGED`

<Note>
  Automatic refunds require the sender's account number and bank code to be present in the Nomba webhook payload. If these details are missing, the refund is skipped and logged with a reason — you can handle it manually from the developer dashboard.
</Note>

***

## Idempotency

Every Nomba event carries a unique `requestId`. Vouch stores this as `nombaReference` on the transfer record, which has a database-level unique constraint. If Nomba retries a delivery (common in production), the duplicate is detected and silently dropped — the agreement is never double-credited.

***

## Developer Audit Log Events

Every reconciliation writes a structured entry to your developer log, visible in the dashboard:

| `eventType`              | When                                                   |
| ------------------------ | ------------------------------------------------------ |
| `RECONCILIATION_MATCHED` | Payment matched and `amountReceived` updated           |
| `OVERPAYMENT_FLAGGED`    | Payment exceeded tolerance; automatic refund attempted |
| `ESCROW_FUNDED`          | Agreement advanced to `FUNDED`                         |
| `ESCROW_FUND_FAILED`     | An error occurred during state machine advancement     |

***

## Polling as a Fallback

If you need to check agreement status from your frontend or in a background job, use `vouch.escrow.status()` at any time:

```typescript theme={null}
import Vouch from 'vouch-sdk';

const vouch = new Vouch('your-api-key');

const agreement = await vouch.escrow.status('agr_abc123');
console.log(agreement.status); // 'PENDING' | 'PARTIAL' | 'FUNDED' | ...
```

For waiting on payment confirmation, a lightweight polling loop with a 5-second interval is a reliable approach for most bank transfer windows:

```typescript theme={null}
async function waitForFunding(agreementId: string, maxAttempts = 24) {
  for (let i = 0; i < maxAttempts; i++) {
    const agreement = await vouch.escrow.status(agreementId);
    if (['FUNDED', 'OVERFUNDED'].includes(agreement.status)) {
      return agreement;
    }
    await new Promise(r => setTimeout(r, 5000)); // wait 5 seconds
  }
  throw new Error('Funding timeout after 2 minutes');
}
```

<Tip>
  For instant bank transfers (NIP), a 5-second poll interval over 24 attempts covers a 2-minute window — enough for the vast majority of payments. For manual transfers, consider a longer window or a user-triggered re-check button.
</Tip>
