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

# Webhooks

> Subscribe to and handle webhooks from the Tight API

## Get Started

The Tight API uses webhooks to communicate changes to entities and for notable lifecycle events, e.g. an invoice being
paid. To get started, simply reach out to your Tight API contact and provide the following info:

1. Your Tight API `client_id`
2. Webhook URL(s) for the Sandbox environment
3. Webhook URL(s) for the Production environment

Tight sends webhooks from the following IP addresses:

* 3.221.92.254
* 34.200.77.221

## Entity update webhooks

Any time an entity is updated in the Tight API, whether by a user or a third-party integration, the Tight API sends an
`ENTITY_UPDATE` webhook to communicate that change. These webhooks contain a JSON body with the full detail of the
entity that was updated:

```json theme={null}
{
  "editor": {
    "userId": "fake_userId"
  },
  "company": {
    "id": "co_1234",
    "apiCompanyId": "id_1234"
  },
  "type": "ENTITY_UPDATE",
  "url": "/v6/banks/transactions/btn_936723",
  "data": {
    "id:": "btn_936723",
    "type": "REVENUE",
    "amount": 15399
    /* remainder of the bank transaction's properties */
  }
}
```

## Lifecycle webhooks

The Tight API's `LIFECYCLE` webhooks provide real-time lifecycle event data, useful for performing analytics, notifying
users of pertinent events, and ultimately optimizing activation & retention. The webhooks contain a JSON body with data
relevant to the event:

```json theme={null}
{
  "editor": {
    "userId": "fake_userId"
  },
  "company": {
    "id": "co_1234"
  },
  "type": "LIFECYCLE",
  "event": "bankAccountCreated",
  "datetime": "2025-04-01T11:31:04.1Z",
  "eventData": {
    "entityId": "fake_id",
    "apiName": "PLAID",
    "type": "DEPOSITORY"
  }
}
```

Refer to the [Lifecycle Event Dictionary](/guides/business-owners/lifecycle-event-dictionary) for the full list of lifecycle events.

<Card title="Additional use cases" icon="thumbs-up">Lifecycle Events can be used in varying ways depending on your
product's use case. For example, if your product manages corporate/employee spend, it may benefit your user experience
to make the `transactionCreated` event trigger a push notification prompting the user to add a receipt image. The
`eventData` included with the `transactionCreated` event contains the information necessary to route the user to the
exact transaction that requires a receipt.</Card>

## Webhook verification

Tight signs every webhook, so that you have the option to verify the webhooks you receive. This helps you protect
against a bad actor flooding your server with fake webhooks.

**Verifying Webhooks**
Tight follows the [JSON Web Token (JWT) standard](https://jwt.io/introduction) and includes its JWTs in the
`Tight-Verification` HTTP header of the webhook.

To verify a Tight JWT, follow these steps:

<Steps>
  <Step>
    Extract the `Tight-Verification` HTTP header from the Tight webhook
  </Step>

  <Step>
    Select [a JWT library](https://jwt.io/libraries) of your choice
  </Step>

  <Step>
    Pass in HMAC-SHA256 as the signing algorithm to your selected JWT library and then use the library to verify the value of the `Tight-Verification` header against your `webhook_secret`

    ```javascript theme={null}
    import 'dotenv/config';
    import express from "express";
    import { jwtVerify } from "jose";

    const app = express();

    // Your webhook_secret
    const webhook_secret = process.env.SECRET;

    app.post("/webhook", express.raw({type: "application/json"}), async (req, res) => {
        const jwt = req.get('Tight-Verification');

        let payload;
        try {
            // Verify the JWT using the HMAC-SHA256 signing algorithm
            const jwtVerifyResult = await jwtVerify(jwt, webhook_secret, {algorithms: ["HS256"]});
            payload = jwtVerifyResult.payload;
        } catch (err) {
            return res.status(400).send(`JWT failed to verify: ${err.message}`);
        }
    });
    ```

    <Note>If your library does not automatically decode the JWT, Base64Url decode the JWT payload and deserialize it into a JSON object.</Note>
  </Step>

  <Step>
    Ensure that the webhook is not expired. Verify that the difference between the `iat` field of the payload and the current `NumericDate` timestamp is within your tolerance. Tight recommends a default tolerance of 5 minutes.

    ```javascript theme={null}
    app.post("/webhook", express.raw({type: "application/json"}), async (req, res) => {
        // Validate the iat, i.e. make sure the webhook has not expired
        // Tight recommends a default tolerance of five minutes
        const TIME_TOLERANCE = (60 * 5);
        let timestamp = payload.iat;
        const now = Math.floor(Date.now() / 1000);
        if (now - timestamp >= TIME_TOLERANCE) {
            return res.status(400).send("Webhook expired")
        }
    });
    ```
  </Step>

  <Step>
    Ensure that the webhook was not modified by anyone other than Tight. Verify that the `request_body_sha256` field of the payload is equal to the SHA-256 hash of the webhook's request body.

    ```javascript theme={null}
    import { timingSafeEqual } from "node:crypto";
    const { subtle } = globalThis.crypto;

    app.post("/webhook", express.raw({type: "application/json"}), async (req, res) => {
        // Validate the webhook body hash
        const enc = new TextEncoder();
        const bodyDigest = await subtle.digest("SHA-256", req.body);
        const webhookHash = enc.encode(btoa(String.fromCharCode(...new Uint8Array(bodyDigest))));
        const claimHash = enc.encode(payload.request_body_sha256 as string);
        if (!timingSafeEqual(webhookHash, claimHash)) {
            return res.status(400).send("Webhook payload modified")
        }


        // Verification succeeded
        // You can process the webhook here or call the next handler in your request-response cycle
        res.send("Webhook verification succeeded");
    });
    ```
  </Step>
</Steps>

<Check>
  If all verification steps succeed, you can be confident the webhook came from Tight and is safe to process.
</Check>
