Introduction
In this tutorial, you learn how to use the Webhooks API V2 to create and manage your webhooks. You also learn how to validate the webhook signature and process received event messages in your application.
Prerequisites
This tutorial assumes that you already have:
- Basic knowledge of webhooks and how they work.
- Experience with web application deployment or using Heroku/Netlify.
- Registered your own Service Application on the iTwin Platform. You can find the steps to register a Service Application here. The Webhooks API V2 can only be called by Service Applications.
Suggested materials
We recommend using this editor and debugger tool for developing iTwin.js applications. It is free, open source, and includes a GUI for working with GIT.
If you want to test the REST API calls directly, use Postman or any other tool capable of sending HTTP requests. You will need an authorization token for the requests to work.
This tutorial uses Heroku to deploy the test application.
1. Create a webhook
Webhooks allow you to subscribe to events happening in the iTwin Platform. Webhooks provide an easy way to automate workflows inside the iTwin Platform.
You can create a webhook in two scopes: Account
and iTwin
. The scope
property on the Webhook controls this.
Account Webhooks
All webhooks created with the Account scope are scoped to the account of the Service Application that created them. This means all event types the webhooks subscribe to that happen inside that account will be sent to the webhook. Note that Service Application accounts are separate from the user's account that created the Service Application. The account is created when the Service Application is created.
Request
Send a POST request to the Create webhook endpoint:
Request Body
The request body is defined in the Create webhook documentation.
To create an iTwin webhook, set the following properties:
- callbackUrl - A public endpoint of your application where you expect the event to be sent.
- eventTypes - A list of event types you want to subscribe to. You can find a full list here.
- secret (optional) - At least a 32-character string value. Used to validate the request to the callback URL. If no value is given, a secret will be generated and returned. For more information, see here.
- scope - The scope of the events that will be received.
{
"callbackUrl":"https://HOSTNAME/events",
"scope": "account",
"secret": "optional-32-character-value"
"eventTypes":[
"iModels.iModelDeleted.v1",
"accessControl.memberAdded.v1"
]
}
Response
On a successful request, the operation returns HTTP status code 202 (Accept), indicating that the webhook has been successfully created.
{
"webhook": {
"id": "00000000-0000-0000-0000-000000000000",
"scope": "Account",
"scopeId": "00000000-0000-0000-0000-000000000000",
"active": false,
"callbackUrl": "https://HOSTNAME/events",
"secret": "<secret value>",
"eventTypes": ["iModels.iModelDeleted.v1", "accessControl.memberAdded.v1"]
}
}
On a successful response, you will receive the webhook secret if you did not provide one in the request. You will need it later to validate received events. Store the secret in your application storage to prepare for receiving events. For this tutorial, use it to replace the const secret
value in function validateSignature
from the "Add event authentication" section in the "Create your application" step.
iTwin Webhooks
iTwin scoped Webhooks receive events for anything created directly inside that iTwin. To create an iTwin level webhook, the Service Application must have the webhooks_maintainer
permission assigned at the iTwin level or be an Organization Administrator for the Organization that owns the given iTwin. For more information, see the Authorizing Service Applications for Managing iTwin Level Webhooks tutorial.
If you create the webhook with the iTwin scope, then the property scopeId
is required.
Request
Send a POST request to the Create webhook endpoint:
Request Body
The request body is defined in the Create webhook documentation.
To create an iTwin webhook, set the following properties:
- callbackUrl - A public endpoint of your application where you expect the event to be sent.
- eventTypes - A list of event types you want to subscribe to. You can find a full list here.
- secret (optional) - At least a 32-character string value. Used to validate the request to the callback URL. If no value is given, a secret will be generated and returned. For more information, see here.
- scope - The scope of the events that will be received.
- scopeId - The ID of the iTwin.
{
"callbackUrl":"https://HOSTNAME/events",
"scope": "itwin",
"scopeId": "00000000-0000-0000-0000-000000000000",
"secret": "optional-32-character-value"
"eventTypes":[
"iModels.iModelDeleted.v1",
"accessControl.memberAdded.v1"
]
}
Response
On a successful request, the operation returns HTTP status code 202 (Accept), indicating that the webhook has been successfully created.
{
"webhook": {
"id": "00000000-0000-0000-0000-000000000000",
"scope": "iTwin",
"scopeId": "00000000-0000-0000-0000-000000000000",
"active": false,
"callbackUrl": "https://HOSTNAME/events",
"secret": "<secret value>",
"eventTypes": ["iModels.iModelDeleted.v1", "accessControl.memberAdded.v1"]
}
}
On a successful response, you will receive the webhook secret if you did not provide one in the request. You will need it later to validate received events. Store the secret in your application storage to prepare for receiving events. For this tutorial, use it to replace the const secret
value in function validateSignature
from the "Add event authentication" section in the "Create your application" step.
2. Activate a webhook
To start receiving events to your webhook, you must first activate it. By default, webhooks are created inactive. This step is the same for both Account and iTwin Webhooks.
Request
Send a PATCH request to the Update webhook endpoint:
- Include an Authorization header with a valid Bearer token.
- Only Service Applications can call the Webhooks API V2. For more information on Service Applications and how to obtain an access token, see here. You can find a list of your Service Applications here.
- You can find the
webhookId
from the response of the Get webhooks operation.
Request Body
The request body is defined in the Update webhook documentation.
To activate a webhook, set the active
field to true
.
{
"active": true
}
Response
On a successful request, the operation returns HTTP status code 200 (OK), indicating that the webhook is successfully updated.
{
"webhook": {
"id": "00000000-0000-0000-0000-000000000000",
"scope": "Account",
"scopeId": "00000000-0000-0000-0000-000000000000",
"active": true,
"callbackUrl": "https://HOSTNAME/events",
"secret": "<secret value>",
"eventTypes": ["iModels.iModelDeleted.v1", "accessControl.memberAdded.v1"]
}
}
Your webhook is now active and ready to start sending events. In the next step, we will create an application to receive and react to events.
3. Create your application
Since webhooks send events via HTTP requests, you need to have an application running that exposes a public HTTP endpoint. The callback URL
we set in the Create Webhook step is this public endpoint. In this tutorial, we use Node.js together with Express to create a test application to capture and react to our events.
Initialize the project
To start off, create a new directory for your application and execute the following initialization commands. These commands will initialize a new npm project, install the required dependencies, and configure TypeScript.
cd your-project
npm init -y
npm install express
npm install -D typescript @types/express
tsc --init
After initialization, update the freshly generated tsconfig.json
file by setting the outDir
property to "dist"
.
"outDir":"dist"
Next, update the package.json
file to set the application entry point and start
script. Once done, the project is ready for the next step.
"main":"dist/index.js",
"scripts":{
"start":"tsc && node dist/index.js"
}
Create express server
Now let's start implementing the application. First, create a new file in your project directory src/index.ts
. This will be the application's starting point. In the example below, you can see that we will have a public HTTP endpoint that accepts POST requests app.post("/events", () => {})
. This is because event messages are sent using the POST method. Note the line app.use(express.text({ type: "application/json" }))
above, which makes the server treat requests with JSON content as text and not deserialize them initially because the raw payload will be required for event authorization in one of the upcoming steps.
import express from 'express';
const app = express();
app.use(express.text({ type: 'application/json' }));
app.post('/events', () => {
// Handle the event
});
const port = 5000;
app.listen(port, () => {
console.log('Application was started.');
});
Add event authentication
To authenticate the event source, we need to add event signature validation. The event signature is an HMAC-SHA256
string included in the request Signature
header. For validation, we will use Node.js crypto
utility, which lets us generate the same type of signature on our end. The generated signature and the signature included in the request should match to pass the authentication. Let's start adding validation by creating a new function function validateSignature(payload: string, signatureHeader: string)
. As the first parameter, it will expect the raw request payload, and as the second parameter, it will expect the signature header value. This function will also need the webhook secret
, which we created before in the Create Webhook step. This value can be updated later using the Update Webhooks endpoint. Since the signature header value contains the cryptographic algorithm name and the signature value separated by =
, we need to extract these values into separate variables const [algorithm, signature] = signatureHeader.split("=")
. Then, using all the existing variables, we can generate a signature crypto.createHmac(algorithm, secret).update(payload, "utf-8").digest("hex")
. Lastly, we need to check if both signatures match and return the result.
import crypto from "crypto";
. . .
function validateSignature(payload: string, signatureHeader: string): boolean {
// Replace with your own webhook secret later
const secret = "<secret value>";
const [algorithm, signature] = signatureHeader.split("=");
const generated_sig = crypto.createHmac(algorithm, secret).update(payload, "utf-8").digest("hex");
return generated_sig.toLowerCase() === signature.toLowerCase();
}
Define data models
Before we can start receiving the events, we need to prepare the models for the expected data. You can find the schema for the base event and all other available events here. Create a new file src/models.ts
and create event types by matching the schema. For this example, we will create two event types: iModels.iModelDeleted.v1
and accessControl.memberAdded.v1
.
export type Event = {
content: iModelDeletedEvent | NamedVersionCreatedEvent;
eventType: string;
enqueuedDateTime: string;
messageId: string;
webhookId: string;
iTwinId: string;
};
export type iModelDeletedEvent = {
imodelId: string;
userId: string;
};
export type MemberAddedEvent = {
memberId: string;
eventCreatedBy: string;
memberType: string;
roleId: string;
roleName: string;
};
Event handling
Now that we have everything ready for event handling, we can start implementing it. First, we want to validate that the request you receive came from our Webhooks Service. You can do this by checking the signature header. If the request either does not contain a signature header or a request body, you can go ahead and return '401 Unauthorized' if (!signatureHeader || !req.body) res.sendStatus(401)
. If the request does have these components, then we can proceed with further processing logic and try to validate the event signature using the function we defined previously if (!validateSignature(req.body, signatureHeader)) res.sendStatus(401)
. If validation fails, we can assume that the event was sent from an unexpected source and safely return '401 Unauthorized' as well.
If we do not receive a response within 5 seconds, we will count that request as failed and start the retry procedure. To avoid any inadvertent timeouts, we suggest validating the request, putting any work you will be doing in reaction to the event on a separate thread, and then returning '200 OK'. More information about the retry procedure can be found here.
import { Event, NamedVersionCreatedEvent } from './models';
app.post('/events', (req, res) => {
const signatureHeader = req.headers['signature'] as string;
if (!signatureHeader || !req.body) res.sendStatus(401);
if (!validateSignature(req.body, signatureHeader)) {
res.sendStatus(401);
} else {
const event = JSON.parse(req.body) as Event;
switch (event.eventType) {
case 'accessControl.memberAdded.v1': {
const content = event.content as MemberAddedEvent;
console.log(
`Member (Id:${content.memberId}) was added to iTwin (${event.iTwinId})! Member was granted the ${content.roleName} role (Id: ${content.roleId}).`
);
break;
}
default:
res.sendStatus(400); //Unexpected event type
}
}
res.sendStatus(200);
});
Deploy
For this application to work, you need to deploy it to be publicly accessible. If you have any preferences for the deployment, go ahead and use your own deployment method and platform. If not, you can follow the tutorial and deploy the application using Heroku:
- Create a Heroku Remote.
- Deploy by pushing the code.
- Use
heroku logs --tail
to monitor the behavior of the application.
Once you have the application deployed and running, you will start receiving events if you have followed this tutorial and created and activated your webhook.