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

Webhooks are used to communicate changes to a transaction or other entities within the Tight API. If you subscribe to webhooks, then your server will be alerted every time an entity is modified or a notable lifecycle event occurs.

## How it works

By subscribing to receive ["ENTITY\_UPDATE" webhooks](/v5.0/getting-started/api-patterns/webhooks#entity_update), your server can asynchronously fetch updated data at the exact moment it becomes available. When that new data is available, the Tight API will make a POST call to a URL of your choosing with the data needed for your server to update appropriately. This approach ensures your users always have up-to-date data while removing unnecessary polls/refreshes from your server, saving you valuable resources.

By subscribing to receive ["LIFECYCLE" webhooks](/v5.0/getting-started/api-patterns/webhooks#lifecycle), you can trigger timely lifecycle notifications (e.g. push notifications, emails, and/or other alerts) based on timely user actions. For example, if a user's client views their invoice, the Tight API will make a POST call to a URL of your choosing with the data needed for your server to alert your user that their client viewed the invoice.

## Subscribing to receive webhooks

Tight's API team is here to help! Simply email [api@tight.com](mailto:api@tight.com) with the subject line "Subscribe to Webhooks", and provide the following info:

a. Your API `client_Id`\
b. Webhook URL for the Sandbox environment\
c. Webhook URL for the Production environment

Our API team will respond back within 5 business days with your Sandbox `webhook_secret` and your Production `webhook_secret` and will fire off sample webhooks to your Sandbox Webhook URL, for your team to develop/test with.

## ENTITY\_UPDATE

An "ENTITY\_UPDATE" webhook will contain a JSON body like the following:

```json theme={null}
{
  "userId": "fake_userId",
  "type": "ENTITY_UPDATE",
  "entities": ["bankTransfer", "expense", "revenue", "taxPayment"]
}
```

The possible `entities` and recommended actions on how to update each given entity are listed below:

| Webhook Type   | Entity                  | Recommended action                                                                              |
| :------------- | :---------------------- | :---------------------------------------------------------------------------------------------- |
| ENTITY\_UPDATE | attachment              | GET [/attachments](/v5.0/getting-started/api-patterns/webhooks)                                 |
| ENTITY\_UPDATE | bankAccount             | GET [/bankAccounts](/v5.0/products/data-lakes)                                                  |
| ENTITY\_UPDATE | bankTransfer            | GET [/bankTransfers](/v5.0/getting-started/api-patterns/webhooks)                               |
| ENTITY\_UPDATE | business                | GET [/businesses](/v5.0/products/bank-transactions/income/self-employed-business-income)        |
| ENTITY\_UPDATE | client                  | GET [/clients](/v5.0/products/invoicing/creating-a-client)                                      |
| ENTITY\_UPDATE | expense                 | GET [/expenses](/v5.0/products/bank-transactions/expenses/business-categorization)              |
| ENTITY\_UPDATE | expenseCategory         | GET [/expenses/categories](/v5.0/products/bank-transactions/expenses/business-categorization)   |
| ENTITY\_UPDATE | expenseRule             | GET [/expenses/rules](/v5.0/products/bank-transactions/expenses/business-categorization)        |
| ENTITY\_UPDATE | glAccount               | GET [/glAccounts](/v5.0/products/accounting/chart-of-accounts)                                  |
| ENTITY\_UPDATE | glEntry                 | GET [/glEntries](/v5.0/products/accounting/general-ledger)                                      |
| ENTITY\_UPDATE | integration             | GET [/integrations](/v5.0/integrations/overview)                                                |
| ENTITY\_UPDATE | invoice                 | GET [/invoices](/v5.0/products/invoicing-api)                                                   |
| ENTITY\_UPDATE | invoiceSetup            | GET [/invoiceSetup](/v5.0/products/invoicing-api)                                               |
| ENTITY\_UPDATE | manualJournalEntry      | GET [/manualJournalEntries](/v5.0/products/accounting/general-ledger)                           |
| ENTITY\_UPDATE | payment                 | GET [/payments](/v5.0/products/invoicing/payment-processing)                                    |
| ENTITY\_UPDATE | personalExpenseCategory | GET [/expenses/personalCategories](/v5.0/products/bank-transactions/expenses/personal-expenses) |
| ENTITY\_UPDATE | revenue                 | GET [/revenues](/v5.0/products/bank-transactions/income/self-employed-business-income)          |
| ENTITY\_UPDATE | revenueRule             | GET [/revenueRules](/v5.0/products/bank-transactions/income/self-employed-business-income)      |
| ENTITY\_UPDATE | task                    | GET [/tasks](/v5.0/getting-started/api-patterns/webhooks)                                       |
| ENTITY\_UPDATE | taxEstimates            | GET [/taxEstimates](/v5.0/products/taxes/tax-estimates/tax-estimates)                           |
| ENTITY\_UPDATE | taxPayment              | GET [/taxPayments](/v5.0/products/tax/managing-tax-payments)                                    |
| ENTITY\_UPDATE | time                    | GET [/times](/v5.0/products/time-tracking-1)                                                    |
| ENTITY\_UPDATE | userTaxSetup            | GET [/userTaxSetup](/v5.0/products/taxes/tax-estimates/tax-profile)                             |
| ENTITY\_UPDATE | vendor                  | GET [/vendors](/v5.0/products/invoicing-api)                                                    |

## LIFECYCLE

A "LIFECYCLE" webhook will contain a JSON body like the following:

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

The full list of Lifecycle Events that will trigger a webhook can be found at the [Lifecycle Event Dictionary](/docs/lifecycle-event-dictionary).

> 👍 Additional use cases
>
> The 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.

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

1. Extract the `Tight-Verification` HTTP header from the Tight webhook

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

3. 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, obtained in [Subscribing to receive webhooks](/v5.0/getting-started/api-patterns/webhooks#subscribing-to-receive-webhooks)

```coffeescript Node.js 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.

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

```coffeescript Node.js 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")
  }

  // ...
});
```

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

```coffeescript Node.js 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");
});
```

Once these steps are completed successfully, you can be confident that the webhook came from Tight and that it is safe to process.
