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

# Reporting, refunds & disputes

> Read charges in detail, issue refunds, contest or concede disputes, and pull per-TPA KPIs (volume, auth rate, fees) — everything over the API.

# Reporting, refunds & disputes

Everything on this page is TPA-scoped: pass the target TPA as `account` and authenticate with any processing key authorized for it (partner key, merchant key, or a TPA-pinned key). Works identically on `tagada.processing.*` (direct merchants) and `tagada.partners.processing.*` (partners) — same endpoints, authorization is derived from the key.

Requires `@tagadapay/node-sdk` **≥ 3.7.0**.

***

## Charges

### List

```ts theme={null}
const page = await tagada.partners.processing.charges.list({
  account: 'tpa_xxx',
  status: 'captured',          // optional
  from: '2026-07-01T00:00:00Z', // optional
  limit: 50,
});
for (const charge of page.data) {
  console.log(charge.id, charge.amountMinor, charge.currency, charge.status);
}
if (page.hasMore) {
  await tagada.partners.processing.charges.list({ account: 'tpa_xxx', cursor: page.nextCursor! });
}
```

### Retrieve (full detail)

`charges.retrieve()` returns everything the list omits — payment method and scheme variant, 3DS outcome, auth code, billing country, the **cumulative refunded amount**, and the Tagada CRM identifiers so you can deep-link a charge back to its order and customer:

```ts theme={null}
const charge = await tagada.partners.processing.charges.retrieve('psp_ref_xxx', {
  account: 'tpa_xxx',
});

console.log(charge.paymentMethodVariant); // 'visa'
console.log(charge.threeDSecure);         // 'authenticated' | 'n/a' | …
console.log(charge.authCode);             // issuer auth code
console.log(charge.orderId, charge.customerId); // Tagada CRM ids (or null)

const refundable = charge.amountMinor - charge.refundedAmountMinor;
```

### Card intelligence (BIN metadata)

Every charge detail carries a `card` block resolved from **Tagada's own BIN-enrichment data** — the same fields no matter which acquirer processed the charge (Adyen, Stripe, …). Use it for commercial-card surcharging and risk decisions:

```ts theme={null}
const charge = await tagada.partners.processing.charges.retrieve('psp_ref_xxx', {
  account: 'tpa_xxx',
});

charge.card;
// {
//   bin: '552433',
//   funding: 'credit',            // 'credit' | 'debit'
//   segment: 'commercial',        // 'consumer' | 'business' | 'commercial' | 'government'
//   isCommercial: true,           // true for any non-consumer segment
//   prepaid: false,
//   issuerCountry: 'US',
//   issuerName: 'Chase',
//   issuerCurrency: 'USD',
//   product: 'World Elite Mastercard',
// }

// Example: 0.5% surcharge decision on US commercial cards
if (charge.card?.isCommercial && charge.card.issuerCountry === 'US') {
  // apply surcharge on your side
}
```

The same block ships inside every `charge.*` webhook payload (`event.data.object.card`), so you can react in real time without an extra API call. `card` is `null` when the charge has no BIN or the BIN is unknown to us; individual fields are `null` when that attribute isn't known.

***

## Refunds

### Issue a refund

Full or partial, through the TPA's acquirer — the same engine as the dashboard. The call validates against the remaining refundable amount, so you can never over-refund:

```ts theme={null}
// Partial refund of €5.00
const attempt = await tagada.partners.processing.charges.refund('psp_ref_xxx', {
  account: 'tpa_xxx',
  amountMinor: 500,
});
console.log(attempt.status, attempt.providerRefundId);

// Full refund of whatever remains — just omit amountMinor
await tagada.partners.processing.charges.refund('psp_ref_xxx', { account: 'tpa_xxx' });
```

| Error                    | Meaning                                                                                  |
| ------------------------ | ---------------------------------------------------------------------------------------- |
| `400`                    | Amount exceeds the remaining refundable amount, or the charge is already fully refunded. |
| `404`                    | Charge not found for this TPA.                                                           |
| `422 refund_unsupported` | The TPA's provider has no API refund path.                                               |

### List confirmed refunds

The response above means the provider **accepted** the refund. The confirmed row (webhook-backfilled) appears in `refunds.list()` shortly after:

```ts theme={null}
const refunds = await tagada.partners.processing.refunds.list({
  account: 'tpa_xxx',
  charge: 'psp_ref_xxx',   // optional — only refunds of this charge
});
```

***

## Disputes

### List & retrieve

```ts theme={null}
const open = await tagada.partners.processing.disputes.list({
  account: 'tpa_xxx',
  status: 'needs_response',
});

const dispute = await tagada.partners.processing.disputes.retrieve('dp_xxx', {
  account: 'tpa_xxx',
});
console.log(dispute.reason, dispute.evidenceDueBy); // respond BEFORE this deadline
```

<Warning>
  `evidenceDueBy` is a hard deadline — a dispute with no response is lost automatically. Poll `disputes.list({ status: 'needs_response' })` or subscribe to `dispute.opened` webhooks.
</Warning>

### Contest (submit evidence)

Text fields plus inline base64 files (max 5 files × 4MB; PDF/JPEG/PNG):

```ts theme={null}
import { readFile } from 'node:fs/promises';

const receipt = await readFile('./receipt.pdf');

await tagada.partners.processing.disputes.submitEvidence('dp_xxx', {
  account: 'tpa_xxx',
  evidence: {
    productDescription: 'Monthly subscription, delivered by email',
    customerEmailAddress: 'buyer@example.com',
    shippingTrackingNumber: '1Z999AA10123456784',
    files: [
      {
        slot: 'receipt',
        name: 'receipt.pdf',
        contentType: 'application/pdf',
        contentBase64: receipt.toString('base64'),
      },
    ],
  },
});
```

Provider quirks (enforced server-side, surfaced as `400`):

* **Stripe** — supports drafts (`submit: false`), combined evidence capped at 4.5MB.
* **Adyen** — document-only (your text fields are rendered into a PDF), rejects PNG, PDFs capped at 2MB. One-shot.
* **Tilled** — evidence only, one-shot.

### Concede

```ts theme={null}
await tagada.partners.processing.disputes.accept('dp_xxx', { account: 'tpa_xxx' });
```

Supported on Stripe and Adyen. Tilled has no accept API — conceding = not responding before the deadline (the call returns `422` there).

***

## Per-TPA stats

`tpas.getStats()` gives you the KPIs merchants ask for, over a sliding window (`days`: 1–365, default 30) — volumes and counts by outcome, total fees, **authorization rate**, **average ticket**, and a daily series for charting:

```ts theme={null}
const stats = await tagada.partners.processing.tpas.getStats('tpa_xxx', { days: 30 });

console.log(`gross:      ${stats.volume.grossMinor} ${stats.currency} (${stats.volume.chargeCount} charges)`);
console.log(`auth rate:  ${(stats.authRateBps ?? 0) / 100}%`);
console.log(`avg ticket: ${stats.averageTicketMinor}`);
console.log(`fees:       ${stats.feesMinor}`);
console.log(`refunds:    ${stats.volume.refundedMinor} (${stats.volume.refundedCount})`);
console.log(`disputes:   ${stats.volume.disputedMinor} (${stats.volume.disputedCount})`);

// Daily buckets for a chart, ascending:
for (const day of stats.series) console.log(day.date, day.grossMinor);
```

`authRateBps` is `succeeded / (succeeded + failed)` in basis points (`9000` = 90%); it is `null` when the window has no attempts. `averageTicketMinor` is `null` when nothing succeeded.

<Note>
  Building a partner-wide overview? Loop `tpas.list()` and call `getStats()` per TPA — stats are always computed per TPA, in the TPA's processing currency.
</Note>

***

## Balance & ledger (recap)

Covered in depth on [Payouts, balances & fees](/developer-tools/partners/payouts):

```ts theme={null}
// Live provider balance (one bucket per currency)
const balance = await tagada.partners.processing.balance.retrieve({ account: 'tpa_xxx' });

// Historical ledger (gross / fee / net per entry)
const ledger = await tagada.partners.processing.balanceTransactions.list({ account: 'tpa_xxx' });

// Payout history
const payouts = await tagada.partners.processing.payouts.list({ account: 'tpa_xxx' });
```
