Learn how to use webhooks to automate actions based on specified events.


Overview

Webhooks are automated messages 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's being processed. After a successful payroll connection, data and PDF documents from the payroll provider need time to be processed.

To avoid delaying the user experience or the API responses, you can use webhooks to get updates when the status of a Task changes.

Subscribing

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

Testing

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 payloads in your local environment or MockBin to test webhooks in your browser.

Payload

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

NameInDescription
user-agentheaderAlways set to Truv-Webhook-Client/2.0
x-webhook-signheaderA hash of the created request body created with an 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
user_idbodyThis is the user_id that is set when the bridge token is created

Note: All header field names are case-insensitive. For more information, you can reference the HTTP/1.1 Specifications, Section 3.2.

Timing

While on the Truv side, webhook requests for Task statuses are sent in the same order the statuses are updated (e.g., a full_parse event happens before a done event). But many factors can influence the delivery of webhook requests, such as network latency, outages, etc.

As a result, it's possible not to receive webhook events in the proper order, so it's important when implementing your code for receiving webhook requests from Truv that you reference the updated_at field to track which events happened when.

Security

For you to verify that data through a 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 hexadecimal.

  2. Compare the hash created to the value in the X-WEBHOOK-SIGN header sent with the webhook request. If they match, 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(
        key=key.encode('utf-8'),
        msg=payload.encode('utf-8'),
        digestmod=hashlib.sha256,
    ).hexdigest()
    return f'v1={generated_hash}'

@app.route('/webhook', methods=['POST'])
def webhook():
    return generate_webhook_sign(request.data.decode('UTF-8'), secret)
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "fmt"
  "strings"
)

func generate_webhook_sign(body string, key string) string {

  mac := hmac.New(sha256.New, []byte(key))
  mac.Write([]byte(body))
  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.
app.use(bodyParser.json({
  verify: (req, res, buf) => {
    req.rawBody = buf
  }
}))

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

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

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

  res.send(`v1=${webhook_sign}`).end()
})
class Webhook
  def self.generate_webhook_sign(body, key)
    digest = OpenSSL::Digest.new('sha256')
    return "v1=" + OpenSSL::HMAC.hexdigest(digest, key, body)
  end

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

namespace c_sharp.Controllers
{
  [ApiController]
  [Route("webhook")]
  public class WebhookController : ControllerBase
  {

    [HttpPost]
    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 and Retries

We have strict HTTP request timeouts. Your endpoint should return a successful status code 2xx within 10 seconds. We will honor any redirects signaled with a 3xx status code. Response status codes 4xx and 5xx will be treated as an error. When a request is unsuccessful, we will retry the request no more than three times, with a 30 second delay between each retry. After three unsuccessful retries, we will mark the request as failed and stop further attempts.

Task status change

Sample webhook payload for "task-status-updated":

{
   "webhook_id":"488f424096d3461aa0e4cf11a985f6df",
   "task_id":"521442a087604e599c637db310d07402",
   "link_id":"d39d86dcc20c46e0ae75ba9ab1311a21",
   "product":"income",
   "tracking_info":null,
   "event_type":"task-status-updated",
   "event_created_at":"2022-08-24T13:55:12.845645Z",
   "updated_at":"2022-08-24T13:55:12.845666+00:00",
   "status":"parse"
}

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