Skip to main content
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.
Configure webhook endpoints separately for Sandbox and Production.

Set up webhooks

Configure webhook endpoints in the Truv Dashboard:
  1. Navigate to DevelopmentWebhooks
  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 to expose a local development server:
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

HeaderValue
User-AgentTruv-Webhook-Service/2.0
X-WEBHOOK-SIGNHMAC-SHA256 signature for verification
HTTP header names are case-insensitive per RFC 7230.

Body fields

Every webhook payload includes these common fields:
FieldDescription
webhook_idUnique identifier for this webhook request
event_typeThe event classification
event_created_atTimestamp when the event occurred
user_idAssociated Truv user identifier
Additional fields vary by event type — see 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
Always verify signatures using the raw request body, not parsed JSON. Parsing and re-serializing may change the byte representation.

Code examples

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)
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");
});
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))
}
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
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;
    }
}

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 if you need mutual certificate-based authentication for webhook delivery.

Timeouts and retries

BehaviorDetail
TimeoutYour endpoint must respond with a 2xx status within 10 seconds
Redirects3xx responses are followed
Retries4xx and 5xx responses trigger up to 3 retry attempts at 30-second intervals
Failed deliveryAfter 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.
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.

Handle webhooks

Best practices

Return a 200 response quickly. Process the webhook data asynchronously if needed:
app.post("/webhooks/truv", (req, res) => {
  // Acknowledge immediately
  res.status(200).send("OK");

  // Process asynchronously
  queue.add("process-webhook", req.body);
});
Webhooks may be delivered more than once. Use the webhook_id field for idempotency:
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 });
}
Never process a webhook without verifying the X-WEBHOOK-SIGN header. See Security above.
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 for the full status flow.

Troubleshooting

IssueSolution
Not receiving webhooksVerify your URL is configured under DevelopmentWebhooks for the correct environment
Signature verification failingEnsure you’re using your Access Secret (not Client ID) and hashing the raw body
TimeoutsRespond with 200 immediately, process asynchronously
Failed deliveriesEach event is retried up to 3 times (30s apart), then marked failed. Use the Webhook request history to find and debug failed deliveries

Events by object

Orders (see more)
EventTrigger
order-createdOrder first created, before any status transitions
order-status-updatedOrder status changes
order-refresh-failedRefresh task fails for an order
order-finalizedOrder group reaches its terminal, final state
certification-completedApplicant submits their income self-certification
Tasks (see more)
EventTrigger
task-status-updatedTask status transitions
Bank Accounts (see more)
EventTrigger
bank-accounts-createdFirst bank account discovery
bank-accounts-updatedBank accounts changed on refresh
Shift (see more)
EventTrigger
shifts-createdFirst shift extraction
shifts-updatedShifts changed on refresh
Pay Statement (see more)
EventTrigger
statements-createdFirst statement retrieval
statements-updatedNew statements on refresh
Identity (see more)
EventTrigger
profile-createdFirst profile extraction
profile-updatedProfile changed on refresh
Employment (see more)
EventTrigger
employment-createdFirst employment extraction
employment-updatedEmployment changed on refresh
Links (see more)
EventTrigger
link-connectedSuccessful connection
link-disconnectedRefresh connection failure
link-deletedData and credentials removed