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

# Webhooks

> Receive real-time notifications when verification events occur

Webhooks are HTTP POST requests sent automatically by Truv when events occur during the verification flow. They track Task status changes, Order updates, Link connections, and data object creation, keeping your system in sync as data progresses through processing stages.

<Info>
  Configure webhook endpoints separately for Sandbox and Production.
</Info>

***

## Set up webhooks

Configure webhook endpoints in the [Truv Dashboard](https://dashboard.truv.com):

1. Navigate to **Development** → **Webhooks**
2. Enter your endpoint URL
3. Save the configuration

Your endpoint will begin receiving webhook events for all activity in that environment.

### Test webhooks

* Use **Truv Bridge** in the Dashboard Emulator to trigger webhook events in sandbox
* Use tools like [ngrok](https://ngrok.com/) to expose a local development server:

```bash theme={null}
ngrok http 3000
```

Then configure the ngrok URL as your webhook endpoint in the Dashboard.

***

## Webhook payload structure

All webhook requests include these standard fields:

### Headers

| Header           | Value                                  |
| ---------------- | -------------------------------------- |
| `User-Agent`     | `Truv-Webhook-Service/2.0`             |
| `X-WEBHOOK-SIGN` | HMAC-SHA256 signature for verification |

<Note>
  HTTP header names are case-insensitive per RFC 7230.
</Note>

### Body fields

Every webhook payload includes these common fields:

| Field              | Description                                |
| ------------------ | ------------------------------------------ |
| `webhook_id`       | Unique identifier for this webhook request |
| `event_type`       | The event classification                   |
| `event_created_at` | Timestamp when the event occurred          |
| `user_id`          | Associated Truv user identifier            |

Additional fields vary by event type — see [Events by object](#events-by-object) below.

***

## Security

Every webhook includes an `X-WEBHOOK-SIGN` header with an HMAC-SHA256 signature. Verify it against the raw request body using your Access Secret before processing the event.

### How it works

1. Truv computes an HMAC-SHA256 hash of the raw request body using your Access Secret
2. The hash is sent in the `X-WEBHOOK-SIGN` header with a `v1=` prefix
3. Your server recomputes the hash and compares it to the header value

<Warning>
  Always verify signatures using the **raw request body**, not parsed JSON. Parsing and re-serializing may change the byte representation.
</Warning>

### Code examples

<AccordionGroup>
  <Accordion title="Python" icon="python">
    ```python theme={null}
    import hashlib
    import hmac

    def verify_webhook(payload: str, signature: str, secret: str) -> bool:
        generated_hash = hmac.new(
            key=secret.encode('utf-8'),
            msg=payload.encode('utf-8'),
            digestmod=hashlib.sha256,
        ).hexdigest()
        expected = f'v1={generated_hash}'
        return hmac.compare_digest(expected, signature)
    ```
  </Accordion>

  <Accordion title="Node.js" icon="node-js">
    ```javascript theme={null}
    const crypto = require("crypto");
    const express = require("express");
    const bodyParser = require("body-parser");

    const app = express();

    app.use(bodyParser.json({
      verify: (req, res, buf) => {
        req.rawBody = buf;
      }
    }));

    function verifyWebhook(rawBody, signature, secret) {
      const hash = crypto
        .createHmac("sha256", secret)
        .update(rawBody)
        .digest("hex");
      const expected = `v1=${hash}`;
      return signature === expected;
    }

    app.post("/webhooks/truv", (req, res) => {
      const signature = req.headers["x-webhook-sign"];
      const isValid = verifyWebhook(req.rawBody, signature, process.env.TRUV_SECRET);

      if (!isValid) {
        return res.status(401).send("Invalid signature");
      }

      res.status(200).send("OK");
    });
    ```
  </Accordion>

  <Accordion title="Go" icon="golang">
    ```go theme={null}
    import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "fmt"
    )

    func verifyWebhook(body string, signature string, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write([]byte(body))
      expected := fmt.Sprintf("v1=%s", hex.EncodeToString(mac.Sum(nil)))
      return hmac.Equal([]byte(expected), []byte(signature))
    }
    ```
  </Accordion>

  <Accordion title="Ruby" icon="gem">
    ```ruby theme={null}
    require 'openssl'

    def verify_webhook(body, signature, secret)
      digest = OpenSSL::Digest.new('sha256')
      expected = "v1=" + OpenSSL::HMAC.hexdigest(digest, secret, body)
      Rack::Utils.secure_compare(expected, signature)
    end
    ```
  </Accordion>

  <Accordion title="C#" icon="microsoft">
    ```csharp theme={null}
    using System.Security.Cryptography;
    using System.Text;

    private bool VerifyWebhook(string body, string signature, string secret)
    {
        using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
        {
            byte[] hashValue = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
            string expected = "v1=" + BitConverter.ToString(hashValue)
                .Replace("-", "").ToLower();
            return expected == signature;
        }
    }
    ```
  </Accordion>
</AccordionGroup>

### Originating IP addresses

Allowlist the current Truv webhook IP addresses if your network requires source filtering:

* `34.212.57.93`
* `44.224.243.166`
* `52.25.14.79`

### Additional authentication options

Beyond signature verification, Truv supports:

* Truv-signed certificates for webhook mTLS
* Client-signed certificates for webhook mTLS
* OAuth 2.0, where Truv obtains access tokens for secure webhook delivery (optional, configured with Truv)
* Custom headers such as client ID and client secret, configured with Truv

See [mTLS](/api-reference/security#mtls-for-webhooks) if you need mutual certificate-based authentication for webhook delivery.

***

## Timeouts and retries

| Behavior            | Detail                                                                                        |
| ------------------- | --------------------------------------------------------------------------------------------- |
| **Timeout**         | Your endpoint must respond with a `2xx` status within **10 seconds**                          |
| **Redirects**       | `3xx` responses are followed                                                                  |
| **Retries**         | `4xx` and `5xx` responses trigger up to **3 retry attempts** at **30-second intervals**       |
| **Failed delivery** | After the 3 retry attempts fail, Truv stops retrying that event and marks the delivery failed |

***

## Event ordering

Webhooks are delivered in event order. For example, `full_parse` fires before `done`. However, network conditions can cause delays in delivery.

<Tip>
  Use the `event_created_at` field to sequence events correctly in your system, rather than relying on delivery order. When you need the authoritative full resource state, fetch it from the API instead of relying on the webhook payload alone.
</Tip>

***

## Handle webhooks

### Best practices

<AccordionGroup>
  <Accordion title="Respond within 10 seconds" icon="clock">
    Return a `200` response quickly. Process the webhook data asynchronously if needed:

    ```javascript theme={null}
    app.post("/webhooks/truv", (req, res) => {
      // Acknowledge immediately
      res.status(200).send("OK");

      // Process asynchronously
      queue.add("process-webhook", req.body);
    });
    ```
  </Accordion>

  <Accordion title="Handle duplicates with idempotency" icon="copy">
    Webhooks may be delivered more than once. Use the `webhook_id` field for idempotency:

    ```javascript theme={null}
    async function handleWebhook(payload) {
      const exists = await db.webhooks.findOne({
        webhookId: payload.webhook_id
      });

      if (exists) return; // Already processed

      await processEvent(payload);
      await db.webhooks.insert({ webhookId: payload.webhook_id });
    }
    ```
  </Accordion>

  <Accordion title="Always verify signatures" icon="shield-check">
    Never process a webhook without verifying the `X-WEBHOOK-SIGN` header. See [Security](#security) above.
  </Accordion>

  <Accordion title="Track the task-status-updated event" icon="list-check">
    The `task-status-updated` event is the primary signal for monitoring connection progress. Key statuses to handle:

    * **`done`**: All data has been downloaded and processed. Fetch verification reports.
    * **`login_error`**: Authentication failed. The user may need to retry.
    * **`mfa_error`**: MFA verification failed.
    * **`config_error`**: Provider configuration issue.

    See [Connection Lifecycle](/api-reference/tasks/lifecycle) for the full status flow.
  </Accordion>
</AccordionGroup>

***

## Troubleshooting

| Issue                          | Solution                                                                                                                                                                                           |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Not receiving webhooks         | Verify your URL is configured under **Development** → **Webhooks** for the correct environment                                                                                                     |
| Signature verification failing | Ensure you're using your **Access Secret** (not Client ID) and hashing the raw body                                                                                                                |
| Timeouts                       | Respond with `200` immediately, process asynchronously                                                                                                                                             |
| Failed deliveries              | Each event is retried up to 3 times (30s apart), then marked failed. Use the [Webhook request history](/api-reference/webhooks/object#webhook-request-history) to find and debug failed deliveries |

## Events by object

**Orders** <small>([see more](/api-reference/orders/events))</small>

| Event                     | Trigger                                            |
| ------------------------- | -------------------------------------------------- |
| `order-created`           | Order first created, before any status transitions |
| `order-status-updated`    | Order status changes                               |
| `order-refresh-failed`    | Refresh task fails for an order                    |
| `order-finalized`         | Order group reaches its terminal, final state      |
| `certification-completed` | Applicant submits their income self-certification  |

**Tasks** <small>([see more](/api-reference/tasks/events))</small>

| Event                 | Trigger                 |
| --------------------- | ----------------------- |
| `task-status-updated` | Task status transitions |

**Bank Accounts** <small>([see more](/api-reference/bank-accounts/events))</small>

| Event                   | Trigger                          |
| ----------------------- | -------------------------------- |
| `bank-accounts-created` | First bank account discovery     |
| `bank-accounts-updated` | Bank accounts changed on refresh |

**Shift** <small>([see more](/api-reference/shifts/events))</small>

| Event            | Trigger                   |
| ---------------- | ------------------------- |
| `shifts-created` | First shift extraction    |
| `shifts-updated` | Shifts changed on refresh |

**Pay Statement** <small>([see more](/api-reference/statements/events))</small>

| Event                | Trigger                   |
| -------------------- | ------------------------- |
| `statements-created` | First statement retrieval |
| `statements-updated` | New statements on refresh |

**Identity** <small>([see more](/api-reference/identity/events))</small>

| Event             | Trigger                    |
| ----------------- | -------------------------- |
| `profile-created` | First profile extraction   |
| `profile-updated` | Profile changed on refresh |

**Employment** <small>([see more](/api-reference/employment/events))</small>

| Event                | Trigger                       |
| -------------------- | ----------------------------- |
| `employment-created` | First employment extraction   |
| `employment-updated` | Employment changed on refresh |

**Links** <small>([see more](/api-reference/links/events))</small>

| Event               | Trigger                      |
| ------------------- | ---------------------------- |
| `link-connected`    | Successful connection        |
| `link-disconnected` | Refresh connection failure   |
| `link-deleted`      | Data and credentials removed |
