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

# Webhooks

> The notifications TapiPay sends to your backend when something changes on a charge.

A webhook is a `POST` call TapiPay makes to an endpoint of **yours** when something you care about happens: a payment settles, a charge is declined, or a charge is reversed. That way you find out right away, without polling the API.

<Note>
  Webhooks are **not emitted by the API**, which does not implement them. They are emitted by TapiPay's charging platforms, and they are configured **during onboarding**: there is no endpoint to register them or change the URL.
</Note>

## The events

| Event                                                       | `type`                            |
| ----------------------------------------------------------- | --------------------------------- |
| [Payment notification](/en/webhooks/payment-notification)   | `SERVICE` or `REFERENCED-PAYMENT` |
| [Failed bank debit](/en/webhooks/failed-bank-debit)         | `AUTO_DEBIT_FAILED`               |
| [Bank debit chargeback](/en/webhooks/bank-debit-chargeback) | `AUTO_DEBIT_CHARGEBACK`           |
| [Failed card autopay](/en/webhooks/failed-card-autopay)     | `CARD_AUTOPAY_FAILED`             |
| [Failed card payment](/en/webhooks/failed-card-payment)     | `CARD_PAYMENT_STATUS`             |
| [Card chargeback](/en/webhooks/card-chargeback)             | `CARD_PAYMENT_CHARGEBACK`         |

<Warning>
  **Route on the `type` field.** Every event arrives at the same endpoint of yours, so `type` is what tells one from another. Do not infer the event from the fields the payload carries.

  With one caveat: on the payment notification, `type` describes **what was charged**, not what happened. What tells you whether that payment settled, failed or was reversed is its `status` field. On the other five, the `type` already identifies the fact.
</Warning>

<Note>
  The [payment notification](/en/webhooks/payment-notification) comes from the referenced payment platform and has its own contract, different from the rest: its `status` is lowercase, its `type` identifies the kind of operation rather than the event, and its configuration is documented on that page. The other five share everything below.
</Note>

## A failure and a chargeback are not the same

The five events in this family split into two groups, and it is worth not confusing them: they say different things about the same charge.

<CardGroup cols={2}>
  <Card title="Failure" icon="triangle-exclamation">
    The charge never went through. These arrive with `status: "FAILED"`.
  </Card>

  <Card title="Chargeback" icon="rotate">
    The charge had gone through and was later reversed. These carry no `status`, because they do not describe the state of a payment but a later fact about an already processed charge.
  </Card>
</CardGroup>

## Common fields

The five events share this base:

| Field         | Type           | Description                                                                                               |
| ------------- | -------------- | --------------------------------------------------------------------------------------------------------- |
| `type`        | String         | Which event this is. Use it to route.                                                                     |
| `operationId` | String         | Unique identifier of the operation. **Use it as your idempotency key.**                                   |
| `amount`      | Number         | Amount involved.                                                                                          |
| `companyCode` | String \| null | Company code. **The key is always there**, with a `null` value if the event does not carry it.            |
| `companyName` | String \| null | Company name. Same rule as `companyCode`.                                                                 |
| `hash`        | String \| null | Verification hash. **The key is always there**, with a `null` value if you have no public key configured. |

<Tip>
  `companyCode`, `companyName` and `hash` are **always present as a key**, even when they are `null`. You can read them without checking existence, but you do have to handle the `null` before using the value.
</Tip>

## The verification hash

Your endpoint is public, so anyone can call it. The `hash` field lets you confirm the request comes from TapiPay and that the payload was not tampered with.

It is generated by encrypting with **RSA** against the public key you hand over during onboarding. If you do not configure a `publicKey`, the field still arrives, with a `null` value.

<Warning>
  **Do not assume `hash` carries a value.** Check that it is not `null` before using it. If your integration relies on it to authenticate, an unexpected `null` means the public key was not configured: confirm it with TapiPay before going live.
</Warning>

The hash is one mechanism among several. The others (API key, bearer token, IP allowlist, mTLS) are configured the same way for every webhook and are detailed in [Payment notification](/en/webhooks/payment-notification#validating-the-request-comes-from-tapipay).

## Error detail is optional

Card events can include the error code and message the provider returned, inside an `additionalData` object. Whether they arrive depends on a flag in **your configuration**, `sendErrorInfo`, set during onboarding.

<Warning>
  **`additionalData` is dropped entirely when it would be empty.** It does not arrive as an empty object or with `null` fields: the key is simply not there. Check that it exists before reading inside it.
</Warning>

It is dropped in two cases: when `sendErrorInfo` is off, and when it is on but the provider reported no detail.

```javascript theme={null}
// Correct: handles the key not being there
const code = notification.additionalData?.errorCode ?? null;
```

<Note>
  **Bank debit** events work differently: the decline detail (`bankErrorCode`, `bankErrorDescription`) **always** travels, at the root of the payload and without depending on `sendErrorInfo`.
</Note>

## Idempotency

TapiPay can retry a notification, so your endpoint has to tolerate receiving the same event more than once. The key is `operationId`.

<Warning>
  Store processed `operationId` values in **persistent** storage (a table with a unique index, or Redis), never in process memory: if you restart the server between the original attempt and the retry, an in-memory marker is lost and the event is processed twice.
</Warning>

Your endpoint requirements (return 2xx, in under 5 seconds, reachable from TapiPay's IPs) are the same for every webhook and live in [Payment notification](/en/webhooks/payment-notification#requirements-for-your-endpoint).

<Tip>
  Every delivery attempt is recorded on TapiPay's side, along with its result, so a notification you believe was lost can be traced.
</Tip>
