Skip to content

General guidelines for processing Truto webhook events.

Webhook event payload structure

The webhook event is a JSON object with the following properties. When a sync job sends file contents as sync_job_run:record records, the same fields arrive as a multipart/form-data body instead.

Body

  • id is the unique ID for the webhook event.
  • event is the name of the event. E.g, integrated_account:created. It's in the format truto_entity:action, where truto_entity can be a resource in Truto and action can be created, updated, deleted, etc.
  • payload is the data associated with the action. In most cases, it will be updated resource. For record:* events it holds resource, records, integrated_account_id, raw_event_type, and raw_payload (the event as the integration sent it).
  • environment_id is the environment this webhook event is associated with.
  • created_at is the time when this webhook event was generated.
  • webhook_id is the ID of the webhook associated with this event.

An example webhook event sent when a new integrated account is connected is shown below,

{
  "id": "3a0da6ba-b2d1-473f-957c-51f6825e3623",
  "event": "integrated_account:created",
  "payload": {
    "id": "79a39d69-e27e-49cb-b9a9-79f5eea7aa26",
    "tenant_id": "acme-1",
    "environment_integration_id": "27a8c0ff-0c2e-4383-b651-0772b3515921",
    "context": {},
    "created_at": "2023-06-16T09:21:21.000Z",
    "updated_at": "2023-06-16T09:21:21.000Z",
    "is_sandbox": false,
    "unified_model_override": {},
    "environment_id": "ac15abdc-b38e-47d0-97a2-69194017c177",
    "integration": {
      "id": "1fa47bf3-5f1f-4b65-bcd0-8d07ab455e15",
      "name": "helpscout",
      "config": {
        "label": "Help Scout"
      }
    },
    "environment_integration": {
      "id": "27a8c0ff-0c2e-4383-b651-0772b3515921",
      "integration_id": "1fa47bf3-5f1f-4b65-bcd0-8d07ab455e15",
      "environment_id": "ac15abdc-b38e-47d0-97a2-69194017c177",
      "show_in_catalog": true,
      "is_enabled": true,
      "created_at": "2023-02-16T09:27:16.000Z",
      "updated_at": "2023-02-16T09:27:16.000Z"
    }
  },
  "environment_id": "ac15abdc-b38e-47d0-97a2-69194017c177",
  "created_at": "2023-06-16T09:21:22.369Z",
  "webhook_id": "077ec306-5756-43fa-9a06-0cc0da4eabe0"
}

For security, Truto adds a X-Truto-Signature header which contains the URL-safe Base64 encoded SHA256 HMAC of the raw request body, without = padding. For the request body shown above, the following header will be added to the request,

X-Truto-Signature: format=sha256,v=WDHLSeu0EEcUf9YV5N659YZ9EFOnfuIOrLj8UMBDhOo

Verification of the webhook event

You can get the SHA256 HMAC in the v part of the header value and do the following steps to make sure the webhook event is legit and sent from Truto,

  1. Store the v value sent as part of X-Truto-Signature header. WDHLSeu0EEcUf9YV5N659YZ9EFOnfuIOrLj8UMBDhOo in the example above.
  2. Calculate the SHA256 HMAC of the request body using the secret attribute returned to you when you had created the webhook. IMPORTANT: Please use the raw bytes received as the body and not the JSON parsed version of it that most frameworks return you in the middleware function. For multipart/form-data deliveries, sign the raw multipart body exactly as received.
  3. Calculate the Base64 encoded version of the data calculated in step 2. Make sure that the Base64 encoding is URL safe (- and _ in place of + and /) and has no = padding.
  4. Compare the value obtained in step 3 with the value stored in step 1. They should be equal. If they are not, then the payload has been tampered with and the webhook event is not generated by Truto.
const crypto = require("crypto");
 
function verifyWebhook(headerValue, secret, body) {
  // Reject requests without a signature header
  if (typeof headerValue !== "string" || !headerValue.includes("v=")) {
    return false;
  }
 
  // Get HMAC from header
  const hmacFromHeader = Buffer.from(headerValue.split("v=")[1]);
 
  // Calculate the SHA256 HMAC of the request body, URL-safe Base64 without padding
  const calculatedHmac = Buffer.from(
    crypto.createHmac("sha256", secret).update(body).digest("base64url")
  );
 
  // timingSafeEqual throws when lengths differ
  if (hmacFromHeader.length !== calculatedHmac.length) {
    return false;
  }
  return crypto.timingSafeEqual(hmacFromHeader, calculatedHmac);
}
 
//  Note: You need to pass the raw body of the webhook request to the verifyWebhook function. If you are using Express.js, you can get the raw body like this:
app.use(
  bodyParser.json({
    verify: function (req, res, buf) {
      req.rawBody = buf;
    },
  })
);
 
// And then in your route:
app.post("/webhook", (req, res) => {
  let isVerified = verifyWebhook(
    req.headers["x-truto-signature"],
    secret,
    req.rawBody
  );
  console.log(isVerified ? "Webhook Verified" : "Webhook Verification Failed");
});
public static bool VerifyWebhook(string headerValue, string secret, string body)
  {
    // Get HMAC from header
    var hmacFromHeader = Encoding.UTF8.GetBytes(headerValue.Split("v=")[1]);
    var secretBytes = Encoding.UTF8.GetBytes(secret);
    var bodyBytes = Encoding.UTF8.GetBytes(body);
 
    // Calculate the SHA256 HMAC of the request body
    var hmac = new HMACSHA256(secretBytes);
    var calculatedHmac = hmac.ComputeHash(bodyBytes);
    var base64OfCalculatedHmac = Convert.ToBase64String(calculatedHmac);
    string urlSafeBase64String = base64OfCalculatedHmac.Replace('+', '-').Replace('/', '_').TrimEnd('=');
 
    var generatedBytes = Encoding.UTF8.GetBytes(urlSafeBase64String);
 
    return CryptographicOperations.FixedTimeEquals(hmacFromHeader, generatedBytes);
  }