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

# Webhook security

> The mechanisms you can use to check that a webhook comes from TapiPay, and how to request their setup.

Your webhook endpoint is public, so anyone can call it. TapiPay offers several mechanisms so you can check that each request comes from TapiPay before processing it. You can enable one or combine several.

> [!WARNING]
>
> **Security is not configured through the API.** You must contact the TapiPay team directly to request a security configuration. Until you do, your webhooks arrive with no mechanism enabled.

> [!NOTE]
>
> This page applies to the [bank debit](https://devs.tapipay.la/en/webhooks/failed-bank-debit.md), [card](https://devs.tapipay.la/en/webhooks/failed-card-payment.md), [order creation](https://devs.tapipay.la/en/webhooks/order-creation.md), [order payment](https://devs.tapipay.la/en/webhooks/order-payment.md) and [payment method registered](https://devs.tapipay.la/en/webhooks/payment-method-registered.md) webhooks. The [payment notification](https://devs.tapipay.la/en/webhooks/payment-notification.md) is sent by another platform and its mechanisms are on that page.

## The mechanisms

| Mechanism | What your endpoint receives | What you validate | What you provide to TapiPay |
| - | - | - | - |
| [Fixed headers](#fixed-headers) | One or more headers with the names and values you agree on, for example `x-api-key`. | That the header comes with the expected value. | The name and value of each header. |
| [Bearer token](#bearer-token) | `Authorization: Bearer <token>`. | That the token is yours, or that your authorization server issued it. | The fixed token, or the credentials for your OAuth 2.0 server. |
| [Encrypted hash](#encrypted-hash) | A `hash` field in the body, encrypted with your public key. | That decrypting it with your private key yields your integration user. | Your RSA public key. |
| [IP allowlist](#ip-allowlist) | Requests that come from known IPs. | That the source IP is on your list. | Nothing: you request the list of IPs. |

> [!TIP]
>
> Combine at least two, for example fixed headers plus an IP allowlist, or a bearer token plus the encrypted hash. A single header-based mechanism is vulnerable if the value leaks.

The `notificationType` header always arrives, with the same value as the `type` in the body. It is for routing, not for authentication.

## Fixed headers

TapiPay adds the headers you agree on to each request, with a fixed value. The most common case is an API key in `x-api-key`.

* **What you provide:** the name and value of each header, through the secure channel the TapiPay team indicates.
* **What you validate:** that the header comes and that its value matches the one you provided. Compare in constant time so you do not leak the value through timing differences.
* **Rotation:** to change the value, agree on the date and time of the change with the TapiPay team. During the change window, accept both the old and the new value.

## Bearer token

TapiPay sends a token in the `Authorization: Bearer <token>` header. There are two variants:

| Variant | How it works | What you provide |
| - | - | - |
| **Fixed token** | TapiPay always sends the same token. | The token. |
| **Token from your OAuth 2.0 server** | TapiPay gets the token from your authorization server with the `client_credentials` flow and renews it periodically. | The URL of your token endpoint, the `client_id` and the `client_secret`. |

In the OAuth 2.0 variant, your endpoint validates the token like any other one your authorization server issues: signature, expiration and audience.

## Encrypted hash

TapiPay includes a `hash` field in the body, encrypted with the RSA public key you provide. Only you can decrypt it, with your private key.

| Aspect | Value |
| - | - |
| Algorithm | RSA with OAEP padding (SHA-1). |
| Encoding | Hexadecimal. |
| Content, default version | Your integration user, as text. |
| Content, version `v2` | The JSON `{ "clientUsername": "<your user>", "operationId": "<identifier>" }`. |

In `v2`, `operationId` is the identifier of that notification: the `id` field in the [order](https://devs.tapipay.la/en/webhooks/order-payment.md) and [payment method registered](https://devs.tapipay.la/en/webhooks/payment-method-registered.md) webhooks, and the `operationId` field in the bank debit and card ones. Check that they match: this way a `hash` copied from another notification does not validate this one.

> [!WARNING]
>
> **The hash does not sign the payload.** It proves that TapiPay built the request for your integration, but not that the rest of the body was not altered on the way. Combine it with another mechanism.

If you did not provide a public key, the `hash` field arrives anyway, with value `null`. The version (`v2` or default) is set when your integration is configured: ask for `v2`.

To generate the key pair:

```bash theme={null}
# Private key: keep it on your server, never share it
openssl genrsa -out tapipay-webhooks-private.pem 2048

# Public key: this is the one you provide to TapiPay
openssl rsa -in tapipay-webhooks-private.pem -pubout -out tapipay-webhooks-public.pem
```

## IP allowlist

TapiPay can provide you with the list or ranges of IPs the webhook requests come from, so your endpoint accepts only those IPs. Request it from the TapiPay team for each environment (staging and production).

> [!NOTE]
>
> Ask to be notified before any change to that list. A new IP your allowlist does not know makes you reject legitimate webhooks.

## Example: validating a request

The right order in your handler is: validate the origin, answer 2xx quickly and only then process. This example combines a fixed header with the encrypted hash in its `v2` version.

```javascript theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();
const PRIVATE_KEY = process.env.TAPI_WEBHOOK_PRIVATE_KEY;
const CLIENT_USERNAME = process.env.TAPI_CLIENT_USERNAME;

function validHeader(value, expected) {
  if (!value) return false;
  const a = Buffer.from(value);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function validHash(notification) {
  if (!notification.hash) return false;
  try {
    const content = crypto
      .privateDecrypt(
        { key: PRIVATE_KEY, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha1" },
        Buffer.from(notification.hash, "hex"),
      )
      .toString("utf8");
    const { clientUsername, operationId } = JSON.parse(content);
    // In orders and payment method registered it is `id`; in bank debit and card, `operationId`
    const identifier = notification.id ?? notification.operationId;
    return clientUsername === CLIENT_USERNAME && operationId === identifier;
  } catch {
    return false;
  }
}

app.post("/tapi/webhooks", express.json(), (req, res) => {
  // 1. Validate the origin before processing
  if (!validHeader(req.header("x-api-key"), process.env.TAPI_WEBHOOK_KEY) || !validHash(req.body)) {
    return res.sendStatus(401);
  }

  // 2. Answer quickly and process afterwards
  res.sendStatus(200);
  enqueue(req.body);
});
```

> [!NOTE]
>
> In the default version of the hash, the decrypted content is only your integration user: compare it directly with `CLIENT_USERNAME`, without `JSON.parse`.

## Retries after a rejection

If your endpoint does not answer 2xx, TapiPay retries the delivery a limited number of times over the following minutes and then stops retrying. A rejection because of credentials (`401` or `403`) is also retried, but it keeps failing until the configuration is fixed.

> [!WARNING]
>
> **Test the configuration in staging before going to production.** If a mechanism is misconfigured, for example a header with a different value, your endpoint rejects every webhook and the notices of that period are lost. If you start rejecting webhooks you expected, contact the TapiPay team.

## How to request the configuration

**1. Choose the mechanisms**

Decide which mechanisms you want to combine and for which environments: staging, production or both.

**2. Contact the TapiPay team**

Request the configuration and indicate the mechanisms and the webhooks you receive. The [order and payment method](https://devs.tapipay.la/en/webhooks.md#orders-and-payment-methods) webhooks share a single configuration; ask for the security to cover **all** the groups of webhooks you receive, so none of them arrives without it.

**3. Provide the data through a secure channel**

Provide the values (headers, tokens, credentials, public key) through the channel the TapiPay team indicates. Never send a secret by email or chat in plain text. The public key is not a secret.

**4. Test in staging**

Check that your endpoint accepts the staging webhooks with the mechanisms enabled before requesting the same configuration in production.


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