Overview
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.
Subscribing
Visit Truv Dashboard Development > Webhooks and add a URL for your environment. Each environment has 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 payload in your local environment or MockBin to test webhooks in your browser.
Payload
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:
Name | In | Description |
---|---|---|
X-WEBHOOK-SIGN | header | A hash of the request body created with an {{ definitions.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 |
Events
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 Name | Description |
---|---|
task_id | The identifier of the Task associated to 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 |
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 Name | Description |
---|---|
order_id | The identifier of the order |
order_number | Any info passed into the order_number for the order. Nullable. |
employer_id | Unique Employer ID of the order |
link_id | The identifier of the Link associated to the Task . Nullable. |
product | Which product the Task is for (employment or income ) |
status | The new order status following orders lifecycle |
Timing
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.
Security
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:
-
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 hexidecimal. -
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(
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
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.