Skip to main content

Webhooks

The Webhooks API allows you to subscribe to events happening in an ITX account. Rather than making an API call when an event happens in a connected corporation group, ITX can send an HTTP request to a configured endpoint. Webhooks can be more scalable than regularly polling for changes, especially for apps with a large install base.

Using the Webhooks API requires the following:

  • You must deploy a publicly available and secure (HTTPS) endpoint for that URL that can handle the webhook payloads specified in this documentation. Note that your endpoint should not enforce any kind of dynamic authentication. Learn more about validating requests in the security section below.

Webhook settings and subscriptions

Before configuring webhooks, you need to prepare a URL to send those notifications to.

As for now, webhooks and related subscriptions must be configured by ITX.

Webhook subscriptions

Webhook subscriptions tell ITX which events you would like to receive. As for now subscription must be set up by ITX.

The following subscription types are supported:

SUBSCRIPTION TYPEDESCRIPTION
customer.creationGet notified if any customer is created.
customer.updateGet notified if any customer is updated.
customer.deletionGet notified if any customer is deleted.
contactperson.creationGet notified if any contact person is created.
contactperson.updateGet notified if any contact person is updated.
contactperson.deletionGet notified if any contact person is deleted.
contactperson.customerAssociationCreationGet notified if any contact person has been associated with a customer.
contactperson.customerAssociationDeletionGet notified if any contact person is no longer associated with a customer.

Webhook payloads

The endpoint at the target URL that you specify in your app's webhooks settings will receive POST requests containing JSON formatted data from ITX.

To ensure that the requests you're getting at your webhook endpoint are actually coming from ITX, ITX populates a X-ITX-Signature header with a SHA-256 hash built using your client secret combined with details of the request. Learn more about validating request signatures in the security section below.

Use the table below to view details about fields that may be contained in the payload.

FieldDescription
eventIdThe ID of the event that triggered this notification.
subscriptionTypeThe type of subscription this notification is for. Review the list of supported subscription types in the section above.
objectIdThe ID of the object that was created, updated, or deleted.
corpIdThe customer's ITX corporation ID where the event occurred.
appIdThe ID of your application. This can be used in case you have multiple applications pointing to the same webhook URL.
occurredAtWhen this event occurred as an ISO timestamp with millisecond precision.
triggeredByUserIdThe ID of the user that triggered this notification, if applicable.
FieldDescription
fromObjectIdThe ID of the record that the association change was made from.
toObjectIdThe ID of the secondary record in the association event.
{
"events": [
{
"eventId": "itx.event.2b55af58-adc7-4433-9968-f6fbeefe61fb",
"subscriptionType": "customer.creation",
"objectId": 1246965,
"corpId": 600000012,
"occurredAt": "2025-01-15T13:37:00.000Z",
"triggeredByUserId": 123
}
]
}

As shown above, you should expect to receive an array of objects in a single request. The batch size can vary, but will be under 100 notifications. ITX will send multiple notifications when many events have occurred within a short period of time. For example, if you've subscribed to new customers and a large number of customers are imported, ITX will send you the notifications for these imported contacts in batches and not necessarily one per request.

ITX does not guarantee that you'll receive these notifications in the order they occurred. Use the occurredAt property for each notification to determine when the event that triggered the notification occurred.

Security

To ensure that the requests you're getting at your webhook endpoint are actually coming from ITX, ITX populates a X-ITX-Signature header with a SHA-256 hash of the concatenation of the app secret for your application and the request body ITX is sending. Contact ITX to receive your client secret. Note that the secret you receive will be a base64 encoded string. To use the secret to validate requests in your application it must first be base64 decoded into a byte array.

To verify this signature, concatenate the app secret of your application and the un-parsed request body of the request you're handling, and get a SHA-256 hash of the result. Compare the resulting hash with the value of the X-ITX-Signature. If these values match, then this verifies that this request came from ITX. Or, the request came from someone else who knows your application secret. It's important to keep this value secret.

If these values do not match, then this request may have been tampered with in-transit or someone may be spoofing webhook notifications to your endpoint.

Validating the request signature

The X-ITX-Signature-v1 header will be an HMAC SHA-256 hash built using the client secret of your app combined with details of the request. It will also include a X-ITX-Request-Timestamp header. The X-ITX-Request-Timestamp header contains a Unix timestamp in milliseconds.

When validating a request using the X-ITX-Signature-v1 header, you'll need to

  • Reject the request if the timestamp is older than 5 minutes.

  • Create a utf-8 encoded string that concatenates together the following: requestMethod + requestUrl + requestBody + timestamp. The timestamp is provided by the X-ITX-Request-Timestamp header.

  • Create an HMAC SHA-256 hash of the resulting string using the application secret as the secret for the HMAC SHA-256 function.

  • Base64 encode the result of the HMAC function.

  • Compare the hash value to the signature. If they're equal then this request has been verified as originating from ITX. It's recommended that you use constant-time string comparison to guard against timing attacks.

The Node.js code snippet below details how you could incorporate request validation for a POST request if you were running an Express server to handle incoming requests. Keep in mind that the code block below is an example and omits certain dependencies you might need to run a fully-featured Express service. Confirm that you're running the latest stable and secure libraries when implementing request validation for your specific service.

// Introduce any dependencies. Only several dependencies related to this example are included below:
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
const port = process.env.PORT || 4000;

app.use(bodyParser.urlencoded({ extended: false}));
app.use(bodyParser.json());

app.post('/webhook-test', (request, response) => {
response.status(200).send('Received webhook subscription trigger');

const {
url,
method,
body,
headers,
hostname
} = request;

// Parse headers needed to validate signature
const signatureHeader = headers["x-itx-signature-v1"]
const timestampHeader = headers["x-itx-request-timestamp"];

// Validate timestamp
const MAX_ALLOWED_TIMESTAMP = 300000; // 5 minutes in milliseconds
const currentTime = Date.now();
if (currentTime - timestamp > MAX_ALLOWED_TIMESTAMP) {
console.log("Timestamp is invalid, reject request");
// Add any rejection logic here
}

// Concatenate request method, URI, body, and header timestamp
const uri = `https://${hostname}${url}`;
const rawString = `${method}${uri}${JSON.stringify(body)}${timestamp}`;

// Create HMAC SHA-256 hash from resulting string above, then base64-encode it
const hashedString = crypto.createHmac("sha256", process.env.CLIENT_SECRET).update(rawString).digest("base64");

// Validate signature: compare computed signature vs. signature in header
if (crypto.timingSafeEqual(Buffer.from(hashedString), Buffer.from(signatureHeader)) {
console.log("Signature matches! Request is valid.");
// Proceed with any request processing as needed.
} else {
console.log("Signature does not match: request is invalid");
// Add any rejection logic here.
}
});