> ## 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.

# Webhooks

> Receive signed, real-time events for charges, refunds, disputes, payouts and chargeback alerts — with a delivery log, automatic retries, and replay.

# Webhooks

TagadaPay pushes processing events to your HTTPS endpoint as they happen: a charge fails, a payout lands, a dispute opens. One partner-level endpoint receives events for **all your TPAs** — the `account` field on every event tells you which merchant it belongs to (Stripe Connect semantics). Direct merchants subscribe per-TPA the same way.

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

***

## 1. Create an endpoint

```ts theme={null}
const endpoint = await tagada.partners.processing.webhookEndpoints.create({
  url: 'https://example.com/webhooks/tagada',
  enabledEvents: ['payout.*', 'dispute.*', 'charge.failed', 'chargeback_alert.*'],
});

// ⚠️ Store this — it signs every delivery to this endpoint.
console.log(endpoint.secret); // whsec_…
```

With a **partner key** the endpoint is created at partner scope automatically (events for all your TPAs). With a TPA-pinned merchant key it is scoped to that TPA. `enabledEvents` accepts exact types (`payout.paid`), prefixes (`payout.*`), or `'*'` for everything.

Manage endpoints with `list()`, `retrieve(id)`, `update(id, { url, enabledEvents, status })` and `del(id)`. Set `status: 'disabled'` to pause deliveries instantly.

## 2. Verify and handle events

Every delivery is signed with your endpoint's `whsec_…` secret (header `tagadapay-signature`, Stripe-style `t=<unix>,v1=<hmac>`). Use the SDK helper — it verifies and parses in one call, and throws on a bad signature:

```ts theme={null}
import express from 'express';
import { constructProcessingEvent } from '@tagadapay/node-sdk';

const app = express();

app.post('/webhooks/tagada', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = constructProcessingEvent(
      req.body.toString('utf8'),          // MUST be the raw body
      req.headers['tagadapay-signature'] as string,
      process.env.TAGADA_WEBHOOK_SECRET!, // the endpoint's whsec_…
    );
  } catch {
    return res.sendStatus(400);
  }

  switch (event.type) {
    case 'payout.paid':
      console.log(`Payout paid for ${event.account}:`, event.data.object);
      break;
    case 'dispute.created':
      notifyMerchant(event.account, event.data.object);
      break;
  }

  res.sendStatus(200); // respond 2xx fast; do heavy work async
});
```

The event envelope:

```json theme={null}
{
  "id": "evt_9f2c41d3a8b7e6f01c2d3e4f",
  "object": "event",
  "type": "payout.paid",
  "account": "tpa_1f770ea2c614",
  "mode": "live",
  "created": "2026-07-15T17:40:12.000Z",
  "data": {
    "object": {
      "object": "payout",
      "id": "po_8823…",
      "amountMinor": 125000,
      "currency": "EUR",
      "status": "paid",
      "bankAccountLast4": "1234"
    }
  }
}
```

Event ids are **deterministic** — if a payment processor redelivers the same underlying webhook to us, you will not receive it twice. Still design handlers to be idempotent on `event.id`.

## Event types

| Family                | Types                                                                                                                                                                          |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Charges               | `charge.authorized`, `charge.succeeded`, `charge.failed`, `charge.voided`, `charge.refunded`                                                                                   |
| Refunds               | `refund.succeeded`, `refund.failed`                                                                                                                                            |
| Disputes              | `dispute.created`, `dispute.updated`, `dispute.closed`                                                                                                                         |
| Payouts               | `payout.created`, `payout.paid`, `payout.failed`, `payout.canceled`                                                                                                            |
| Chargeback alerts     | `chargeback_alert.created`, `chargeback_alert.refunded`                                                                                                                        |
| Account notifications | any merchant notification type (`compliance.document_required`, `account.activated`, `payout.succeeded`, …) when the TPA's notification preferences enable the webhook channel |

<Note>
  `charge.*` payloads include a `card` block with PSP-agnostic BIN intelligence (funding `credit`/`debit`, segment `consumer`/`business`/`commercial`/`government`, `isCommercial`, `prepaid`, issuer country/name/currency, card product) resolved from Tagada's own enrichment data — ready for commercial-card surcharging and risk rules without any acquirer-specific configuration. See [Card intelligence](/developer-tools/partners/reporting#card-intelligence-bin-metadata).
</Note>

## Retries

A delivery that doesn't get a 2xx within 10 seconds is retried with backoff **1 min → 10 min → 1 h → 6 h → 24 h** (6 attempts total), then marked failed. Failures never block other events.

## 3. Delivery log & replay

Audit exactly what was sent, when, and what your server answered:

```ts theme={null}
const { data: deliveries } = await tagada.partners.processing.webhookEndpoints.deliveries.list(
  endpoint.id,
  { status: 'failed', limit: 50 },
);

for (const d of deliveries) {
  console.log(d.eventType, d.status, d.statusCode, d.attempt, d.error);
}

// Missed something during an outage? Re-send the exact original payload:
await tagada.partners.processing.webhookEndpoints.deliveries.replay(endpoint.id, deliveries[0].id);
```

Replays create a fresh delivery (the historical row is preserved) with the **exact original payload** — same `event.id`, so idempotent consumers stay safe.

## Testing your integration

Point a temporary endpoint at a request inspector (e.g. `https://webhook.site/…`), subscribe to `'*'`, then trigger a test event — the fastest is an on-demand payout or a small refund on a test TPA. Check the delivery log to confirm the `200`.
