Webhooks are automated messages that are sent by Truv as an HTTP POST request to a predefined URL when an event occurs on the platform.

We recommend using webhooks to track Task status changes while it is being processed. After a successful payroll connection, data and PDF documents that come from the payroll provider need time to be processed. To not delay the user experience or the API responses, you can use webhooks to get updates when the status of a Task changes.


Visit Truv Dashboard Development > Webhooks and add a URL for your environment. Each environment has separate URL and needs to be specified independently.


You can test hooks by running Truv Bridge in Emulator. In case you don't have an endpoint for webhooks today, we recommend using ngrok to test payload in your local environment or MockBin to test webhooks in your browser.


There are many fields available when Truv sends a request to a webhook endpoint and quite a few depend on the event that is being sent. Here's a list of the common fields that will occur in every call regardless of the event:

X-WEBHOOK-SIGNheaderA hash of the request body created with an {{ definitions.Access_key }}
webhook_idbodyA unique identifier for this specific webhook request
event_typebodyAn identifier of the event the webhook request is sent for
updated_atbodyThe time the event occurred


Task status change

Sample webhook payload for "task-status-updated"

    "webhook_id": "609a82aab21e4d9ba2569f35e9e8f26a",
    "event_type": "task-status-updated",
    "updated_at": "2021-04-26T13:02:20.369267+00:00",
    "task_id": "67f2924530564282bbaf6d27655e94a4",
    "link_id": "64f8e374949c4b769706028022626bf1",
    "product": "income",
    "tracking_info": "27266f35-bb54-44c3-8905-070641a0c0aa",
    "status": "login"

Event task-status-updated occurs whenever the status of a Task changes. When you receive a task-status-updated
event with a status of done all the data for the Task is downloaded and documents have been processed.
You can use the link_id value to locate the access_token in your system and retrieve the latest payroll data from the respective endpoint.

Field NameDescription
task_idThe identifier of the Task associated to the event
link_idThe identifier of the Link associated to the Task
productWhich product the Task is for (employment, income or admin)
tracking_infoAny info passed into the bridge_token for the Link . Nullable.
statusThe new task status from the Task Lifecycle

Order status change

Sample webhook payload for "order-status-updated"

    "webhook_id": "609a82aab21e4d9ba2569f35e9e8f26a",
    "event_type": "order-status-updated",
    "updated_at": "2021-04-26T13:02:20.369267+00:00",
    "order_id": "67f2924530564282bbaf6d27655e94a4",
    "order_number": "100",
    "employer_id": "56f8e374949c4b769706028022626zz1",
    "link_id": "64f8e374949c4b769706028022626bf1",
    "product": "income",
    "status": "completed"

Event order-status-updated occurs whenever the status of orders changes. When you receive a order-status-updated event with a status of completed it means the order has been successfully processed and a payroll account(s) is linked.

Field NameDescription
order_idThe identifier of the order
order_numberAny info passed into the order_number for the order. Nullable.
employer_idUnique Employer ID of the order
link_idThe identifier of the Link associated to the Task . Nullable.
productWhich product the Task is for (employment or income)
statusThe new order status following orders lifecycle


While on the Truv side webhook requests for Task statuses are sent in the same order the statuses are updated (ie. a full_parse event happens before a done event) there are many factors that can influence the delivery of webhook requests, such as network latency, outages, etc. As a result it's possible to not receive webhook events in the proper order so it's important when implementing your code to receive webhook requests from Truv that you reference the updated_at field to track which events happened when.


In order for you to be able to verify that data via webhook is coming from Truv we implemented webhook signatures. Every webhook request we send contains an X-WEBHOOK-SIGN header which is an HMAC hash of the request body using your Access key as the hashing key and SHA-256 as the hashing function.

Follow the steps below to verify the request is coming from us:

  1. Use a hash library to create an HMAC hash of the raw request body received by Truv using the Access key you normally place in the X-Access-Secret header as the hashing key. Make sure to use SHA-256 as the hash function and that the final hash is converted to hexidecimal.

  2. Compare the hash created to the value in the X-WEBHOOK-SIGN header sent with the webhook request. If they match you can rest assured that Truv sent the request and you can continue to process the webhook.

import hashlib
import hmac

def generate_webhook_sign(payload: str, key: str) -> str:
    generated_hash = hmac.new(
    return f'v1={generated_hash}'

@app.route('/webhook', methods=['POST'])
def webhook():
    return generate_webhook_sign(request.data.decode('UTF-8'), secret)
import (

func generate_webhook_sign(body string, key string) string {

  mac := hmac.New(sha256.New, []byte(key))
  return hex.EncodeToString(mac.Sum(nil))

func webhook(w http.ResponseWriter, r *http.Request) {
  b, _ := ioutil.ReadAll(r.Body)
  convertedBody := string(b)
  signature := generate_webhook_sign(convertedBody, os.Getenv("API_SECRET"))
  fullSignature := fmt.Sprintf("v1=%s", signature)

  fmt.Fprintf(w, fullSignature)
const crypto = require("crypto")

// ensure all request bodies are parsed to JSON. Callback function
// keeps a copy of the raw body for webhooks.
  verify: (req, res, buf) => {
    req.rawBody = buf

const generate_webhook_sign = (body, key) => {
  return crypto.createHmac("sha256", key)

app.post("/webhook", async (req, res) => {

  const body = req.rawBody.toString()
  const webhook_sign = generate_webhook_sign(body, API_SECRET)

class Webhook
  def self.generate_webhook_sign(body, key)
    digest = OpenSSL::Digest.new('sha256')
    return "v1=" + OpenSSL::HMAC.hexdigest(digest, key, body)

  def self.post(body)
    return self.generate_webhook_sign(body, Citadel.client_secret)
using System.Threading.Tasks;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Security.Cryptography;
using System;

namespace c_sharp.Controllers
  public class WebhookController : ControllerBase

    public async Task<string> Post()
      using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
        string body = await reader.ReadToEndAsync();
        return generateWebhookSign(body, Environment.GetEnvironmentVariable("API_SECRET"));

    private string generateWebhookSign(string body, string key)
      using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
        // Compute the hash of the input file.
        byte[] hashValue = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));

        return "v1=" + BitConverter.ToString(hashValue).Replace("-", "").ToLower();


HTTP Timeouts

We have strict HTTP request timeouts: 3 second for a connection timeout and 5 second for a read timeout (wait for the response after connection). The receiving API should respect those timeouts, otherwise the webhook events will not be received successfully.