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:
Name | In | Description |
---|---|---|
user-agent | header | Always set to Truv-Webhook-Client/2.0 |
x-webhook-sign | header | A hash of the created request body created with an Access key |
webhook_id | body | A unique identifier for this specific webhook request |
event_type | body | An identifier of the event the webhook request is sent for |
updated_at | body | The time the event occurred |
user_id | body | This 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:
-
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 useSHA-256
as the hash function and that the final hash is converted to hexadecimal. -
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 Name | Description |
---|---|
task_id | The identifier of the Task associated with the event. |
link_id | The identifier of the Link associated to the Task. |
product | Which product the Task is for (employment , income or admin ). |
tracking_info | Any info passed into the bridge_token for the Link . Nullable. |
status | The new task status from the Task Lifecycle. |